【Spring Boot】3.4 JWT 的使用

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

引入组件

<!-- 引入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保存的用户登录时间对比 不同则验证失败(以此可以实现单点登录)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值