创建 Token
在 oj-common-security 中引入依赖

创建 JWT 工具类
package com.bite.common.security.utils;
import com.bite.common.core.contains.JwtContains;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.HashMap;
import java.util.Map;
public class JwtUtils {
/**
* 生成令牌
*
* @param claims 数据
* @param secret 密钥
* @return 令牌
*/
public static String createToken(Map<String, Object> claims, String secret) {
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
return token;
}
/**
* 从令牌中获取数据
*
* @param token 令牌
* @param secret 密钥
* @return 数据
*/
public static Claims parseToken(String token, String secret) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
}
createToken: 根据传入的 claims 和 secret 创建一个 token。
注意:这里的 secret 需要保密随机,不能硬编码,建议定期更换。可采用 Nacos 配置管理 secret。
硬编码(Hard Coding)是指在程序代码中直接嵌入固定数值、字符串或逻辑,而非通过变量、配置文件或外部输入来动态获取的编程方式。这种做法会导致代码灵活性降低,维护成本增加。
SignatureAlgorithm.HS512 是创建时使用的算法。
使用示例:
public static void main(String[] args) {
Map<String,Object> claim=new HashMap<>();
claim.put("userid",123456);
System.out.println(createToken(claim,"ddwdwdwdw5dfw5"));
}
获取到 token 后通过 JwtUtils.parseToken 进行解码(需要 token 和 secret)。
public static void main(String[] args) {
String token="eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyaWQiOjEyMzQ1Nn0.F0jIBBpUgyhUPZSSrKVHOBBatVhFEIRSWh6LRjPXC5kdJ3TsFOzByhBryx_nV9nLlFpeE-4ZvOESQMWKn9nDjw";
Claims claims=parseToken(token,"ddwdwdwdw5dfw5");
System.out.println(claims);
}
在 OJ-modules 中引入 oj-common-security

定义常量类
public class CacheConstants {
public final static String LOGIN_TOKEN_KEY = "login_tokens:";
public final static long EXPIRATION = 720;
}
public class JwtContains {
public static final String LOGIN_USER_ID="userId";
public static final String LOGIN_USER_KEY="userKey";
}
定义登录用户
@Data
public class LoginUser {
private Integer identity; //普通用户 1 管理员 2
}
引入依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool-all.version}</version>
</dependency>
<dependency>
<groupId>com.bite</groupId>
<artifactId>oj-common-redis</artifactId>
<version>${oj-common-redis.version}</version>
<scope>compile</scope>
</dependency>
定义 TokenService 类
@Service
public class TokenService {
@Autowired
private RedisService redisService;
/**
* 创建令牌
*/
public String createToken(Long userId,String secret,Integer identity) {
String userKey = UUID.fastUUID().toString();
// Jwt 存储信息
Map<String, Object> claim = new HashMap<String, Object>();
claim.put(JwtContains.LOGIN_USER_ID,userId);
claim.put(JwtContains.LOGIN_USER_KEY,userKey);
String token=JwtUtils.createToken(claim,secret);
String key=CacheConstants.LOGIN_TOKEN_KEY+userKey;
LoginUser loginUser=new LoginUser();
loginUser.setIdentity(UserIdentity.ADMIN.getCode());
redisService.setCacheObject(key, loginUser, CacheConstants.EXPIRATION, TimeUnit.MINUTES);
return token;
}
}
核心逻辑:
- 通过 UUID 获取唯一的 userKey。
- 创建 claim,存放用户的 userId 及 userKey。
- 调用方传入通过 Nacos 获取的密钥,传给
createToken。 - 生成 token。
- 创建缓存 key(常量 + userKey),将用户身份存入 Redis。
修改登录接口 Service 部分

修改 oj-common-security 项目

身份认证
引入依赖

填入白名单
创建白名单类,从 Nacos 获取放行 URL。
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {
private List<String> whites = new ArrayList<>();
public List<String> getWhites() {
return whites;
}
public void setWhites(List<String> whites) {
this.whites = whites;
}
}
修改 Nacos 配置:

自定义过滤器
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.bite.common.core.comtains.CacheConstants;
import com.bite.common.core.comtains.HttpConstants;
import com.bite.common.core.domain.R;
import com.bite.common.core.enums.ResultCode;
import com.bite.common.core.enums.UserIdentity;
import com.bite.common.redis.service.RedisService;
import com.bite.common.security.model.LoginUser;
import com.bite.common.security.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Value("${jwt.secret}")
private String secret;
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
if (matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
String token = getToken(request);
if (StrUtil.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims;
try {
claims = JwtUtils.parseToken(token, secret);
if (claims == null) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
} catch (Exception e) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userKey = JwtUtils.getUserKey(claims);
boolean isLogin = redisService.hasKey(getTokenKey(userKey));
if (!isLogin) {
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
if (StrUtil.isEmpty(userid)) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
LoginUser user = redisService.getCacheObject(getTokenKey(userKey), LoginUser.class);
if (url.contains(HttpConstants.SYSTEM_URL_PREFIX) && !UserIdentity.ADMIN.getCode().equals(user.getIdentity())) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
if (url.contains(HttpConstants.FRIEND_URL_PREFIX) && !UserIdentity.ORDINARY.getCode().equals(user.getIdentity())) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
return chain.filter(exchange);
}
private boolean matches(String url, List<String> patternList) {
if (StrUtil.isEmpty(url) || CollectionUtils.isEmpty(patternList)) {
return false;
}
for (String pattern : patternList) {
if (isMatch(pattern, url)) {
return true;
}
}
return false;
}
private boolean isMatch(String pattern, String url) {
AntPathMatcher matcher = new AntPathMatcher();
return matcher.match(pattern, url);
}
private String getTokenKey(String token) {
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
private String getToken(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(HttpConstants.AUTHENTICATION);
if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {
token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
}
return token;
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
log.error("[鉴权异常处理] 请求路径:{}", exchange.getRequest().getPath());
return webFluxResponseWriter(exchange.getResponse(), msg, ResultCode.FAILED_UNAUTHORIZED.getCode());
}
private Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String msg, int code) {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
R<?> result = R.fail(code, msg);
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
@Override
public int getOrder() {
return -200;
}
}
异常处理说明
此处未抛出异常而是直接返回响应,是因为 @RestControllerAdvice 仅对 Spring Web 有效,而 Spring Cloud Gateway 属于 WebFlux 环境。


RedisConfig 配置
RedisConfig 修改需增加 @AutoConfigureBefore(RedisAutoConfiguration.class) 注解,否则报错。目的是让 Spring 优先注册自定义的 RedisTemplate。

配置 Secret

配置 Redis

测试验证





