JWT 登录认证及 token 自动续期方案解读

2023-11-12

欢迎关注方志朋的博客,回复”666“获面试宝典

方志朋

号主为CSDN博客之星,博客访问量突破一千万,著有畅销书《深入理解SpringCloud与微服务构建》。主要分享Java、后端架构等技术,用大厂程序员的视角来探讨技术进阶、面试指南、职业规划等。15W技术人的选择!

97篇原创内容

公众号

来源:juejin.cn/post/6932702419344162823

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

https://juejin.cn/post/6916150628955717646

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

技术选型

要实现认证功能,很容易就会想到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();
    }
}

 

热门内容:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JWT 登录认证及 token 自动续期方案解读 的相关文章

随机推荐

  • openGL之API学习(一零六)wglGetProcAddress

    获取当前显卡中OpenGL函数的指针地址 PROC wglGetProcAddress LPCSTR Arg1 Arg1 Points to a null terminated string that is the name of the
  • 学习Linux命令的正确姿势

    大家好 我是良许 大家应该注意到了 最近我的公众号文章末尾都挂着自己录制的 Linux命令从小白到大神 课程 这个课程我从开始录制到制作完成 足足花了一个半月 如果加上前期的资料收集与教案准备 肯定有将近三个月 首先跟大家讲讲我为什么要开发
  • 电器元件——LM7805

    LM7805是一款常用的线性三端稳压IC 其外形封装虽然有多种 但输出电压皆为5V 区别就是封装不同 最大输出电流可能不一样 最常用的是如下图所示的TO 220封装的7805 TO 220封装的7805 上图所示的这种封装的7805 最高输
  • 设计模式 - Provider Pattern(提供者模式)

    设计模式 Provider Pattern 提供者模式 作者 webabcd 介绍 为一个API进行定义和实现的分离 示例 有一个Message实体类 对它的操作有Insert 和Get 方法 持久化数据在SqlServer数据库中或Xml
  • tcpreplay-tcprewrite

    1 介绍 参考链接 https www cnblogs com zlslch p 7325599 html utm source itdadao utm medium referral tcpreplay是一种pcap包的重放工具 它可以将
  • 【OpenCV】形态学图像处理学习笔记

    目录 腐蚀操作 膨胀操作 开运算操作 闭运算操作 梯度运算操作 礼帽运算操作 黑帽运算操作 腐蚀操作 cv2 erode 图像对象 内核 卷积核 interations 内核 核越大 腐蚀程度越高可自行定义 例如 kernel np one
  • Java面试题--shiro

    Shiro可以做哪些工作 Shiro可以帮助我们完成 认证 授权 加密 会话管理 与Web集成 缓存等 shiro有哪些组件 Authentication 身份认证 登录 验证用户是不是拥有相应的身份 Authorization 授权 即权
  • PG数据库恢复指定错误备份文件时怎么解决

    项目场景 测试环境中对分库分表PG库的同步数据 表结构等操作 问题描述 当我们在给分库分表执行备份恢复时 把不是本分片库的备份文件导入至本分片库 导致数据错乱 比如我们将一个8分库 0 7 的备份文件命名为dump 0 ddl dump 1
  • Sqli-labs之Less-20和Less-21和Less-22

    Less 20 基于错误的cookie头部POST注入 首先从已知的条件中我们知道这又是一道 头部注入 那么我们先输入正确的用户名和密码看一下登录成功是什么样子的 回显有User Agent IP这样从当次Request直接获取的 也有Co
  • 兴业银行利用以太坊区块链发行债券,金融科技冲击下的银行业未来(上篇)

    点击上方 蓝色字 可关注我们 暴走时评 日前 兴业银行通过以太坊区块链发行了类证券代币的债券 兴业银行的举动可能意味着银行承认 即使比特币或以太等无许可协议可能会带来颠覆性的威胁 他们依旧无法放弃其中潜在的巨大机遇 作者 Michael J
  • PHP+Laravel框架RabbitMQ简单使用

    RabbitMQ安装教程请转到 RabbitMQ安装教程 超详细 1 创建生产者 在app Http Controllers里创建一个php控制器文件 namespace App Http Controllers use App Http
  • docker中安装jupyter,并远程打开jupyter

    一 拉取镜像 拉取一个自带miniconda的镜像源 docker pull continuumio miniconda3 二 启动容器 docker run id p 宿主机端口 容器端口 name 自己取的容器名 v 宿主机目录 容器目
  • Python 综合应用小项目一

    数据库报错重连机制 利用异常捕获来获取mysql断开的报错 然后再重连 1 import MySQLdb as mysql 2 3 class DB 4 def init self host user passwd db name 5 se
  • 基于spring validation多层对象校验

    1 第一层对象定义 package com ybw validation demo vo import lombok AllArgsConstructor import lombok Data import lombok NoArgsCon
  • idea workspace.xml 报错

    1 找到 workspace xml 位置并删除 2 重新install
  • LeGO-LOAM 系列(1): LeGO-LOAM 安装以及概述

    一 github GitHub RobustFieldAutonomyLab LeGO LOAM 二 安装依赖 1 ROS Ubuntu 64 bit 16 04 ROS Kinetic 比较常规 就不赘述了 2 gtsam Georgia
  • 集合引用类型 上

    目录 Object Array 创建数组 数组的静态方法 数组空位 数组索引 检测数组 迭代器方法 复制和填充方法 转换方法 栈方法 队列方法 排序方法 操作方法 搜索和位置方法 迭代方法 归并方法 Object 显式地创建Object 的
  • Windows7 Python3 搭建Scrapy 爬虫框架

    Windows7 64位 Python3 7 安装Scrapy 提示如下错误信息 解决办法 1 在python库中下载twisted相应的包 whl文件 官网地址 https www lfd uci edu gohlke pythonlib
  • Android底部导航栏的三种风格实现

    一 效果图展示 如果动图没有动的话 也可以看下面这个静态图 以下挨个分析每个的实现 这里只做简单的效果展示 大家可以基于目前代码做二次开发 二 BottomNavigationView 这是 Google 给我们提供的一个专门用于底部导航的
  • JWT 登录认证及 token 自动续期方案解读

    欢迎关注方志朋的博客 回复 666 获面试宝典 方志朋 号主为CSDN博客之星 博客访问量突破一千万 著有畅销书 深入理解SpringCloud与微服务构建 主要分享Java 后端架构等技术 用大厂程序员的视角来探讨技术进阶 面试指南 职业