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接口文档进行测试


浙公网安备 33010602011771号