SpringBoot集成Jwt实现token登陆权限认证

近年来,随着前后端分离、微服务等架构的兴起,传统的cookie+session身份验证模式已经逐渐被基于Token的身份验证模式取代。接下来介绍如何在Spring Boot项目中集成JWT实现Token验证。

一、JWT入门

1.什么是JWT

JWT (Json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。它定义了一种紧凑的,自包含的方式,用于通信双方之间以JSON对象的形式安全传递信息。JWT使用HMAC算法或者是RSA的公私秘钥的数字签名技术,所以这些信息是可被验证和信任的。

JWT官网:https://jwt.io/

JWT(Java版)的github地址:https://github.com/jwtk/jjwt

2.JWT的结构

在使用 JWT 前,需要先了解它的组成结构。它是由以下三段信息构成的:

  • Header 头部(包含签名和/或加密算法的类型)
  • Payload 载荷 (存放有效信息)
  • Signature 签名/签证

将这三段信息文本用‘.’连接一起就构成完整的JWT字符串,也是就我们需要的Token。如下所示:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyUGFzc3dvcmQiOiIxMjNRV0Vxd2UiLCJ1c2VyQWNjb3VudCI6InFpbmdjaGVuZyIsImV4cCI6MTc1NTc0MzMzOX0.p6Ht-BAgxyvIC5axwa6jF6V0It2MSU6g3z3N0Dp3Fes

JWT的数据结构还是比较复杂的,Header,Payload,Signature中包含了很多信息,建议大家最好是能够了解。

3.JWT的请求流程

JWT的请求流程也特别简单,首先使用账号登录获取Token,然后后面的各种请求,都带上这个Token即可。具体流程如下:

1. 客户端发起登录请求,传入账号密码;

2. 服务端使用私钥创建一个Token;

3. 服务器返回Token给客户端;

4. 客户端向服务端发送请求,在请求头中该Token;

5. 服务器验证该Token;

6. 返回结果。

二、Spring Boot 如何集成JWT

JWT提供了基于Java组件:java-jwt帮助我们在Spring Boot项目中快速集成JWT,接下来进行SpringBoot和JWT的集成。

1.引入JWT依赖

创建普通的Spring Boot项目,修改项目中的pom.xml文件,引入JWT等依赖。示例代码如下:

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.14.0</version>
</dependency>
2.创建&验证Token

创建通用的处理类TokenUtil,负责创建和验证Token。示例代码如下:

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.greenorange.promotion.common.ErrorCode;
import com.greenorange.promotion.exception.ThrowUtils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Map;


@Component
public class JWTUtils {


    private static final String SECRET = "qingcheng";


    @Resource
    private RedisTemplate<String, String> redisTemplate;


    /**
     * 生成JWT token
     */
    public String generateToken(Map<String, String> map) {

        Calendar instance = Calendar.getInstance();
        // 默认7天过期
        instance.add(Calendar.DATE, 7);

        //创建jwt builder
        JWTCreator.Builder builder = JWT.create();

        // payload
        map.forEach(builder::withClaim);

        return builder.withExpiresAt(instance.getTime())  //指定令牌过期时间
                .sign(Algorithm.HMAC256(SECRET));
    }


    /**
     * 解析JWT token
     */
    public DecodedJWT verify(String token) {
        // 检查 token 是否在黑名单中
        String tokenFromBlacklist = redisTemplate.opsForValue().get(token);
        ThrowUtils.throwIf(tokenFromBlacklist != null, ErrorCode.NO_AUTH_ERROR, "JWT 已被注销");
        return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    }
}

 3.创建自定义注解,声明调用该接口时需要具备哪种角色/权限

import java.lang.annotation.*;
 
/**
 *  JWT 权限注解
 **/
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresPermission {
     /**
      * 接口调用权限
      */
     String mustRole() default " ";
}
3.创建切面AOP,负责拦截所有被@RequiresPermission注解标记的方法,验证Token是否有效
import com.auth0.jwt.interfaces.DecodedJWT;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.greenorange.promotion.annotation.RequiresPermission;
import com.greenorange.promotion.common.ErrorCode;
import com.greenorange.promotion.exception.ThrowUtils;
import com.greenorange.promotion.model.entity.UserInfo;
import com.greenorange.promotion.model.enums.UserRoleEnum;
import com.greenorange.promotion.service.userInfo.UserInfoService;
import com.greenorange.promotion.utils.JWTUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


/**
 * 权限校验AOP
 */

@Slf4j
@Aspect
@Component
public class PermissionCheck {


    @Resource
    private UserInfoService userInfoService;


    @Resource
    private JWTUtils jwtUtils;

 
    /***
     * 执行拦截
     **/
    @Around("@annotation(requiresPermission)")
    public Object check(ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable {
        // 获取请求对象
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        // 接口的权限
        String mustRole = requiresPermission.mustRole();
        // 获取接口权限的枚举类
        UserRoleEnum interfaceRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
        ThrowUtils.throwIf(interfaceRoleEnum == null, ErrorCode.NO_AUTH_ERROR);
        // 获取用户权限
        String token = request.getHeader("Authorization");
        ThrowUtils.throwIf(StringUtils.isBlank(token), ErrorCode.NO_AUTH_ERROR, "JWT为空");
        // 解析token
        DecodedJWT decodedJWT = jwtUtils.verify(token);
        String userAccount = decodedJWT.getClaim("userAccount").asString();
        String userPassword = decodedJWT.getClaim("userPassword").asString();
        String userRole = decodedJWT.getClaim("userRole").asString();
        // 查询用户信息
        LambdaQueryWrapper<UserInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(UserInfo::getUserAccount, userAccount).eq(UserInfo::getUserPassword, userPassword);
        // 如果是小程序用户, 就加上权限条件
        lambdaQueryWrapper.eq(StringUtils.isNotBlank(userRole), UserInfo::getUserRole, userRole);
        UserInfo userInfo = userInfoService.getOne(lambdaQueryWrapper);
        ThrowUtils.throwIf(userInfo == null, ErrorCode.OPERATION_ERROR, "用户不存在");
        // 将用户id存入request,方便后续在接口中使用
        request.setAttribute("userId", userInfo.getId());

        // 获取用户权限的枚举类
        if (userRole == null) userRole = userInfo.getUserRole();
        UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(userRole);

        // 校验角色
        ThrowUtils.throwIf(UserRoleEnum.BAN.equals(userRoleEnum), ErrorCode.PARAMS_ERROR, "用户已被封禁");
        Map<UserRoleEnum, Integer> userRoleMap = new HashMap<>();
        userRoleMap.put(UserRoleEnum.USER, 1);
        userRoleMap.put(UserRoleEnum.STAFF, 2);
        userRoleMap.put(UserRoleEnum.SUPERVISOR, 3);
        userRoleMap.put(UserRoleEnum.MANAGER, 4);
        userRoleMap.put(UserRoleEnum.ADMIN, 5);
        userRoleMap.put(UserRoleEnum.BOSS, 6);

        Integer userRoleNumber = userRoleMap.get(userRoleEnum);
        Integer interfaceRoleNumber = userRoleMap.get(interfaceRoleEnum);
        if (userRoleNumber == 1) {
            ThrowUtils.throwIf(interfaceRoleNumber > 1, ErrorCode.NO_AUTH_ERROR);
        } else if (userRoleNumber == 2) {
            ThrowUtils.throwIf(interfaceRoleNumber > 2, ErrorCode.NO_AUTH_ERROR);
        } else if (userRoleNumber == 3) {
            ThrowUtils.throwIf(interfaceRoleNumber > 3, ErrorCode.NO_AUTH_ERROR);
        } else if (userRoleNumber == 4) {
            ThrowUtils.throwIf(interfaceRoleNumber > 4, ErrorCode.NO_AUTH_ERROR);
        } else if (userRoleNumber == 5) {
            ThrowUtils.throwIf(interfaceRoleNumber != 5, ErrorCode.NO_AUTH_ERROR);
        } else {
            ThrowUtils.throwIf(interfaceRoleNumber < 5, ErrorCode.NO_AUTH_ERROR);
        }

        return joinPoint.proceed();
    }
 
}

 4.编写登录接口,生成token

/**
 * 小程序端用户密码登录
 * @param userInfoMiniPasswordLoginRequest 小程序用户密码登录请求体
 * @return token
 */
@PostMapping("mini/pwd/login")
@Operation(summary = "小程序端用户密码登录", description = "参数:小程序用户密码登录请求体,权限:管理员(boss, admin),方法名:userInfoMiniLogin")
public BaseResponse<String> userInfoMiniLoginByPwd(@Valid @RequestBody UserInfoMiniPasswordLoginRequest userInfoMiniPasswordLoginRequest) {
    String token = userInfoService.userInfoMiniLoginByPwd(userInfoMiniPasswordLoginRequest);
    return ResultUtils.success(token);
}

具体业务层实现

/**
 * 小程序端用户密码登录
 */
@Override
public String userInfoMiniLoginByPwd(UserInfoMiniPasswordLoginRequest userInfoMiniPasswordLoginRequest) {
    String phoneNumber = userInfoMiniPasswordLoginRequest.getPhoneNumber();
    ThrowUtils.throwIf(RegexUtils.isPhoneInvalid(phoneNumber), ErrorCode.PARAMS_ERROR, "手机号格式无效");
    String userPassword = userInfoMiniPasswordLoginRequest.getUserPassword();
    String userRole = userInfoMiniPasswordLoginRequest.getUserRole();
    LambdaQueryWrapper<UserInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(UserInfo::getPhoneNumber, phoneNumber);
    lambdaQueryWrapper.eq(UserInfo::getUserRole, userRole);
    UserInfo userInfo = this.getOne(lambdaQueryWrapper);
    ThrowUtils.throwIf(userInfo == null, ErrorCode.OPERATION_ERROR, "手机号未注册");
    lambdaQueryWrapper.eq(UserInfo::getUserPassword, userPassword);
    userInfo = this.getOne(lambdaQueryWrapper);
    ThrowUtils.throwIf(userInfo == null, ErrorCode.OPERATION_ERROR, "密码不正确");

    Map<String, String> payload = new HashMap<>();
    payload.put("userAccount", phoneNumber);
    payload.put("userPassword", userPassword);
    payload.put("userRole", userInfo.getUserRole());
    return jwtUtils.generateToken(payload);
}

使用Knife4j接口文档进行测试

4.编写退出登录接口,对用户角色进行身份验证和鉴权

/**
 * 小程序端用户退出登录(用户退出时将 token 加入 Redis 黑名单)
 * @return 是否退出登录成功
 */
@GetMapping("mini/logout")
@Operation(summary = "小程序端用户退出登录", description = "参数:JWT,权限:管理员(boss, admin),方法名:userInfoMiniLogout")
@RequiresPermission(mustRole = UserConstant.DEFAULT_ROLE)
public BaseResponse<Boolean> userInfoMiniLogout(@RequestHeader("Authorization") String token) {
    // 获取token的过期时间
    DecodedJWT decodedJWT = jwtUtils.verify(token);
    long expirationTime = decodedJWT.getExpiresAt().getTime() - System.currentTimeMillis();
    // 将token存入Redis黑名单,并设置过期时间与token一致
    redisTemplate.opsForValue().set(token, token, expirationTime, TimeUnit.MILLISECONDS);
    return ResultUtils.success(true);
}

使用Knife4j接口文档进行测试

 
posted @ 2025-07-22 14:32  chenxinzhi  阅读(260)  评论(0)    收藏  举报