同一账号只能在一台设备登录实现思路。
注意:登录是在白名单(直接放行的接口)。生成的token携带账号信息。
1.用户每次登录生成token时,将账号当成key,token当成value,以token的过期时间存入redis中。
2.用户访问的时候,在拦截器解析token,获取账号,拿账号去redis中获取value,如果是value的token与当前用户携带过来的token一致就放行。如果不一致,则告诉前端重复登录,让前端清除token,跳转到登录页面。
3.用户在另一台设备登录时,也是存入redis,这样就刷新了token的值和redis的过期时间。
这是我的一个实现思路,有没有大佬可以指点,互相学习一下。
写了个小demo,主要代码如下:
1.拦截器代码:
package com.yblue.config;
import com.yblue.dto.Payload;
import com.yblue.dto.UserInfo;
import net.minidev.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* @author: JiaXinMa
* @description: 解决跨域和权限
* @date: 2021/3/26
*/
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
FilterProperties filterProperties;
@Autowired
JwtProperties jwtProps;
@Autowired
StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//表示接受任意域名的请求,也可以指定域名
response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
//该字段可选,是个布尔值,表示是否可以携带cookie
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*");
//预请求,这里可以不加,但是其他语言开发的话记得处理options请求
// 如:异步请求的时候前端发两次请求,其中一次是测试通不通
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
return true;
}
//获取当前访问的URL接口后的路径
String uri = request.getRequestURI(); //如:/api/user/xxx
//判断uri地址,如果为 /error 则可能是请求方式、参数、访问路径不对
if (uri.equals("/error")) {
this.responseError(response, 405, "请求方式、参数类型、访问地址错误、数据库方面异常!");
return false;
}
//1.判断当前访问路径是否在白名单列表中
List<String> allowPaths = filterProperties.getAllowPaths();
for (String allowPath : allowPaths) {
if (uri.contains(allowPath)) {
//如果在,直接放行了
return true;
}
}
//2.校验token合法性
Payload<UserInfo> payload = null;
try {
String token = request.getHeader(jwtProps.getCookie().getCookieName());
// String token = CookieUtils.getCookieValue(request, jwtProps.getCookie().getCookieName());
payload = JwtUtils.getInfoFromToken(token, jwtProps.getPublicKey(), UserInfo.class);
String redisToken = redisTemplate.opsForValue().get(payload.getInfo().getUsername());
if (token.equals(redisToken)) {
return true;
} else {
this.responseError(response, 401, "您的账号在另一台设备上登录,请及时修改密码!");
}
} catch (Exception e) {
this.responseError(response, 401, "登录失效或未登录!");
return false;
}
//3.如果你的想校验权限、角色,可以在你的userInfo里封装,然后在下面验证,不过我现在的userInfo没放
// UserInfo userInfo = payload.getInfo();
// List modules = userInfo.getModules();
// for (Module module : modules) {
// if (uri.equals(module.getUrl())) {//判断用户是否有访问路径的权限
// return true;
// } else {
// this.responseError(response, 403, "权限不足!");
// }
// }
return false;
}
/**
* @author: JiaXinMa
* @description: 响应错误代码 处理响应错误信息的方法,可以拿去用
* @date: 2021/8/23
*/
public void responseError(HttpServletResponse response, Integer code, String returnMessage) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("code", code);
jsonObject.put("msg", returnMessage);
response.getWriter().append(jsonObject.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
2.业务层代码:
package com.yblue.service;
import com.yblue.config.JwtProperties;
import com.yblue.config.JwtUtils;
import com.yblue.domain.User;
import com.yblue.dto.Payload;
import com.yblue.dto.UserInfo;
import com.yblue.exception.pojo.ExceptionEnum;
import com.yblue.exception.pojo.MdException;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author: JiaXinMa
* @description: 验证授权 Service层
* @date: 2021/3/25
*/
@Service
@Transactional
public class AuthService {
@Autowired
UserService userService;
@Autowired
private JwtProperties jwtProps;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* @author: JiaXinMa
* @description: 登录
* @date: 2021/5/28
*/
public Map login(String username, String password) {
try {
Map<String, Object> map = new HashMap();//存放token和用户信息
//1. 判断用户名和密码是否正确
User loginUser = userService.findUserByNameAndPwd(username, password);
UserInfo userInfo = new UserInfo(loginUser.getUserId(), loginUser.getName(), loginUser.getUsername());
//2.生成token
String token = this.generateToken(userInfo);
map.put(jwtProps.getCookie().getCookieName(), token);
map.put("userInfo", userInfo);
//3.将token放进redis中
redisTemplate.opsForValue().set(loginUser.getUsername(), token, 60, TimeUnit.MINUTES);
return map;
} catch (Exception e) {
throw new MdException(ExceptionEnum.INVALID_USERNAME_PASSWORD);
}
}
/**
* @author: JiaXinMa
* @description: 生成token
* @date: 2021/4/6
*/
public String generateToken(UserInfo info) {
//利用JwtUtils+私钥生成加密token
//以前是的工具类是通过cookies带过去了,然后现在他们说要放在请求头上,所以那些类名字没改,不妨碍业务的使用
return JwtUtils.generateTokenExpireInMinutes(info, jwtProps.getPrivateKey(), jwtProps.getCookie().getExpire());
}
}
3.控制层代码
package com.yblue.controller;
import com.yblue.domain.User;
import com.yblue.service.AuthService;
import com.yblue.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @author: JiaXinMa
* @description: 验证 控制器
* @date: 2021/5/28
*/
@Slf4j
@RestController
@RequestMapping("/api")
public class AuthController {
@Autowired
AuthService authService;
@Autowired
UserService userService;
@PostMapping("/auth/login")
public Map login(@RequestBody User user) {
log.info("/api/auth/login:{}", user);
return authService.login(user.getUsername(), user.getPassword());
}
@GetMapping("/user/findByUserId")
public User findByUserId(@RequestParam("userId") Integer userId) {
log.info("/api/user/findByUserId:{}", userId);
return userService.findByUserId(userId);
}
}
效果如下图:
第一次:
第二次:
如果有些小伙伴有更好的解决方案或者觉得该方案有什么不足的,可以提出来一起讨论交流。
想看更多精彩内容,可以关注我的博客园
我的博客园