问题1:如何使jwt生成的 token在用户登出之后失效?
由于jwt生成的token是无状态的,这体现在我们在每一次请求时 request都会新建一个session对象:
举个例子:
@PostMapping(value = "/authentication/logout")
public ResponseEntity logOut(HttpServeletRequest request ,@RequestHeader(name = "Authorization") String authHeader) throws AuthenticationException, IOException {
HttpSession session =request.getSession();
session.isNew();//true
return null;}
这就会导致我们在退出系统之后,使用原token依然有机会被系统检验通过,而有权限访问系统;
所以我们需要借助别的方式使得该token失效(并不是token真正失效而是将其加入黑名单或者使用别的什么方式标记它);
1,加入黑名单的方式需要额外的占用存储空间,本次暂未考虑该方式;
2,除了加,我们还可以使用减法的方式来实现:
首先我们在redis中将用户登陆的token存起来,
这里要考虑用户可以多端同时登陆(即每个设备都会产生一个token),如果一个设备登出而不想影响其他设备的登录,此时就要将登出的那个token单独处理;
这里做了一个设计是"将所有用户(同一用户)登录token已list的形式存在redis中",如果一个用户登出,则将该用户的token从list中删除,下次请求时会校验list中是否有该key,如果有放行,否则就要重新登录;
代码实现:
登陆时缓存token:
HttpStatus httpStatus;
BaseResult baseResult;
HttpHeaders httpHeaders = new HttpHeaders();
String password = RSAUtils.privateDecrypt(user.getPassword(), RSAUtils.getPrivateKey(Const.PRIVATE_KEY));
if (!HmrsUtils.isValidPassword(password)) {
log.error("密码不匹配:{}", user.getUsername());
throw new PasswordLengthException();
}
//登陆的时候考虑浏览器的因素,可以多个浏览器同时登录一个账号
String token = authService.login(user.getUsername(), password);
if (StrUtil.isEmpty(token)) {
httpHeaders.add("Authorization", null);
httpStatus = HttpStatus.BAD_REQUEST;
baseResult = HmrsUtils.setHttpBaseResult(httpStatus.value(), "登录失败", "用户名/密码错误");
} else {
httpHeaders.add("Authorization", token);
httpStatus = HttpStatus.OK;
baseResult = HmrsUtils.setHttpBaseResult(httpStatus.value(), "登录成功", null);
log.info("{}用户登录", user.getUsername());
User userInfo = userService.queryUserInfoWithOutPwd(user.getUsername());
baseResult.setUserInfo(userInfo);
//登录成功后缓存用户信息 --key -->username value ---缓存用户信息作为热信息
redisRWService.saveJsonObject(user.getUsername(), userInfo);//存入的是一个对象
//缓存token 一个集合 就存字符串
redisRWService.saveTokenStorage(user.getUsername() + Const.TOKEN_PREFIX, token);
// redisRWService.saveJsonObject(user.getUsername() + Const.TOKEN_PREFIX, token);
redisRWService.setKeyExpireTime(user.getUsername() + Const.TOKEN_PREFIX, Const.REDIS_TOKEN_EXPIRE);
log.info("redis已缓存用户信息");
登出时list中移除该token:
HttpStatus httpStatus;
BaseResult baseResult;
HttpHeaders httpHeaders = new HttpHeaders();
String userName = JwtTokenUtil.getUserName(authHeader);
String token = authHeader.substring(Const.TOKEN_PREFIX.length() + 1);
//如果有key,说明有用户登录
if (redisRWService.hasKey(userName + Const.TOKEN_PREFIX)) {
//取出这个list
List list = redisRWService.takeTokenStorage(userName + Const.TOKEN_PREFIX);
//如果集合中包含这个token,则可以正常登出,否则就是非法请求
if (list.contains(token)) {
//在集合中删除该token后重新存入redis
List collect = list.stream().filter(str -> !str.equals(token)).collect(Collectors.toList());
redisRWService.deleteKey(userName + Const.TOKEN_PREFIX);
redisRWService.saveAllTokenStorage(userName + Const.TOKEN_PREFIX, collect);
httpStatus = HttpStatus.OK;
baseResult = HmrsUtils.setHttpBaseResult(httpStatus.value(), "登出成功", null);
log.info("{}登出", userName);
httpHeaders.add("Authorization", null);
return new ResponseEntity<>(baseResult, httpHeaders, httpStatus);
} else {
httpStatus = HttpStatus.FORBIDDEN;
baseResult = HmrsUtils.setHttpBaseResult(httpStatus.value(), "登出失败", "请重新登录");
log.error("{}登出失败", userName);
return new ResponseEntity<>(baseResult, null, httpStatus);
}
//否则就是该账号用户长时间未登录
} else {
httpStatus = HttpStatus.FORBIDDEN;
baseResult = HmrsUtils.setHttpBaseResult(httpStatus.value(), "登出失败", "长时间未登录,请登陆后重试");
log.error("{}登出失败", userName);
return new ResponseEntity<>(baseResult, null, httpStatus);
}
}
生效的交给拦截器/过滤器:
@Slf4j
@Component
public class JwtTokenInterceptor implements HandlerInterceptor {
@Autowired
private RedisRWService redisRWService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String authHeader = request.getHeader(Const.HEADER_STRING);
if (StrUtil.isNotEmpty(authHeader)) {
String token = authHeader.substring(Const.TOKEN_PREFIX.length() + 1);
//从token中取出用户名
String userName = JwtTokenUtil.getUsernameFromToken(token);
if (StrUtil.isNotEmpty(userName)) {
//能解析出用户名 判断登录状态
if (redisRWService.hasKey(userName + Const.TOKEN_PREFIX)) {
List list = redisRWService.takeTokenStorage(userName + Const.TOKEN_PREFIX);
//如果集合中有这个token
if (list.contains(token)) {
log.info("{}用户登录", userName);
return true;
} else {
String jsonString = JSONObject.toJSONString(HmrsUtils.setHttpBaseResult(Const.TOKEN_EXPIRE, "请登录再试", "登录已过期,请重新登录"));
HmrsUtils.returnJson(response, jsonString);
return false;
}
} else {
String jsonString = JSONObject.toJSONString(HmrsUtils.setHttpBaseResult(Const.TOKEN_EXPIRE, "长时间未登录", "长时间未登录,请登录后重试"));
HmrsUtils.returnJson(response, jsonString);
return false;
}
} else {
String jsonString = JSONObject.toJSONString(HmrsUtils.setHttpBaseResult(400, "登陆失败", "非法请求"));
HmrsUtils.returnJson(response, jsonString);
return false;
}
} else {
String jsonString = JSONObject.toJSONString(HmrsUtils.setHttpBaseResult(400, "登陆失败", "用户名/密码不正确"));
HmrsUtils.returnJson(response, jsonString);
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
package com.hmrs.filter;
import com.alibaba.fastjson2.JSONObject;
import com.hmrs.comm.Const;
import com.hmrs.util.HmrsUtils;
import com.hmrs.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Override
public FilterConfig getFilterConfig() {
return super.getFilterConfig();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain
chain) throws ServletException, IOException {
String authHeader = request.getHeader(Const.HEADER_STRING);
if (authHeader != null && authHeader.startsWith(Const.TOKEN_PREFIX)) {
final String authToken = authHeader.substring(Const.TOKEN_PREFIX.length() + 1);
String username = JwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (JwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} else if (username == null) {
log.error("登录已过期");
String result = JSONObject.toJSONString(HmrsUtils.setHttpBaseResult(1002, "failed", "登陆已过期"));
HmrsUtils.returnJson(response, result);
return;
}
}
chain.doFilter(request, response);
}
}