05 SS之结合SS+Redis+JWT实现认证

之前的一切的有一个比较显著的问题是: 假如用户主动登出, 这意味着系统应将用户的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配置

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

# 配置redis信息
spring:
  redis:
    host: 192.168.43.33
    port: 6379
    database: 0
    password: 666666

1.2.2 修改认证(登陆成功)处理器 , 使其能将JWT等信息放到Redis中

第一次认证(登录)成功后将JWT等信息存好

java 复制代码
@Component
public class MyAuthenticationSuccessHandle implements AuthenticationSuccessHandler {

    //注入一个序列化器, 可以将JSON序列化, 反序列化
    @Resource
    private ObjectMapper objectMapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public 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);
    }

    @Override
    public 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();
        //将对象转化为JSON
        String responseJson = objectMapper.writeValueAsString(httpResult);

        PrintWriter writer = response.getWriter();
        writer.println(responseJson);
        writer.flush();
    }

}

1.3 修改登出成功处理器,完成步骤2

java 复制代码
/**
 * 退出成功处理器,用户退出成功后,执行此处理器
 */
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    //使用此工具类的对象进行序列化操作
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    
    public 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存在的情况下才允许登录

cpp 复制代码
@Resource
private StringRedisTemplate stringRedisTemplate;

//        从redis中获取token
        String tokenInRedis = stringRedisTemplate.opsForValue().get("logintoken:" + jwtToken);
        if(!StringUtils.hasText(tokenInRedis)){
            printFront(response, "用户已退出,请重新登录");
            return;
        }

新JWTUtil

java 复制代码
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();
            //验证JWT
            DecodedJWT 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();
            //验证JWT
            DecodedJWT 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();
            //验证JWT
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Claim auth = decodedJWT.getClaim("auth");
            return auth.asList(String.class);
        }catch (TokenExpiredException e){
            e.printStackTrace();
        }
        return null;
    }

}
yaml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: Mysql998168

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.sunsplanter.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

my:
  secretKey: zheshi一个miyao
相关推荐
施嘉伟4 分钟前
静默安装金仓数据库,到底有多简单?
数据库
虫小宝5 分钟前
Java分布式架构下的电商返利APP技术选型与架构设计实践
java·分布式·架构
007php0079 分钟前
百度面试题解析:Zookeeper、ArrayList、生产者消费者模型及多线程(二)
java·分布式·zookeeper·云原生·职场和发展·eureka·java-zookeeper
Tapdata11 分钟前
实时物化视图的新路径:从传统 Join 到跨源实时查询
数据库
optimistic_chen14 分钟前
【Java EE进阶 --- SpringBoot】Mybatis - plus 操作数据库
数据库·spring boot·笔记·java-ee·mybatis·mybatis-plus
4Forsee37 分钟前
【Android】浅析 Android 的 IPC 跨进程通信机制
android·java
FJW02081444 分钟前
关系型数据库大王Mysql——DDL语句操作示例
数据库·mysql
言之。1 小时前
Chroma 开源的 AI 应用搜索与检索数据库(即向量数据库)
数据库·人工智能·开源
来旺1 小时前
互联网大厂Java面试全解析及三轮问答专项
java·数据库·spring boot·安全·缓存·微服务·面试
Json____1 小时前
使用node Express 框架框架开发一个前后端分离的二手交易平台项目。
java·前端·express