跳到主要内容微服务项目在线 OJ 系统:JWT 令牌与网关身份认证实现 | 极客日志Javajava
微服务项目在线 OJ 系统:JWT 令牌与网关身份认证实现
介绍微服务项目中在线 OJ 系统的身份认证实现方案。核心步骤包括构建 JWT 工具类生成令牌,利用 Redis 存储用户会话信息,通过 Nacos 动态配置安全白名单,并在网关层实现自定义过滤器进行请求鉴权。方案强调密钥不应硬编码,建议通过配置文件管理,同时处理了 WebFlux 环境下的异常响应逻辑,确保系统安全性与灵活性。
BigDataPan1 浏览 创建 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 {
public static String createToken(Map<String, Object> claims, String secret) {
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
return token;
}
public static Claims parseToken {
Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
(String token, String secret)
return
createToken:根据传入的 claims 和 secret 创建一个 token。
注意:secret 需要保密随机,不能硬编码,建议通过 Nacos 配置管理,并定期更换。
硬编码(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 后通过 JwtUtil 的 parseToken 进行解码(需要 token 和 secret)。
public static void main(String[] args) {
String token="eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyaWQiOjEyMzQ1Nn0.F0jIBBpUgyhUPZSSrKVHOBBatVhFEIRSWh6LRjPXC5kdJ3TsFOzByhBryx_nV9nLlFpeE-4ZvOESQMWKn9nDjw";
Claims claims=parseToken("eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyaWQiOjEyMzQ1Nn0.F0jIBBpUgyhUPZSSrKVHOBBatVhFEIRSWh6LRjPXC5kdJ3TsFOzByhBryx_nV9nLlFpeE-4ZvOESQMWKn9nDjw","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;
}
引入依赖
<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();
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。
- 通过 claim 和 secret 生成 token。
- 创建变量 key(常量 + userKey),将 key 存入 Redis,value 是用户身份。
对 login 接口中 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;
}
}
自定义过滤器
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 修改说明:需增加 @AutoConfigureBefore(RedisAutoConfiguration.class) 注解以确保 RedisTemplate 优先注册。
配置 secret
配置 redis