• jwttoken+redis+springsecurity


    思路

    jwttoken不设置过期时间
    redis管理过期时间,并且续签
    redis中key="login:"+userId, value=jwtUser
    再次访问时,解析token中userId,并且根据过期时间自动续签
    
    • 1
    • 2
    • 3
    • 4

    JWT 实现登录认证 + Token 自动续期方案
    pom文件配置

    <!--Redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.3.5.RELEASE</version>
    </dependency>
    
    <!--JWT-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.10.7</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.10.7</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.10.7</version>
        <scope>runtime</scope>
    </dependency>
    
    <!--spring security-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    application.yml配置

    spring:
      redis:
        database: 0
        host: redis_ip
        port: redis_port
        connect-timeout: 30000
        lettuce:
          pool:
            # 最大阻塞等待时间,负数表示没有限制
            max-wait: -1
            # 连接池中的最大空闲连接
            max-idle: 10
            # 连接池中的最小空闲连接
            min-idle: 0
            # 连接池中最大连接数,负数表示没有限制
            max-active: 50
        password: redis_password
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    service层logon方法

    @Override
    public Object login(LoginDTO loginDTO) {
      UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
      Authentication authenticate;
      try {
        // 对登录用户进行认证
        authenticate = authenticationManager.authenticate(authenticationToken);
      } catch (AuthenticationException ex) {
        throw new ServiceException(ex.getMessage());
      }
      if (Objects.isNull(authenticate)) {
        throw new ServiceException("登录失败");
      }
      JWTUser jwtUser = (JWTUser)authenticate.getPrincipal();
      if(jwtUser.getUserDO().getDeleteFlag()!=0){
        throw new ServiceException("用户已被停用,如需开启,请联系管理员");
      }
      LoginSuccessDTO loginSuccessDTO = new LoginSuccessDTO();
      loginSuccessDTO.setUserId(jwtUser.getId());
      loginSuccessDTO.setUsername(jwtUser.getUsername());
      loginSuccessDTO.setNickName(jwtUser.getUserDO().getNickName());
      loginSuccessDTO.setUserRealName(jwtUser.getUserDO().getUserRealName());
      loginSuccessDTO.setDepartment(jwtUser.getUserDO().getDepartment());
      Map<String, Object> userInfo = DataConvert.BeanPropertyNameTraverse(loginSuccessDTO);
      String token = JwtUtils.generateToken(userInfo);
      String userId = jwtUser.getId();
      Date expirationTime = JwtUtils.getExpirationTime();
      redisUtils.set("login:" + userId, jwtUser, AuthConstant.EXPIRATION_TIME_IN_SECOND, TimeUnit.SECONDS);
      loginSuccessDTO.setLoginTime(new Date());
      loginSuccessDTO.setExpireTime(expirationTime);
      loginSuccessDTO.setToken(token);
      return loginSuccessDTO;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    @Override
    public Object logout() {
      /*
       * 获取SecurityContextHolder中的userId。
       * 注销操作也是需要携带token的,spring security已经在内部对token进行验证,
       * 并能够从redis取出用户信息
       */
      UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
      JWTUser jwtUser = (JWTUser)authentication.getPrincipal();
      String userId = jwtUser.getId();
      redisUtils.del("login:"+userId);
      return "注销成功";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    token工具类

    package cma.sxqxgxw.utils.jwt;
    
    import cma.sxqxgxw.auth.constant.AuthConstant;
    import cma.sxqxgxw.common.exception.ServiceException;
    import io.jsonwebtoken.*;
    import io.jsonwebtoken.security.Keys;
    
    import javax.crypto.SecretKey;
    import java.util.Map;
    import java.util.Date;
    
    public class JwtUtils {
        private static final String SIGNATURE = "!QW@f5g%T^U&f3r32523534634634654754ygrgfdjjuhfdsdf6";
    
        /**
         * 计算token的过期时间
         *
         * @return 过期时间
         */
        public static Date getExpirationTime() {
            return new Date(System.currentTimeMillis() + AuthConstant.EXPIRATION_TIME_IN_SECOND * 1000);
        }
    
        /**
         * 生成Token
         * @param claims 用户信息
         * @return token
         */
        public static String generateToken(Map<String, Object> claims) {
            Date createdTime = new Date();
            //Date expirationTime = getExpirationTime();
            byte[] keyBytes = SIGNATURE.getBytes();
            SecretKey key = Keys.hmacShaKeyFor(keyBytes);
            return Jwts.builder()
                    .setClaims(claims)
                    .setIssuedAt(createdTime)
                    //.setExpiration(expirationTime)
                    .signWith(key, SignatureAlgorithm.HS256)
                    .compact();
        }
    
        /**
         * 从token中获取claim
         *
         * @param token token
         * @return claim
         */
        public static Claims getClaimsFromToken(String token) {
            try {
                return Jwts.parser()
                        .setSigningKey(SIGNATURE.getBytes())
                        .parseClaimsJws(token)
                        .getBody();
            } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
                throw new ServiceException("Token invalided.");
            }
        }
    
        /**
         * 获取token的过期时间
         *
         * @param token token
         * @return 过期时间
         */
        public static Date getExpirationDateFromToken(String token) {
            return getClaimsFromToken(token)
                    .getExpiration();
        }
    
        /**
         * 判断token是否过期
         *
         * @param token token
         * @return 已过期返回true,未过期返回false
         */
        public static Boolean isTokenExpired(String token) {
            Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }
    
        /**
         * 判断token是否非法
         *
         * @param token token
         * @return 未过期返回true,否则返回false
         */
        public static Boolean validateToken(String token) {
            System.out.println("token valid");
            return !isTokenExpired(token);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    redis工具类

    @Slf4j
    @Component
    public class RedisUtils {
    
        private final RedisTemplate<String, Object> redisTemplate;
        @Autowired
        public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        /**
         * 指定缓存失效时间
         *
         * @param key  键
         * @param time 时间(秒)
         * @return
         */
        public boolean expire(String key, long time) {
            try {
                if (time > 0) {
                    redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据key 获取过期时间
         *
         * @param key 键 不能为null
         * @return 时间(秒) 返回0代表为永久有效
         */
        public long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
    
        /**
         * 判断key是否存在
         *
         * @param key 键
         * @return true 存在 false不存在
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                log.info("Redis连接失败,可能是Redis服务未启动。" + e.getMessage());
                return false;
            }
        }
    
        /**
         * 删除缓存
         *
         * @param key 可以传一个值 或多个
         */
        @SuppressWarnings("unchecked")
        public boolean del(String... key) {
            if (key != null && key.length > 0) {
                if (key.length == 1) {
                    Boolean delete = redisTemplate.delete(key[0]);
                    return delete;
                } else {
                    Long delete = redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
                    return true;
                }
            }
            return false;
        }
    
        //============================String=============================
    
        /**
         * 普通缓存获取
         *
         * @param key 键
         * @return 值
         */
        public Object get(String key) {
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
    
        /**
         * 普通缓存放入
         *
         * @param key   键
         * @param value 值
         * @return true成功 false失败
         */
        public boolean set(String key, Object value) {
            try {
                redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {
                log.info("插入操作时,Redis连接失败,可能是Redis服务未启动。" + e.getMessage());
                return false;
            }
        }
    
        /**
         * 普通缓存放入并设置时间
         *
         * @param key   键
         * @param value 值
         * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
        public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, timeUnit);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124

    jwt过滤器
    生成的token中不带有过期时间,token的过期时间由redis进行管理
    当redis过期时间小于10分钟时,redis过期时间续签30分钟

    @Component
    public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
        private final RedisUtils redisUtils;
    
        @Autowired
        public JWTAuthorizationFilter(AuthenticationManager authenticationManager, RedisUtils redisUtils) {
            super(authenticationManager);
            this.redisUtils = redisUtils;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            String token = request.getHeader("token");
            if (!StringUtils.hasText(token)) {
                request.setAttribute("errorMessage", "Token缺失,请检查请求Header");
                chain.doFilter(request, response);
                return;
            }
            try {
                Claims claims = JwtUtils.getClaimsFromToken(token);
                String userId = (String) claims.get("userId");
                boolean hasKey = redisUtils.hasKey("login:" + userId);
                if (!hasKey) {
                    request.setAttribute("errorMessage", "Token无效或已经过期");
                    throw new ServiceException("Token无效或已经过期");
                }
                // 令牌续签
                long expire = redisUtils.getExpire("login:" + userId);
                System.out.println("剩余时间: "+expire);
                if (expire < AuthConstant.RENEWAL_TIME_IN_SECOND) {
                    redisUtils.expire("login:" + userId, AuthConstant.EXPIRATION_TIME_IN_SECOND);
                    System.out.println("令牌续签" + AuthConstant.EXPIRATION_TIME_IN_SECOND + "秒");
                }
    
                JWTUser jwtUser = (JWTUser) redisUtils.get("login:" + userId);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtUser, null, jwtUser.getAuthorities());
                // 设置认证状态
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                chain.doFilter(request, response);
            } catch (ServiceException e) {
                request.setAttribute("errorMessage", e.getMessage());
                chain.doFilter(request, response);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    过期时间常量

    public class AuthConstant {
      /**
       * 令牌有效时长
       */
      public static final Long EXPIRATION_TIME_IN_SECOND = 60*30L;
    
      /**
       * 过期时间剩余多少时间时续签
       */
      public static final Long RENEWAL_TIME_IN_SECOND = 60*10L;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Redis配置类

    package cma.sxqxgxw.common.config.redis;
    
    import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Bean;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * Redis配置
     * 主要指定自定义序列化器,避免序列化反序列化失败
     */
    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            // 参照StringRedisTemplate内部实现指定序列化器
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            // 设置key的序列化器
            redisTemplate.setKeySerializer(keySerializer());
            redisTemplate.setHashKeySerializer(keySerializer());
            // 设置value的序列化器
            // 解决autoType is not support.xx.xx的问题
            String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
            GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
            redisTemplate.setValueSerializer(serializer);
            redisTemplate.setHashValueSerializer(serializer);
            return redisTemplate;
        }
    
        private RedisSerializer<String> keySerializer(){
            return new StringRedisSerializer();
        }
    
        //使用Jackson序列化器
        private RedisSerializer<Object> valueSerializer(){
            return new GenericJackson2JsonRedisSerializer();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    Jwt拦截器

    package cma.sxqxgxw.common.interceptor;
    
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    @Component
    public class JwtInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            return true;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  • 相关阅读:
    安全易用的文件同步程序:Syncthing | 开源日报 No.70
    快速了解服务器单CPU与双CPU
    rabbitMq安装
    动态库和静态库的制作
    Docker-(基础服务)-数据库安装-简单版: Redis数据库,Mysql数据库
    MyCat|Shardingsphere-proxy:jdbc连接MySQL8.0.33的query_cache_size异常解决方案
    后端:推荐 2 个 .NET 操作的 Redis 客户端类库
    Spring Data JPA系列3:JPA项目中核心场景与进阶用法介绍
    typora设置标题自动编号
    知识点汇总
  • 原文地址:https://blog.csdn.net/weixin_43732943/article/details/133780863