【黑马点评优化】1-使用JWT登录认证+redis实现自动续期

项目github地址如下:
https://github.com/xianghua-2/hm-dianping

0 前言

原先的黑马点评中。用户登陆以及后续的鉴权,用的都是用uuid随机生成的token。并不包含任何有效信息

存在以下缺点

  • 缺陷 1:每次请求需查询 Redis
    UUID 本身不携带用户信息,每次鉴权需通过 token 作为 Key 查询 Redis 获取用户数据,高频请求下会显著增加 Redis 负载(参考 9)。
  • 缺陷 2:无法快速验证 Token 合法性
    UUID 没有加密或签名机制,容易被伪造(如恶意构造 token 绕过鉴权),安全性低。
  • 缺陷 3:无法主动失效 Token
    若用户主动退出或 Token 泄露,只能依赖 Redis 过期策略被动等待失效,无法实时拦截。

因此想要用jwt+redis来做登录鉴权

  • 目标 1:减少 Redis 查询压力
    JWT 中直接携带用户 ID,鉴权时无需每次查询用户基础信息(如 ID、角色),仅需解析 JWT。
  • 目标 2:增强安全性
    JWT 通过签名机制防止篡改,结合 Redis 管理 Token 状态,支持主动失效(如踢人下线)。
  • 目标 3:优化存储结构
    仅需将动态或敏感数据(如权限列表、会话状态)存 Redis,减少冗余(参考 36)。

1 原先的redis实现登录鉴权

先说明一下原先的登录注册是如何实现的

具体代码在com/hmdp/service/impl/UserServiceImpl.java 中

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
//        // 3.校验验证码
//        Object cacheCode = session.getAttribute("code");
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();

        // @TODO 先不校验验证码
        if(cacheCode == null || !cacheCode.equals(code)){
            //3.不一致,报错
            return Result.fail("验证码错误");
        }
        //注释掉以上部分


        //一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();

        //5.判断用户是否存在
        if(user == null){
            //不存在,则创建
            user =  createUserWithPhone(phone);
        }

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), //beanToMap方法执行了对象到Map的转换
                CopyOptions.create()
                        .setIgnoreNullValue(true) //BeanUtil在转换过程中忽略所有null值的属性
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); //对于每个字段值,它简单地调用toString()方法,将字段值转换为字符串。
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);

    }

重点关注如下信息

可以看到,先随机生成UUID 作为登录令牌token,也就是后面要存入redis中的key。(之后这个key就对应该用户了)

后面,将用户信息转换成hashMap结构,最后将key和value(用户信息)一起存入redis中。

后续用户发起请求时,会带着生成的token,然后redis中就可以根据token查找到对应用户的信息。

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), //beanToMap方法执行了对象到Map的转换
                CopyOptions.create()
                        .setIgnoreNullValue(true) //BeanUtil在转换过程中忽略所有null值的属性
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); //对于每个字段值,它简单地调用toString()方法,将字段值转换为字符串。
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);

2 JWT登录认证+Redis自动续期

可以看到,由于UUID中并不带有有效信息,并且生成的token比较长,会增加redis的存储压力。

在这里,就考虑到使用JWT来实现登录认证和后续的鉴权。

在此之前,我们先来了解一下现有的常用的登录认证方法。以及什么是认证(Authentication),什么是鉴权(Authorization)。

2.1 认证(identification)授权 (authorization)和鉴权(Authorization)

2.1.1 认证(identification)

认证通俗的来讲就是验证用户的身份,证明你是你自己。

举个例子,上班打卡的时候,需要用人脸认证/指纹认证,当你的人脸和指纹与数据库中录入的相匹配的时候,就打卡成功。

具体到互联网中:

  • 用户名密码登录
  • 邮箱发送登陆链接
  • 微信登录

一般用于登录某个系统。

2.1.2 授权 (authorization)

简单来说,授权一般是指获取用户的委派权限。在英文中对应于authorization这个单词。

在信息安全领域,授权是指资源所有者委派执行者,赋予执行者指定范围的资源操作权限,以便执行者代理执行对资源的相关操作。这里面包含有如下四个重要概念,

资源所有者,拥有资源的所有权利,一般就是资源的拥有者。
资源执行者,被委派去执行资源的相关操作。
操作权限,可以对资源进行的某种操作。
资源,有价值的信息或数据等,受到安全保护。
需要说明的是,资源所有者和执行者可以是自然人,就是普通用户,但不限于自然人。在信息安全领域,资源所有者和执行者,很多时候是应用程序或者机器。比如用户在浏览器上登录一个网站,那么这个浏览器就成为一个执行者,它在用户登录后获取了用户的授权,代表着用户执行各种指令,进行购物、下单、付钱、转账等等操作。

同时,资源所有者和执行者可以是分开的不同实体,也可以是同一个。若是分开的两者,则资源执行者是以资源所有者的代理形式而存在。

授权的实现方式非常多也很广泛,我们常见的银行卡、门禁卡、钥匙、公证书,这些都是现实生活中授权的实现方式。其实现方式主要通过一个共信的媒介完成,这个媒介不可被篡改,不可随意伪造,很多时候需要受保护,防止被窃取。

在互联网应用开发领域,授权所用到的授信媒介主要包括如下几种,

通过web服务器的session机制,一个访问会话保持着用户的授权信息
通过web浏览器的cookie机制,一个网站的cookie保持着用户的授权信息
颁发授权令牌(token),一个合法有效的令牌中保持着用户的授权信息
前面两者常见于web开发,需要有浏览器的支持。

2.1.2 鉴权(authentication)

鉴权比认证多的其实是授权这一步。

鉴权:确认用户/实体身份及权限。同时进行认证和授权。确认用户的身份后,授予其访问资源的权限。

鉴权是指对于一个声明者所声明的身份权利,对其所声明的真实性进行鉴别确认的过程。在英文中对应于authentication这个单词。

鉴权主要是对声明者所声明的真实性进行校验。若从授权出发,则会更加容易理解鉴权。授权和鉴权是两个上下游相匹配的关系,先授权,后鉴权。授权和鉴权两个词中的“权”,是同一个概念,就是所委派的权利,在实现上即为授信媒介的表达形式。

因此,鉴权的实现方式是和授权方式有一一对应关系。对授权所颁发授信媒介进行解析,确认其真实性。下面是鉴权的一些实现方式,

门禁卡:通过门禁卡识别器
钥匙:通过相匹配的锁
银行卡:通过银行卡识别器
互联网web开发领域的session/cookie/token:校验session/cookie/token的合法性和有效性
鉴权是一个承上启下的一个环节,上游它接受授权的输出,校验其真实性后,然后获取权限(permission),这个将会为下一步的权限控制做好准备。

再举个例子。

你通过用户名/密码登录到学校的图书馆里系统中。学校的图书馆管理系统确认了你的身份 (认证

之后,图书馆系统会根据你的身份(学生/老师/管理员)来分配不同的权限。(授权

鉴权就是上述两步加起来。认证+授权。

2.2 为什么项目中要鉴权以及现有鉴权方案

登录时,需要输入手机号和验证码进行登录。

在这一步进行登录认证。

但是,当用户登录之后,后面会不停的向服务器发起请求,服务器是怎么知道是哪个用户发出的呢?(HTTP无状态协议)

  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • 服务器为什么要知道是哪个用户发出的?

    答:因为有些页面只有登录之后/特定用户才能访问。

所以,为了解决http无状态协议带来的,服务器不知道哪个用户发起的请求这一问题。

现有的解决方案有:cookie,session,jwt,token。

简单说明一下这三者的区别和共同点。

Cookie

  • cookie 存储在客户端:cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
  • cookie 是不可跨域的:每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。

Session

  • session 是另一种记录服务器和客户端会话状态的机制
  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中

image.png

session 认证流程:

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
  • 是一种认证授权机制。
  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
  • 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。

2.3 项目中的鉴权(认证+授权)

因此,使用JWT+Redis来实现项目鉴权。

用户登录

  • 生成jwttoken(token中存储用户id)

  • Redis中也将用户id作为key,用户信息+当前生成的jwttoken作为value

鉴权:

  • 后续,从请求的authorization字段中,获取jwttoken
  • 解析jwttoken,得到用户id
  • 根据用户id,去redis查当前用户最新对应的jwttoken2
  • 判断jwttoken是否和jwttoken2一致。
  • 一致,则说明当前jwttoken有效,放行。

为什么redis中除了要存储用户信息,同时还要存储当前生成的jwttoken呢?

  • 因为假如不存储当前生成的jwttoken的话,而是只根据从jwt中解析出的userid,去redis中查询当前是否有效。会存在以下情况
  • 用户a先在某平台登录,此时后端返回给当前的jwttoken1;()
  • 之后,用户a退出登录。
  • 之后,某时刻,用户a再次登录平台,此时后端会返回给新的jwttoken2;
  • 与此同时,我们假设用户b盗取了用户a上次登录的jwttoken1;
  • 此时,用户b也能够登录后端。(因为jwttoken1,jwttoken2解析出的userId都是一致的。去redis中查询的话,由于用户a现在是在登录的状态,所以redis中该userId也是有效的)

我们通过存储当前最新的jwttoken就能防止这种情况。

方案对比表如下:

维度UUID Token + RedisJWT + Redis
鉴权性能每次请求查 Redis仅解析 JWT,按需查 Redis
安全性低(无签名)高(签名防篡改)
主动失效能力依赖 Redis 过期支持实时踢人下线
存储冗余高(完整用户数据)低(仅动态数据)
适用场景简单低频场景中高频、需安全控制的场景

3 实现

3.1 jwt依赖引入

  1. pom.xml中引入依赖
        <!--引入jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
        </dependency>

3.2 Jwt相关文件

3.2.1 JwtUtil.java

com/hmdp/utils/JwtUtil.java

在utils下创建工具类文件JwtUtil.java

内容如下:

  • createJWT(String secretKey, long ttlMillis, Map<String, Object> claims)

    根据传入的密钥,过期时间和要存储的信息,创建jwttoken

  • parseJWT(String secretKey, String token)

    根据传入的密钥和jwttoken,解析出jwt中存储的信息。

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;


public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

3.2.5 JWTUtil单元测试

(1)在pom.xml中引入juit相关依赖

        <!--引入junit-->
        <!-- 单元测试Junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

(2)在test/java/com.hmdp文件夹下

新建utils package

之后,新建JwtUtilTest.java文件如下。

运行单元测试即可

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.security.Keys;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;

import javax.crypto.SecretKey;

public class JwtUtilTest {

    // 密钥
    private static final String SECRET_KEY = "ThisIsA32BytesLongSecretKeyForHS256";

    private Map<String,Object> sampleClaims;

    @Before
    public void setUp() {
        sampleClaims = new HashMap<>();
        sampleClaims.put("userId", 1001);
        sampleClaims.put("username", "testUser");
    }

    // 正常场景测试
    @Test
    public void testCreateJWT() {
        String token = JwtUtil.createJWT(SECRET_KEY, 3600000L, sampleClaims);

        Claims claims = JwtUtil.parseJWT(SECRET_KEY, token);
        System.out.println(claims);
        // 断言验证
        Assertions.assertEquals(1001, claims.get("userId"));
        Assertions.assertEquals("testUser", claims.get("username"));
        Assertions.assertNotNull(claims.getExpiration());
    }

    // 异常场景:过期Token测试
    @Test
    public void should_throw_expired_exception() throws InterruptedException {
        // 生成1毫秒过期的Token
        String token = JwtUtil.createJWT(SECRET_KEY, 1L, sampleClaims);
        Thread.sleep(2); // 确保过期

        Assertions.assertThrows(ExpiredJwtException.class,
                () -> JwtUtil.parseJWT(SECRET_KEY, token));
    }

    // 异常场景:签名密钥错误测试
    @Test
    public void should_throw_signature_exception() {
        String token = JwtUtil.createJWT(SECRET_KEY, 3600000L, sampleClaims);

        Assertions.assertThrows(SignatureException.class,
                () -> JwtUtil.parseJWT("wrongSecretKey", token));
    }

    // 边界测试:空claims处理
    @Test
    public void should_handle_empty_claims() {
        Map<String, Object> emptyClaims = new HashMap<>();
        String token = JwtUtil.createJWT(SECRET_KEY, 3600000L, emptyClaims);

        Claims claims = JwtUtil.parseJWT(SECRET_KEY, token);
        Assertions.assertTrue(claims.isEmpty());
    }

}

3.2.3 JwtClaimsConstant.java

com.hmdp下新建一个constant package之后,新建JwtClaimsConstant类

com/hmdp/constant/JwtClaimsConstant.java

public class JwtClaimsConstant {

    public static final String EMP_ID = "empId";
    public static final String USER_ID = "userId";
    public static final String PHONE = "phone";
    public static final String USERNAME = "username";
    public static final String NAME = "name";

}

3.2.4 JwtProperties

com.hmdp下新建properties包,之后新建JwtProperties类

com/hmdp/properties/JwtProperties.java

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "hmdp.jwt")
@Data
public class JwtProperties {

//    /**
//     * 管理端员工生成jwt令牌相关配置
//     */
//    private String adminSecretKey;
//    private long adminTtl;
//    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;
    private long userTtl;
    private String userTokenName;

}

3.2.5 application.yaml

加上以下内容。

在application中配置jwttoken的密钥,过期时间以及headername

hmdp:
  jwt:
#    # 设置jwt签名加密时使用的秘钥
#    admin-secret-key: itcast
#    # 设置jwt过期时间
#    admin-ttl: 7200000
#    # 设置前端传递过来的令牌名称
#    admin-token-name: token
    user-secret-key: ThisIsA32BytesLongSecretKeyForHS256
    user-ttl: 7200000
    user-token-name: authorization

3.3 登录鉴权逻辑修改

3.3.1 登录鉴权逻辑设计思路

(1)之前的登录鉴权(UUID+Redis)

回想一下,之前的登录鉴权主要涉及两个地方,一个是用户登录,一个是双拦截器拦截用户请求时。

登录

  • 用户登录时根据uuid生成token,并且将用户信息存储到redis中(key是token

登录完后发的其他请求

  • 登陆后的其他请求,发送时会带着authorizatioin字段(存储token值)。请求会经过以下两个拦截器。
  • 拦截器1(refreshToken):
    • 拦截所有请求
    • 判断当前请求中是否有authorizatioin字段
    • 有的话根据authorizatioin字段的token去redis中查询是否存在当前用户
      • 存在的话,将查到的用户信息放到ThreadLocal中,并刷新redis中token的有效期
      • 不存在的话,放行
    • 没有的话放行
  • 拦截器2(LoginInterceptor)
    • 拦截需要登录的页面请求(比如/user/login/me)
      • 从ThreadLocal中查找是否有当前用户
      • 无则不放行
      • 查到了放行。

(2)现在的登录鉴权(JWT+Redis)

因此,现在我们也是。

  • 用户登录时生成jwttoken(存储的有效信息为userid),并且将用户信息和jwttoken存储到redis中(key是userid
  • 登陆后的其他请求,发送时会带着authorizatioin字段,解析authorizatioin中的jwttoken,之后从jwttoken中解析出userId,根据userId去redis中查询,判断当前用户是否登录。

登录

  • 用户登录时生成jwttoken(存储的有效信息为userid),并且将用户信息和jwttoken存储到redis中(key是userid

登录完后发的其他请求

  • 登陆后的其他请求,发送时会带着authorizatioin字段(存储token值)。请求会经过以下两个拦截器。
  • 拦截器1(refreshToken):
    • 拦截所有请求
    • 判断当前请求中是否有authorizatioin字段
    • 有的话根据authorizatioin字段的jwttoken解析出userId,根据userId去redis中查询是否存在当前用户,并且对比jwttoken和redis中查出的jwttoken是否一致。
      • 存在且一致的话,将查到的用户信息放到ThreadLocal中,并刷新redis中token的有效期
      • 不存在的话,放行
    • 没有的话放行
  • 拦截器2(LoginInterceptor)
    • 拦截需要登录的页面请求(比如/user/login/me)
      • 从ThreadLocal中查找是否有当前用户
      • 无则不放行
      • 查到了放行。

3.3.2 登录UserServiceImpl修改

修改UserServiceImpl.java中的login方法如下:

    // @TODO 2.使用JWT实现登录功能
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
//        // 3.校验验证码
//        Object cacheCode = session.getAttribute("code");
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();

        // @TODO 假如要生成1k个用户测试的话,注释掉以下部分,先不校验验证码
//        if(cacheCode == null || !cacheCode.equals(code)){
//            //3.不一致,报错
//            return Result.fail("验证码错误");
//        }
        //注释掉以上部分


        //一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();

        //5.判断用户是否存在
        if(user == null){
            //不存在,则创建
            user =  createUserWithPhone(phone);
        }

        // 6.生成JWT
        Map<String,Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID,user.getId());
//        String token = JwtUtil.createJWT(jwtProperties.getSecretKey(),jwtProperties.getTtl(),claims);
//        String jwttoken = JwtUtil.createJWT("ThisIsA32BytesLongSecretKeyForHS256",30*60*1000,claims);
        String jwttoken = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);


        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
//        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), //beanToMap方法执行了对象到Map的转换
                CopyOptions.create()
                        .setIgnoreNullValue(true) //BeanUtil在转换过程中忽略所有null值的属性
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); //对于每个字段值,它简单地调用toString()方法,将字段值转换为字符串。
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + userDTO.getId();
        // 7.4.将jwttoken存入userMap中
        userMap.put("jwttoken",jwttoken);
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.5.设置redis中 userId的有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(jwttoken);

    }

3.3.3 RefreshTokenInterceptor修改

修改RefreshTokenInterceptor如下

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;
    private final JwtProperties jwtProperties; // 直接通过构造器注入


    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate,JwtProperties jwtProperties) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.jwtProperties = jwtProperties; // 手动接收依赖
    }
/* 1.基于token来拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }*/


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
//        String token = request.getHeader("authorization");
        String token = request.getHeader(jwtProperties.getUserTokenName());
        if (StrUtil.isBlank(token)) {
            return true;
        }
        Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(),token);
        Long userId =  claims.get(JwtClaimsConstant.USER_ID,Long.class);
        // 2.基于userId获取redis中的用户
        String key  = LOGIN_USER_KEY + userId;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 4.判断token是否一致,防止有以前生成的jwt,仍然能够登录
        String jwttoken = userMap.get("jwttoken").toString();
        if(!jwttoken.equals(token)){
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

3.3.4 修改MvcConfig如下

import com.hmdp.properties.JwtProperties;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
//Configuration注解表明该类是一个配置类,它允许你通过Java代码来配置Spring应用程序,而不是使用XML文件。Spring容器在启动时会扫描并加载这些配置类。
public class MvcConfig implements WebMvcConfigurer { //WebMvcConfigurer接口允许自定义Spring MVC的配置。通过实现这个接口,可以添加拦截器、视图控制器、视图解析器等。

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private JwtProperties jwtProperties;


    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);

        // order越小,优先级越高,所以是先会通过token刷新拦截器
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate,jwtProperties)).addPathPatterns("/**").order(0); //拦截所有请求
    }
}

4 测试

4.1 登录测试

使用APIfox测试

http://localhost:8081/user/login

其中请求体如下:

// 场景1:手机号+验证码
{
  "phone": "13812345678",
  "code": "123456"
}

在这里插入图片描述

4.2 鉴权测试

GET请求,下面的请求是只有登录后才能够得到结果的。注意要加上authorization参数,参数值为刚刚返回的data。

http://localhost:8081/user/me

当jwttoken不对的时候

在这里插入图片描述

jwttoken正确的时候。

在这里插入图片描述

参考资料

还分不清 Cookie、Session、Token、JWT?看这一篇就够了-阿里云开发者社区 (aliyun.com)

认证、授权、鉴权和权限控制概念区别_鉴权和授权的区别-CSDN博客

JWT(JSON Web Token)没有生效可能有多种原因,以下是一些常见的问题及其解决方法: 1. **Token格式错误**: - 确保JWT的格式正确,通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),这三部分之间用点(.)分隔。 - 例如:`xxxxx.yyyyy.zzzzz` 2. **签名不匹配**: - 确保用于生成和验证JWT的密钥(或公钥/私钥对)一致。如果密钥不匹配,验证将失败。 - 检查代码中用于签名和验证的密钥是否正确。 3. **Token过期**: - JWT通常有一个过期时间(exp claim)。如果当前时间超过了过期时间,Token将失效。 - 检查Token的过期时间,并在需要时生成新的Token。 4. **Token被篡改**: - 如果Token的签名不正确,可能是Token被篡改了。确保Token在传输过程中没有被修改。 - 验证Token的签名以确保其完整性。 5. **编码问题**: - 确保Token在传输过程中没有被错误地编码或解码。例如,Base64编码和解码是否正确。 6. **服务器配置问题**: - 检查服务器端中间件或框架的配置,确保它们正确处理JWT。例如,Express.js中的`jsonwebtoken`中间件配置是否正确。 7. **浏览器存储问题**: - 如果Token存储在浏览器的本地存储或Cookie中,确保没有跨域问题或其他安全问题导致Token无法正确读取。 8. **中间件顺序问题**: -使用Express.js等框架时,确保JWT验证中间件在路由处理之前正确配置。 以下是一个简单的示例,展示了如何在Node.js中使用`jsonwebtoken`库生成和验证JWT: ```javascript const jwt = require('jsonwebtoken'); // 生成JWT const payload = { userId: 123 }; const secretKey = 'your-secret-key'; const token = jwt.sign(payload, secretKey, { expiresIn: '1h' }); console.log('Generated Token:', token); // 验证JWT const tokenToVerify = 'your-jwt-token'; jwt.verify(tokenToVerify, secretKey, (err, decoded) => { if (err) { console.error('Token verification failed:', err); } else { console.log('Decoded Token:', decoded); } }); ```
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值