之前的一切的有一个比较显著的问题是: 假如用户主动登出, 这意味着系统应将用户的token作废, 但实际上并未实现这一步, 导致了即使用户登出, 只要token在有效期内, 用户仍能凭借token直接进入.
即token处于一个无法销毁的状态.
可以借助Redis.
核心步骤是
① 第一次登陆成功之后不仅仅是返回JWT给用户,
而是将JWT在返回给用户的同时存到redis中
key value
logintoken:jwt UsernamePasswordAuthenticationToken
② 用户退出时,从redis中删除该token
③ 第二次以后用户每次访问时,先校验jwt是否合法,如果合法再从redis里面取出logintoken:jwt并判断这个jwt还存不存在,如果不存在就说是用户已经退出来,就返回未登陆。
1.2 修改认证(登陆)成功处理器,完成步骤1
1.2.1 加入依赖, 配置文件中增加redis配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency># 配置redis信息
spring:redis:host: 192.168.43.33port: 6379database: 0password: 666666
1.2.2 修改认证(登陆成功)处理器 , 使其能将JWT等信息放到Redis中
第一次认证(登录)成功后将JWT等信息存好
@Component
public class MyAuthenticationSuccessHandle implements AuthenticationSuccessHandler {//注入一个序列化器, 可以将JSON序列化, 反序列化@Resourceprivate ObjectMapper objectMapper;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {//省略代码,即根据authentication获取用户信息,用户权限信息, 并根据这些信息生成JWT/**第一第二个参数分别是key和value,我们这里选择将JWT填入key中,认证信息填入value中*objectMapper调用方法将authentication序列化为字符串填入value中*第三第四个参数分别是过期时间和时间单位*/stringRedisTemplate.opsForValue().set("logintoken:"+token,objectMapper.writeValueAsString(authentication),30, TimeUnit.MINUTES);}@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {response.setCharacterEncoding("UTF-8");response.setContentType("application/json;charset=utf-8");//借助Lombok实现建造者模式, 并通过建造者模式创建对象HttpResult httpResult = HttpResult.builder().code(1).msg("登陆成功").build();//将对象转化为JSONString responseJson = objectMapper.writeValueAsString(httpResult);PrintWriter writer = response.getWriter();writer.println(responseJson);writer.flush();}}
1.3 修改登出成功处理器,完成步骤2
/*** 退出成功处理器,用户退出成功后,执行此处理器*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {//使用此工具类的对象进行序列化操作@Resourceprivate ObjectMapper objectMapper;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {//从请求头中获取Authorization信息String authorization = request.getHeader("Authorization");//如果授权信息为空,返回前端if(null==authorization){response.setCharacterEncoding("UTF-8");response.setContentType("application/json;charset=utf-8");HttpResult httpResult=HttpResult.builder().code(-1).msg("token不能为空").build();PrintWriter writer = response.getWriter();writer.write(objectMapper.writeValueAsString(httpResult));writer.flush();return;}//如果Authorization信息不为空,去掉头部的Bearer字符串String token = authorization.replace("Bearer ", "");//redis中删除token,这是关键点stringRedisTemplate.delete("logintoken:"+token);response.setCharacterEncoding("UTF-8");response.setContentType("application/json;charset=utf-8");HttpResult httpResult=HttpResult.builder().code(200).msg("退出成功").build();PrintWriter writer = response.getWriter();writer.write(objectMapper.writeValueAsString(httpResult));writer.flush();}
}
1.4 修改过滤器类实现步骤3,
每次登陆前先判断JWT是否存在, 仅当JWT存在的情况下才允许登录
@Resource
private StringRedisTemplate stringRedisTemplate;// 从redis中获取tokenString tokenInRedis = stringRedisTemplate.opsForValue().get("logintoken:" + jwtToken);if(!StringUtils.hasText(tokenInRedis)){printFront(response, "用户已退出,请重新登录");return;}
新JWTUtil
package com.sunsplanter.utils;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 本类用于生成和解析JWT*/
@Component
@Slf4j
public class JWTUtil {/*** 声明一个秘钥*/@Value("${my.secretKey}")private String SECRET;/*** 生成JWT** @param userInfo 用户信息(包括编号和姓名)* @param auth 用户权限*/public String createToken(String userInfo, List<String> auth) {//得到当前的系统时间Date currentDate = new Date();//根据当前时间计算出过期时间 定死为5分钟Date expTime = new Date(currentDate.getTime() + (1000 * 60 * 5));//头数据就是算法alg和类型typ,其中type必须是JWT(用JWT生成token)Map<String, Object> header = new HashMap<>();header.put("alg", "HS256");header.put("typ", "JWT");return JWT.create().withHeader(header) //头.withClaim("user_info", userInfo) //自定义数据.withClaim("auth", auth) //自定义数据.withIssuedAt(currentDate) //创建时间.withExpiresAt(expTime)//过期时间.sign(Algorithm.HMAC256(SECRET));}/*** 验证JWT并解析** @param token 要验证的jwt的字符串*/public static Boolean verifyToken(String token) {try{// 使用秘钥创建一个解析对象JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();//验证JWTDecodedJWT decodedJWT = jwtVerifier.verify(token);System.out.println(decodedJWT);log.info("token验证正确");return true;}catch (TokenExpiredException e){e.printStackTrace();log.info("token验证失败");return false;}}/*** 从JWT里的获取用户信息(包括用户编号和用户名)*/public static String getUserInfoFromToken(String token){try{// 使用秘钥创建一个解析对象JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();//验证JWTDecodedJWT decodedJWT = jwtVerifier.verify(token);Claim username = decodedJWT.getClaim("user_info");return username.asString();}catch (TokenExpiredException e){e.printStackTrace();}return null;}/*** 从JWT里的获取用户权限*/public List<String> getAuth(String token){try{// 使用秘钥创建一个解析对象JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(SECRET)).build();//验证JWTDecodedJWT decodedJWT = jwtVerifier.verify(token);Claim auth = decodedJWT.getClaim("auth");return auth.asList(String.class);}catch (TokenExpiredException e){e.printStackTrace();}return null;}}
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: Mysql998168mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.sunsplanter.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplmy:secretKey: zheshi一个miyao