jwttoken+redis+springsecurity

思路

java 复制代码
jwttoken不设置过期时间
redis管理过期时间,并且续签
redis中key="login:"+userId, value=jwtUser
再次访问时,解析token中userId,并且根据过期时间自动续签

JWT 实现登录认证 + Token 自动续期方案

pom文件配置

java 复制代码
<!--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>

application.yml配置

java 复制代码
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

service层logon方法

java 复制代码
@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;
}
java 复制代码
@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 "注销成功";
}

token工具类

java 复制代码
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);
    }
}

redis工具类

java 复制代码
@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;
        }
    }
}

jwt过滤器

生成的token中不带有过期时间,token的过期时间由redis进行管理

当redis过期时间小于10分钟时,redis过期时间续签30分钟

java 复制代码
@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);
        }
    }
}

过期时间常量

java 复制代码
public class AuthConstant {
  /**
   * 令牌有效时长
   */
  public static final Long EXPIRATION_TIME_IN_SECOND = 60*30L;

  /**
   * 过期时间剩余多少时间时续签
   */
  public static final Long RENEWAL_TIME_IN_SECOND = 60*10L;
}

Redis配置类

java 复制代码
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();
    }
}

Jwt拦截器

java 复制代码
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 小时前
【redis】—— 环境搭建教程
数据库·redis·缓存
古人诚不我欺3 小时前
Redis设置密码认证,以及不重启服务情况下设置临时密码
数据库·redis·缓存
ac.char12 小时前
在 Ubuntu 上安装 Redis 并为其设置登录密码
linux·redis·ubuntu
极客星辰16 小时前
Linux redis-6.2.6安装
linux·运维·redis
NiNg_1_23417 小时前
SpringBoot集成Redis详解
spring boot·redis·bootstrap
看山还是山,看水还是。17 小时前
Redis 命令
前端·数据库·redis·bootstrap
极客先躯1 天前
高级java每日一道面试题-2024年11月02日-Redis篇-Redis6之后为什么开始支持多线程?
java·redis·多线程·优势·多核处理
水月梦镜花1 天前
redis:RDB和AOF机制
数据库·redis·bootstrap
ktkiko111 天前
Redis中的缓存设计
数据库·redis·缓存
ascarl20101 天前
系统启动时将自动加载环境变量,并后台启动 MinIO、Nacos 和 Redis 服务
数据库·redis·缓存