JWT 登录认证 + Token 自动续期方案,写得太好了!(jwt token登出怎么处理)

  本篇文章为你整理了JWT 登录认证 + Token 自动续期方案,写得太好了!(jwt token登出怎么处理)的详细内容,包含有jwttoken过期自动续期解决方案 jwt token登出怎么处理 jwt token验证 jwttoken验证机制 JWT 登录认证 + Token 自动续期方案,写得太好了!,希望能帮助你了解 JWT 登录认证 + Token 自动续期方案,写得太好了!。

  分享Java技术,高并发编程,分布式技术,架构设计,Java面试题,算法,行业动态,程序人生等。

  
作者:何甜甜在吗
 

  链接:https://juejin.cn/post/6932702419344162823

  过去这段时间主要负责了项目中的用户管理模块,用户管理模块会涉及到加密及认证流程,加密已经在前面的文章中介绍了,可以阅读用户管理模块:如何保证用户数据安全。

  今天就来讲讲认证功能的技术选型及实现。技术上没啥难度当然也没啥挑战,但是对一个原先没写过认证功能的菜鸡甜来说也是一种锻炼吧

  要实现认证功能,很容易就会想到JWT或者session,但是两者有啥区别?各自的优缺点?应该Pick谁?夺命三连

  基于session和基于JWT的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而JWT是保存在客户端的

  基于session的认证流程

  用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个session并保存到数据库

  服务器为用户生成一个sessionId,并将具有sesssionId的cookie放置在用户浏览器中,在后续的请求中都将带有这个cookie信息进行访问

  服务器获取cookie,通过获取cookie中的sessionId查找数据库判断当前请求是否有效

  基于JWT的认证流程

  用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个token并保存到数据库

  前端获取到token,存储到cookie或者local storage中,在后续的请求中都将带有这个token信息进行访问

  服务器获取token值,通过查找数据库判断当前token是否有效

  JWT保存在客户端,在分布式环境下不需要做额外工作。而session因为保存在服务端,分布式环境下需要实现多机数据共享

  session一般需要结合Cookie实现认证,所以需要浏览器支持cookie,因此移动端无法使用session认证方案

  JWT的payload使用的是base64编码的,因此在JWT中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全

  如果在JWT中存储了敏感信息,可以解码出来非常的不安全

  经过编码之后JWT将非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage里面。并且用户在系统中的每一次http请求都会把JWT携带在Header里面,HTTP请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的HTTP请求比使用session的开销大得多

  无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT

  无法废弃
 

  一旦签发一个JWT,在到期之前就会始终有效,无法中途废弃。若想废弃,一种常用的处理手段是结合redis

  续签
 

  如果使用JWT做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个HTTP请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间

  选择JWT或session

  我投JWT一票,JWT有很多缺点,但是在分布式环境下不需要像session一样额外实现多机数据共享,虽然seesion的多机数据共享可以通过粘性session、session共享、session复制、持久化session、terracoa实现seesion复制等多种成熟的方案来解决这个问题。但是JWT不需要额外的工作,使用JWT不香吗?且JWT一次性的缺点可以结合redis进行弥补。扬长补短,因此在实际项目中选择的是使用JWT来进行认证

  JWT所需依赖

  

 dependency 

 

   groupId com.auth0 /groupId

   artifactId java-jwt /artifactId

   version 3.10.3 /version

   /dependency

  

 

  JWT工具类

  

public class JWTUtil {

 

   private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);

   //私钥

   private static final String TOKEN_SECRET = "123456";

   * 生成token,自定义过期时间 毫秒

   * @param userTokenDTO

   * @return

   public static String generateToken(UserTokenDTO userTokenDTO) {

   try {

   // 私钥和加密算法

   Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);

   // 设置头部信息

   Map String, Object header = new HashMap (2);

   header.put("Type", "Jwt");

   header.put("alg", "HS256");

   return JWT.create()

   .withHeader(header)

   .withClaim("token", JSONObject.toJSONString(userTokenDTO))

   //.withExpiresAt(date)

   .sign(algorithm);

   } catch (Exception e) {

   logger.error("generate token occur error, error is:{}", e);

   return null;

   * 检验token是否正确

   * @param token

   * @return

   public static UserTokenDTO parseToken(String token) {

   Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);

   JWTVerifier verifier = JWT.require(algorithm).build();

   DecodedJWT jwt = verifier.verify(token);

   String tokenInfo = jwt.getClaim("token").asString();

   return JSON.parseObject(tokenInfo, UserTokenDTO.class);

  

 

  说明:

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

  UserTokenDTO中不带有敏感信息,如password字段不会出现在token中

  Redis工具类

  

public final class RedisServiceImpl implements RedisService {

 

   * 过期时长

   private final Long DURATION = 1 * 24 * 60 * 60 * 1000L;

   @Resource

   private RedisTemplate redisTemplate;

   private ValueOperations String, String valueOperations;

   @PostConstruct

   public void init() {

   RedisSerializer redisSerializer = new StringRedisSerializer();

   redisTemplate.setKeySerializer(redisSerializer);

   redisTemplate.setValueSerializer(redisSerializer);

   redisTemplate.setHashKeySerializer(redisSerializer);

   redisTemplate.setHashValueSerializer(redisSerializer);

   valueOperations = redisTemplate.opsForValue();

   @Override

   public void set(String key, String value) {

   valueOperations.set(key, value, DURATION, TimeUnit.MILLISECONDS);

   log.info("key={}, value is: {} into redis cache", key, value);

   @Override

   public String get(String key) {

   String redisValue = valueOperations.get(key);

   log.info("get from redis, value is: {}", redisValue);

   return redisValue;

   @Override

   public boolean delete(String key) {

   boolean result = redisTemplate.delete(key);

   log.info("delete from redis, key is: {}", key);

   return result;

   @Override

   public Long getExpireTime(String key) {

   return valueOperations.getOperations().getExpire(key);

  

 

  RedisTemplate简单封装

  

public String login(LoginUserVO loginUserVO) {

 

   //1.判断用户名密码是否正确

   UserPO userPO = userMapper.getByUsername(loginUserVO.getUsername());

   if (userPO == null) {

   throw new UserException(ErrorCodeEnum.TNP1001001);

   if (!loginUserVO.getPassword().equals(userPO.getPassword())) {

   throw new UserException(ErrorCodeEnum.TNP1001002);

   //2.用户名密码正确生成token

   UserTokenDTO userTokenDTO = new UserTokenDTO();

   PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);

   userTokenDTO.setId(userPO.getId());

   userTokenDTO.setGmtCreate(System.currentTimeMillis());

   String token = JWTUtil.generateToken(userTokenDTO);

   //3.存入token至redis

   redisService.set(userPO.getId(), token);

   return token;

  

 

  说明:

  判断用户名密码是否正确

  用户名密码正确则生成token

  将生成的token保存至redis

  

public boolean loginOut(String id) {

 

   boolean result = redisService.delete(id);

   if (!redisService.delete(id)) {

   throw new UserException(ErrorCodeEnum.TNP1001003);

   return result;

  

 

  将对应的key删除即可

  更新密码功能

  

public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {

 

   //1.修改密码

   UserPO userPO = UserPO.builder().password(updatePasswordUserVO.getPassword())

   .id(updatePasswordUserVO.getId())

   .build();

   UserPO user = userMapper.getById(updatePasswordUserVO.getId());

   if (user == null) {

   throw new UserException(ErrorCodeEnum.TNP1001001);

   if (userMapper.updatePassword(userPO) != 1) {

   throw new UserException(ErrorCodeEnum.TNP1001005);

   //2.生成新的token

   UserTokenDTO userTokenDTO = UserTokenDTO.builder()

   .id(updatePasswordUserVO.getId())

   .username(user.getUsername())

   .gmtCreate(System.currentTimeMillis()).build();

   String token = JWTUtil.generateToken(userTokenDTO);

   //3.更新token

   redisService.set(user.getId(), token);

   return token;

  

 

  说明:
 

  更新用户密码时需要重新生成新的token,并将新的token返回给前端,由前端更新保存在local storage中的token,同时更新存储在redis中的token,这样实现可以避免用户重新登陆,用户体验感不至于太差

  在实际项目中,用户分为普通用户和管理员用户,只有管理员用户拥有删除用户的权限,这一块功能也是涉及token操作的,但是我太懒了,demo工程就不写了

  在实际项目中,密码传输是加密过的

  

public boolean preHandle(HttpServletRequest request, HttpServletResponse response,

 

   Object handler) throws Exception {

   String authToken = request.getHeader("Authorization");

   String token = authToken.substring("Bearer".length() + 1).trim();

   UserTokenDTO userTokenDTO = JWTUtil.parseToken(token);

   //1.判断请求是否有效

   if (redisService.get(userTokenDTO.getId()) == null

   !redisService.get(userTokenDTO.getId()).equals(token)) {

   return false;

   //2.判断是否需要续期

   if (redisService.getExpireTime(userTokenDTO.getId()) 1 * 60 * 30) {

   redisService.set(userTokenDTO.getId(), token);

   log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token);

   return true;

  

 

  说明:
 

  拦截器中主要做两件事,一是对token进行校验,二是判断token是否需要进行续期
 

  token校验:

  判断id对应的token是否不存在,不存在则token过期

  若token存在则比较token是否一致,保证同一时间只有一个用户操作

  token自动续期: 为了不频繁操作redis,只有当离过期时间只有30分钟时才更新过期时间

  拦截器配置类

  

@Configuration

 

  public class InterceptorConfig implements WebMvcConfigurer {

   @Override

   public void addInterceptors(InterceptorRegistry registry) {

   registry.addInterceptor(authenticateInterceptor())

   .excludePathPatterns("/logout/**")

   .excludePathPatterns("/login/**")

   .addPathPatterns("/**");

   @Bean

   public AuthenticateInterceptor authenticateInterceptor() {

   return new AuthenticateInterceptor();

  

 

  若有纰漏不足,欢迎指出

  近期热文推荐:

  1.1,000+ 道 Java面试题及答案整理(2022最新版)

  2.劲爆!Java 协程要来了。。。

  3.Spring Boot 2.x 教程,太全了!

  4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

  5.《Java开发手册(嵩山版)》最新发布,速速下载!

  觉得不错,别忘了随手点赞+转发哦!

  以上就是JWT 登录认证 + Token 自动续期方案,写得太好了!(jwt token登出怎么处理)的详细内容,想要了解更多 JWT 登录认证 + Token 自动续期方案,写得太好了!的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: