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

news2025/3/13 0:47:06

前言

过去这段时间主要负责了项目中的用户管理模块,用户管理模块会涉及到加密及认证流程。今天就来讲讲认证功能的技术选型及实现。技术上没啥难度当然也没啥挑战,但是对一个原先没写过认证功能的菜鸡来说也是一种锻炼吧。

技术选型

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

业务实现

登陆功能

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();
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/433221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【短篇】函数重载

函数重载 重载使用规则 重载 函数重载也是CPP相较于C的一个优化内容。 在C中我们都知道函数名不能重名 当然这个错误对于我还有类似我这种只学过C的人来说&#xff0c;这个错误是显得多么理所当然。 但是在Cpp中&#xff0c;则对这个功能进行了优化 看到这就有人问了&#x…

我去蔚来试驾了

前面写了比亚迪汉、小鹏P7i的试驾体验&#xff0c;链接如下&#xff1a; 小鹏P7I试驾体验&#xff01; 今天接着分享蔚来ET5的试驾体验&#xff0c;实话实说&#xff0c;我是蔚来ET5的颜粉&#xff0c;颜值也是ET5最大的卖点之一。 我身边不少朋友&#xff0c;不管是男生还是女…

windows下使用vite搭建vue开发环境

windows下使用vite搭建vue开发环境 1 下载安装配置NodeJS1.1 下载1.2 安装1.3 配置1.4 npm镜像加速配置1.6 设置环境变量 2 Vite安装 1 下载安装配置NodeJS 1.1 下载 下载地址&#xff1a;https://nodejs.cn/download 到NodeJS官网&#xff0c;选择Windows安装包即可。 1.2…

网络协议-UDP vs TCP, HTTP2.0和3.0

目录 TCP vs UDP Internet协议群&#xff08;TCP/IP协议群&#xff09; 传输层和网络层 User Data Diagram&#xff08;UDP&#xff09; 连接 重发&#xff08;校验&#xff09; UDP不保证顺序 思考&#xff1a;看了上面的几点&#xff0c;UDP没有虚拟连接、不校验数据、…

Vue ElementUI Axios 前后端案例(day01) 之Vue

前言js 问js是什么&#xff0c;他有什么作用&#xff0c;与html和css的区别是什么 JavaScript&#xff08;简称JS&#xff09;是一种脚本语言&#xff0c;用于在网页上实现交互效果、动态效果和动态数据更新等功能。它是一种解释性语言&#xff0c;需要在浏览器中解释和执行。…

【Paper Note】Swin Transformer: Hierarchical ViT using Shifted Windows

Swin Transformer: Hierarchical ViT using Shifted Windows 概述核心思想整体结构名词解释与vit区别 模型处理过程概括Patch EmbeddingBasicLayerPatch MergingSwin Transform BlockWindow AttentionShifted Window Attention小结 模型使用及代码模型使用环境配置SwinT 代码Pa…

跨境卖家都要知道的:对话式销售

买家可以用他们的指纹登录大多数东西&#xff0c;并通过与它交谈来管理他们的日历。这些人不会填写一份表格&#xff0c;如果他们填写的字段越多&#xff0c;表格的长度就会越长。如果他们知道只会受到骚扰&#xff0c;他们当然不会下载某些东西。 相反&#xff0c;他们更喜欢…

[Linux系统]系统安全及应用一

系统安全及应用一、账号安全基本措施1.1系统账号清理1.1.1将非登录用户的shell设为/sbin/nologin1.1.2锁定长期不使用的账号1.1.3删除无用的账号1.1.4锁定账号文件文件chattr1.1.5查看文件校验和md5sum1.2密码安全控制1.2.1设置密码有效期1.3历史命令限制1.3.1 减少记录命令的条…

5GHz无线局域网系统模拟

移动电视双天线分集接收技术 随着DVB-T在手机电视、车载电视、楼宇电视、地铁电视等户外广播领域内的发展&#xff0c;在这些接收范围内&#xff0c;多径衰落、多普勒频移等小范围衰落是不可避免的问题&#xff0c;解决这些衰落和干扰成为倍受关注的问题。为了解决衰落&#x…

rk3568 点亮LCD (BT656 BT1120)

rk3568 适配 BT656/BT1120 BT.656 TX 和 BT.1120 TX&#xff0c;是一种并行输出接口&#xff0c;而 Camera 对应的是 BT.656 RX和 BT.1120 RX&#xff0c;是一种并行输入接口&#xff0c;两则在协议上是一致的。与同为并口的RGB非常像&#xff0c;在rk3568 芯片上RGB和BT656/B…

【jenkins】Jenkins连接 Gitlab实现 push代码自动构建

目录 一、安装插件 二、构建任务 三、为任务配置触发器 四、到gitlab进行设置webhooks 4.1 设置网络 4.2 到jenkins对应项目的源码库 4.3 测试 4.3.1 点击测试--标签推送事件 4.3.2 点击编辑 一、安装插件 持续部署的第一步需要检查是否安装gitlab插件&#xff1a; gitla…

Doris(9):删除数据(Delete)

Delete不同于其他导入方式&#xff0c;它是一个同步过程。和Insert into相似&#xff0c;所有的Delete操作在Doris中是一个独立的导入作业&#xff0c;一般Delete语句需要指定表和分区以及删除的条件来筛选要删除的数据。 Doris 目前可以通过两种方式删除数据&#xff1a; DE…

记录-JavaScript常规加密技术

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 当今Web开发中&#xff0c;数据安全是一个至关重要的问题&#xff0c;为了确保数据的安全性&#xff0c;我们需要使用加密技术。JavaScript作为一种客户端编程语言&#xff0c;可以很好地为数据进行加…

Spring Boot 安全

目录 1.概述 2.token 2.1.理论 2.2.使用 3.JWT 3.1.理论 3.2.使用 4.oauth 5.Spring Security 5.1.概述 5.2.基本认证授权 5.3.加密 1.概述 在后端来说&#xff0c;安全主要就是控制用户访问&#xff0c;让对应权限的用户能访问到对应的资源&#xff0c;主要是两点…

AOP通知中获取数据

AOP通知中获取数据 之前我们写AOP仅仅是在原始方法前后追加一些操作&#xff0c;接下来我们要说说AOP中数据相关的内容&#xff0c;我们将从获取参数、获取返回值和获取异常三个方面来研究切入点的相关信息。 获取切入点方法的参数&#xff1a;所有的通知类型都可以获取参数 …

Vulhub开源漏洞靶场用Java远程访问

事件起因&#xff0c;被迫参加某竞赛&#xff0c;中途发现&#xff0c;全员摸鱼&#xff0c;遂一起摸鱼Vulhub是一个面向大众的开源漏洞靶场&#xff0c;无需docker知识&#xff0c;简单执行一条命令即可编译、运行一个完整的漏洞靶场镜像。 Installation 在Ubuntu 20.04下安…

JVM 垃圾回收详解之内存分配和回收原则+死亡对象判断方法

前言 当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时&#xff0c;我们就需要对这些“自动化”的技术实施必要的监控和调节。 堆空间的基本结构 Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时&#xff0c;Java 自动内存管理最核…

【STM32】基础知识 第七课 存储器映射 寄存器映射

【STM32】基础知识 第七课 存储器映射 & 寄存器映射 STM32 寻址范围存储器映射存储器功能划分 (F1 为例)Block 0Block 1Block 2寄存器映射 寄存器映射 (F1 为例)寄存器映射举例寄存器地址计算GPIO 外设基地址及偏移量寄存器地址及偏移量寄存器地址计算过程 使用结构体映射寄…

《2-数组》

数组 1.简介&#xff1a; 数组&#xff08;Array&#xff09;是一种固定长度的存储相同数据类型在连续内存空间中的数据结构 引出&#xff1a;[索引 &#xff08;Index&#xff09;]----元素在数组中的位置 2.初始化 写法&#xff1a;一般用到无初始值、给定初始值 在不给定…

中国制造业连续13年全球第一,MES管理系统,打造竞争新优势

根据工业和信息化部最近发布的数据&#xff0c;在2022年&#xff0c;中国的制造业增加值在全球的占比接近30&#xff05;&#xff0c;制造业规模已连续13年位居世界第一。根据国家统计局的最新数字&#xff0c;一到二月份&#xff0c;我国的生产值与去年同期相比上升了2.1&…