单用户登录的实现比较简单,只要在session上动动手脚就行了
单用户登录有两种实现方式
1)当前用户登录验证成功时,如果该用户的账号已经登录,则挤掉已登录的账号,由该用户登录
2)当前用户登录验证成功时,如果该用户的账号已经登录,则提示当前用户该账号已登录,并且不允许当前用户登录
环境
系统:win10
IDE:sts4
springboot2.2.2.RELEASE、 jdk8、 maven3.3.9
挤掉已登录用户
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
server.port=8081
spring.mvc.view.prefix=/templates/
spring.mvc.view.suffix=.html
登录页是Bootstrap框架中的
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="images/favicon.ico">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="css/signin.css" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="/login" method="post">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<p style="color:red;" th:text="${failMsg}" th:if="${not #strings.isEmpty(failMsg)}"></p>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="text" id="uname" name="uname" class="form-control" placeholder="Username" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="pwd" name="pwd" class="form-control" placeholder="Password" required>
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
</form>
</body>
</html>
@Controller
public class DemoOneController {
@Autowired
DemoOneServiceImpl demoService;
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/home")
public String dashboard() {
return "dashboard";
}
@PostMapping("/login")
public String login(@RequestParam String uname, @RequestParam String pwd,
Map<String, Object>map, HttpSession session) {
if(demoService.validateLogin(uname, pwd, session)) {
//把当前用户放到session中
session.setAttribute("username_session", "xiao");
return "redirect:home";
}else {
map.put("failMsg", "用户名或密码错误");
return "index";
}
}
}
注意在 service 中用了静态的 HashMap,用来存储已登录用户的 session id,同一个的会话的 session id 是一样的,这是本例的重点!!!
@Service
public class DemoOneServiceImpl{
public static HashMap<String, String> onlineUsers = new HashMap<>();
public static HashMap<String, String> getOnlineUsers(){
return onlineUsers;
}
public boolean validateLogin(String uname, String pwd, HttpSession session) {
if(uname.equals("xiao")) {
//本例只验证登录用户,密码不管
//既然是挤掉已登录用户,那就不管三七二十一先把用户session都清除掉即可
session.removeAttribute("username_session");
//把当前用户的 session id 放到一个静态 map,在登录拦截器中需要验证使用
onlineUsers.put(uname, session.getId());
return true;
}else {
return false;
}
}
}
每次访问页面都会验证用户登录的有效性,这个拦截器就是用来做这个判断的。
一开始我是把封装对象 service 直接拿过来调用 getOnlineUsers() 获取 session id 的,但是实际运行的时候 demoService 是 null,没办法用。后来就换成了静态方法,通过类名来直接获取。
public class LoginInterceptor implements HandlerInterceptor{
// @Autowired
// DemoOneServiceImpl demoService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Object username = request.getSession().getAttribute("username_session");
if(username != null) {
// 如果有 session
HashMap<String,String> onlineUsers = DemoOneServiceImpl.getOnlineUsers();
String curSid = request.getSession().getId();
String onlineSid = onlineUsers.get(username);
if(onlineSid != null && !curSid.equals(onlineSid)) {
// 如果不是该用户在本地的session
request.setAttribute("failMsg", "您的账号已在别处登录,请重新登录或修改密码");
request.getRequestDispatcher("/index").forward(request, response);
return false;
}else {
return true;
}
}else {
//未登录,转发到登录页
request.setAttribute("failMsg", "未登录或登录过时,请登录");
request.getRequestDispatcher("/index").forward(request, response);
return false;
}
}
}
@Configuration
public class Myconfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 绑定静态资源
registry.addResourceHandler("/**").
addResourceLocations("classpath:/static/","classpath:/public/");
super.addResourceHandlers(registry);
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 定义要拦截和放行的请求、资源
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").
excludePathPatterns("/login","/","/index","/css/**");
super.addInterceptors(registry);
}
}
本例重点就是在 LoginInterceptor 和 DemoOneServiceImpl 里面动了 session 的判断逻辑,只要看这两个类就可以了。
接下来是演示环节
1)在 Chrome 浏览器登录 “xiao” 这个账号,成功登录,并在该页面多次刷新,无异常。
2)同时,另外打开火狐浏览器,登录 “xiao” 这个账号,成功登录,并在该页面多次刷新,无异常。
3)回到 Chrome 浏览器,刷新已登录页面时,回到了登录页,并红字给出了提示
如果账号已登录,提示不允许登录
主要修改 LoginInterceptor 和 DemoOneServiceImpl 类如下
public class LoginInterceptor implements HandlerInterceptor{
// @Autowired
// DemoOneServiceImpl demoService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Object username = request.getSession().getAttribute("username_session");
if(username != null) {
// 如果有 session
HashMap<String,String> onlineUsers = DemoOneServiceImpl.getOnlineUsers();
String curSid = request.getSession().getId();
String onlineSid = onlineUsers.get(username);
if(onlineSid != null && !curSid.equals(onlineSid)) {
// 如果不是该用户在本地的session
request.setAttribute("failMsg", "您的账号已在别处登录,请重新登录或修改密码");
request.getRequestDispatcher("/index").forward(request, response);
return false;
}else {
//这种情况session还没过期,账号又还是活跃状态,此时应该更新本账号的登录时间
long curTime = new Date().getTime();
onlineUsers.put(username + "_loginTime", "" + curTime/1000);
return true;
}
}else {
//未登录,转发到登录页
request.setAttribute("failMsg", "未登录或登录过时,请登录");
request.getRequestDispatcher("/index").forward(request, response);
return false;
}
}
}
为了更友好的错误提示,这个 service 类中 validateLogin() 的返回值类型被我改了,同时,controller 当然也要稍作调整
@Service
public class DemoOneServiceImpl{
public static HashMap<String, String> onlineUsers = new HashMap<>();
public static HashMap<String, String> getOnlineUsers(){
return onlineUsers;
}
public int validateLogin(String uname, String pwd, HttpSession session) {
if(uname.equals("xiao")) {
//本例只验证登录用户,密码不管
Date date = new Date();
//如果该账号已经登录,且session id不同,并且上个用户的session没有超时,则说明是有用户在别处已经登录了
if(onlineUsers.get(uname) != null && !session.getId().equals(onlineUsers.get(uname))) {
Long onlineUser_loginTime = new Long(onlineUsers.get(uname + "_loginTime"));
System.out.println("onlineUser_loginTime==" + onlineUser_loginTime);
long interval = date.getTime()/1000 - onlineUser_loginTime;
System.out.println("interval==" + interval);
if(interval <= 60) {
// 该用户已经登录且session还在有效期内
return 2;
}else {
// 账号虽然在别处已经登录,但是 他的session已经到期了,当前用户就可以登录该账号
onlineUsers.put(uname, session.getId());
onlineUsers.put(uname + "_loginTime", "" + date.getTime()/1000);
return 1;
}
}else {
//把当前用户的 session id 放到一个静态 map,在登录拦截器中需要验证使用
onlineUsers.put(uname, session.getId());
onlineUsers.put(uname + "_loginTime", "" + date.getTime()/1000);
return 1;
}
}else {
//账号密码验证失败
return 0;
}
}
}
这里只贴了改动的地方
@PostMapping("/login")
public String login(@RequestParam String uname, @RequestParam String pwd,
Map<String, Object>map, HttpSession session) {
int ansKey = demoService.validateLogin(uname, pwd, session);
if(1 == ansKey) {
//把当前用户放到session中
session.setAttribute("username_session", "xiao");
return "redirect:home";
}else if(2 == ansKey) {
map.put("failMsg", "该账号已登录,请不要重复登录");
return "index";
}else {
map.put("failMsg", "用户名或密码错误");
return "index";
}
}
为了测试方便,我把session的过期时间设置为了60秒
# 设置session有效时间为1分钟
server.servlet.session.timeout=60
现在就可以启动项目测试了
1)在 Chrome 浏览器登录 “xiao” 这个账号,成功登录,并在该页面多次刷新,无异常。
2)同时,60秒内,另外打开火狐浏览器,登录 “xiao” 这个账号,登录失败,提示“该账号已登录,请不要重复登录”,查看控制台输出
3)等 Chrome 浏览器的 session 过期,在火狐浏览器登录 “xiao” 这个账号,登录成功,查看控制台输出
等个60多秒,session过期,在火狐浏览器就可以正常登录了,而此时回到 chrome 浏览器刷新后会提示 “未登录或登录过时,请登录”
另一种情况就是当前账号一直处于活跃状态时,在本例中即60秒内至少刷新一次,测试另一个用户能不能登录该账号,我测试了是没问题的,无法登录,会提示 “该账号已登录,请不要重复登录”
有兴趣的朋友可以多试试其他情况,看看有没有异常情况,后面慢慢完善,这里我也不一 一贴图了,以上应该能满足基本需求了。