【Spring Boot】3.4 JWT 的使用

本文介绍了如何在Spring Boot项目中使用JWT进行权限验证。内容包括引入JWT组件,创建JWT工具类,设置拦截器验证Token合法性,并讨论了密码修改后旧Token失效和单点登录的简单实现。拦截器利用自定义工具类验证前端传回的Token,并通过Redis存储敏感信息以确保安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引入组件

<!-- 引入JWT依赖 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

创建JWT工具类

import com.alibaba.druid.util.StringUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import java.util.Calendar;
import java.util.Date;

/***
 * Author: YL.Lou
 * Class: JWTUtil
 * Project: washes_base_backstage
 * Introduce: JWT的验证工具类 被生成Token的方法Service调用 也会被 拦截器调用 验签
 * DateTime: 2022-06-29 19:23
 ***/
 
@Slf4j
@Component
public class JWTUtil {
    // 为Redis存储准备的Key前缀 后边跟的是用户的 ID  例如:JWT_SIGN_1 查询到的是 MD5 之后的 用户密码 信息
    public static final String SIGN = "JWT_SIGN_";

    // 加盐
    private static final String SECRET = "lou123321!!!";

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 获取token
     * @param u user
     * @return token
     */
    public String getToken(User u) {

        Calendar instance = Calendar.getInstance();

        //默认令牌过期时间30天
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        instance.add(Calendar.DATE, 7);

        JWTCreator.Builder builder = JWT.create();

        builder.withClaim("userId", String.valueOf(u.getuId()))
                .withClaim("userPhone", u.getuPhone())
                .withClaim("userEmail", u.getuEmail())
                .withClaim("userLoginTime", String.valueOf(u.getuLoginTime()))
                .withClaim("userName", u.getuName())
                .withClaim("expTime", simpleDateFormat.format(new Date(instance.getTime().getTime())));

        // 将 用户ID + 用户密码 用MD5 混淆 再加盐 获取的字符串 用来生成签名
        return builder.withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(BaseUtil.getMD5(u.getuId() + u.getuPassword()) + SECRET));
    }

    /**
     * 验证token合法性 成功返回token
     */
    public DecodedJWT verify(String token, Long uId) throws Exception {

        // 从Redis中获取用户ID + 密码 并被MD5 混淆后的字符串
        String strSign = redisUtil.get(SIGN + uId);

        if(null == strSign){
            throw new Exception("Original Token 无效或已过期");
        }

        if(StringUtils.isEmpty(token)){
            throw new Exception("token不能为空");
        }

        JWTVerifier build = JWT.require(Algorithm.HMAC256(strSign + SECRET)).build();

        return build.verify(token);
    }

   /* public static void main(String[] args) {
        DecodedJWT verify = verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTcxMDg1MDAsInVzZXJuYW1lIjoiYWRtaW4ifQ.geBEtpluViRUg66_P7ZisN3I_d4e32Wms8mFoBYM5f0");
        System.out.println(verify.getClaim("password").asString());
    }*/
}

创建一个拦截器 将验证工具注入拦截器

这里便用到了很多我自己写的工具类和实体类 包括Redis的工具类
在前面的文章中都能找到相应的文件

import com.alibaba.druid.util.StringUtils;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.format.DateTimeFormatter;

/***
 * Author: YL.Lou
 * Class: JWTInterceptor
 * Project: washes_base_backstage
 * Introduce: JWT 拦截器
 * DateTime: 2022-06-29 19:34
 ***/
@Slf4j
@Component
public class JWTInterceptor implements HandlerInterceptor {

    @Autowired
    private JWTUtil jwtUtil;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从Header中获得Token 和 uid 这两个是要与前端同步的

        String token = request.getHeader("token");
        String uid = request.getHeader("uid");

        if(StringUtils.isEmpty(token)){
            throw new Exception("Header 未装载 token");
        }

        if(StringUtils.isEmpty(uid)){
            throw new Exception("Header 未装载 uid");
        }

        try {

            // 得到签名实体
            DecodedJWT verify = jwtUtil.verify(token, Long.valueOf(uid));

            // 得到签名中的登录时间
            String loginTimeFromToken = verify.getClaim("userLoginTime").asString();

            log.info("token:" + loginTimeFromToken);

            // 从Redis中获取用户的信息
            User user = redisUtil.getObject(RedisPrefix.USER + uid, User.class);

            String loginTimeFromRedis = BaseUtil.localDateTime2String(user.getuLoginTime());

            log.info("redis:" + loginTimeFromRedis);

            if (!loginTimeFromRedis.equals(loginTimeFromToken)){
                throw new Exception("用户Token已更新");
            }

        } catch (SignatureVerificationException e) {

            throw new Exception("无效Token签名");
        } catch (TokenExpiredException e) {

            throw new Exception("token过期");
        } catch (AlgorithmMismatchException e) {

            throw new Exception("token算法不一致");
        } catch (Exception e) {
            throw new Exception("token无效:" + e.getMessage());
        }

        return true;
    }
}

这个拦截器就是调用了工具类中验证Token的部分 对前端用户传回的Token是否合法 以及是否保存了UID等信息进行验证
验证通过就放行
验证失败就抛出异常 由统一错误处理类承接 并最终给前端返回错误信息

配置拦截器 注入到SpringBoot容器

import cn.ihuanxi.base.interceptor.JWTInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器的配置文件
 */

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private JWTInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                //拦截的路径
                .addPathPatterns("/**")
                //排除登录接口
                .excludePathPatterns("/public/**");
    }
}

代码很简单,处理的业务逻辑也很简单,就是一个AOP思想的严重体现

生成Token的部分

前端用户拿到的Token通常是在Service层调用JWTUtil生成

    /**
     * 根据短信验证码完成登录
     * @param uPhone
     * @param smsCode
     * @return
     */
    @DS("slaver")
    @Override
    public Result loginBySmsCode(String uPhone, String smsCode) {

   			// 通过检查Redis中是否存在该手机的值即可实现
         String redisValue = redisUtil.get(RedisPrefix.SMS_CODE + uPhone);

         // 如果不为null 说明刚刚发送过手机短信
         if (null == redisValue || redisValue.equals("")) {
             return result.failed("短信验证码无效", smsCode);
         }

         // 判断输入的短信验证码是否正确
         if (!smsCode.equals(redisValue)) {
             return result.failed("短信验证码错误", smsCode);
         }

         // 验证成功后该验证码就会被移除
         redisUtil.delete(RedisPrefix.SMS_CODE + uPhone);

        // 根据手机号获取用户信息
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("u_phone",uPhone);
        User user = userMapper.selectOne(userQueryWrapper);

        // 判断用户是否存在
        if (null == user){
            // TODO: 如果开启自动注册 则此处应该进行二次处理 2022-7-2 16:15:12
            return result.failed("用户手机号码未注册");
        }

        // 更新用户的登录时间 和 手机号码认证状态
        user.setuLoginTime(BaseUtil.str2LocalDateTime(null));
        user.setuPhoneChecked(true);

        // 保存用户登录时间和手机认证状态
        userMapper.updateById(user);

        // 将用户ID+密码 MD5后 存到Redis,用作签名的同时方便后期查验 (30天过期)
        redisUtil.add(RedisPrefix.TOKEN_SIGN + user.getuId().toString(), BaseUtil.getMD5(user.getuId()+user.getuPassword()), 30, TimeUnit.DAYS);

        // 生成Token
        String token = jwtUtil.getToken(user);

        user.setToken(token);

        // 检查用户是否设置过密码 并将结果写入
        user.setPassword(null != user.getuPassword() && !user.getuPassword().equals(""));

        // 将用户所有数据存入Redis 方便后续程序随时调用 避免频繁查库
        redisUtil.add(RedisPrefix.USER + user.getuId().toString(), user);

        // 将生成好的Token返回给前端
        return result.success("用户登录成功", user);
    }
关于密码修改后旧Token失效 以及 单点登录的简单实现

上边的代码很复杂,其核心流程大概就是:

  1. 通过查库中的用户密码和登录时间生成一个 签名用的字符串
  2. 将字符串保存到Redis中
  3. 然后用生成的字符串去给Token值签名
  4. 前端将Token回传时,拦截器拿到用户UID
  5. 根据UID查Redis中的签名字符串 其实就是用户密码和登录时间
  6. 验签通过后就能拿到Token中携带的用户登录时间
  7. 用Token中的用户登录时间和Redis保存的用户登录时间对比 不同则验证失败(以此可以实现单点登录)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值