使用框架
springboot、spring security、 redis 、jwt
需求
一般地,通过token方式获取当前登录者的用户信息,因此需要创建一个token,如果token过期,用户需要重新登录,这样的体验非常差,需要做到用户无感知的情况下,解决刷新token的问题。
解决方案
登录后,创建一个新的token,并保存到redis,并设置过期时间(比如 缓存 过期时间为30分钟),在限定的时间内刷新token。
每次访问接口,刷新redis的缓存时间。
忽略springboot security的配置
添加maven
<!--Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
jwt token工具类
/**
* token验证处理
*
* @author chenxinhui
*/@ Component
public class TokenService {
// 令牌自定义标识
@Value("${token.header}")
private String header;
// 令牌秘钥
@Value("${token.secret}")
private String secret;
// 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private int expireTime;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
@Autowired
private RedisCache redisCache;
/**
* 获取用户身份信息
*
* @return 用户信息
*/ public LoginUser getLoginUser(HttpServlet request request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
// String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
// String userKey = getTokenKey(uuid);
// return redisCache.getCacheObject(userKey);
return JsonUtils.toObject(JsonUtils.toJson(claims.get(Constants.USER_INFO_INFO)), LoginUser.class);
}
return null;
}
/**
* 设置用户身份信息
*/ public void setLoginUser(LoginUser loginUser) {
if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
refreshToken(loginUser);
}
}
/**
* 删除用户身份信息
*/ public void delLoginUser(String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/ public String createToken(LoginUser loginUser) {
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map <String, Object> claims = new HashMap<>(16);
claims.put(Constants.LOGIN_USER_KEY, token);
claims.put(Constants.USER_INFO_INFO, loginUser);
return createToken(claims);
}
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/ public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUser);
}
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/ public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
/**
* 设置用户代理信息
*
* @param loginUser 登录信息
*/ public void setUserAgent(LoginUser loginUser) {
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/ private String createToken(Map<String, Object> claims) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 30);
System.out.println(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, calendar.getTime()));
return Jwts.builder()
.setClaims(claims)
// .setExpiration(calendar.getTime())
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/ private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/ public String get username FromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*
* @param request
* @return token
*/ private String getToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
private String getTokenKey(String uuid) {
return Constants.LOGIN_TOKEN_KEY + uuid;
}
}
设置过滤器
/**
* token过滤器 验证token有效性
*
* @author ruoyi
*/@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
登录
用户输入账号密码后,并生成一个token返回给前端。
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/public String login(String username, String password, String code, String uuid) {
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
if (captcha == null) {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, message Utils.message("user.jcaptcha.expire")));
throw new RuntimeException();
}
if (!code.equalsIgnoreCase(captcha)) {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}
// 用户验证
try {
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
} else {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e. getMessage ()));
throw new CustomException(e.getMessage());
}
}
}
总结与反思
总结 :使用redis,取代session的有状态化,实现 去中心化,无状态化。通过阅读token的相关知识,对去中心化有了新的实践理解。
不足 :此方案没有涉及到对token的刷新,而是仅对redis时效性的延长。下一次更新文章。
感谢读者的转发、点赞、关注
感谢读者的转发、点赞、关注
感谢读者的转发、点赞、关注