微服务项目->在线oj系统(Java-Spring)----6.0
创建token

在oj-common-security中引入依赖

创建jwt⼯具类:
package com.bite.common.security.utils; import com.bite.common.core.comtains.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是创建所使用的算法
使用例子:
首先创建一个token,这里由于是实例,所以密钥随便即可
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; //普通用户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

我们通过claim,secret来生成一个token.
然后我们创建一个变量key(其实就是一个常量+userKey);
然后将key存入redis中,value是用户身份
对login接口中Service部分进行修改

对oj-common-security项目进行修改

身份认证
引入依赖

填入白名单
创建一个白名单类,从nacos中去获取哪些是白名单中的url
@Configuration//配置类 @RefreshScope//实时刷新,使nacos上面的改变立刻生效 @ConfigurationProperties(prefix = "security.ignore") // public class IgnoreWhiteProperties { /** * 放⾏⽩名单配置,⽹关不校验此处的⽩名单 */ private List<String> whites = new ArrayList<>(); public List<String> getWhites() { //拿到nacos配置里面的白名单 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 { //实现GlobalFilter相当于实现了一个自定义过滤器 //实现Order是为什么以后有多个过滤器的时候进行先后排序 // 排除过滤的 uri ⽩名单地址,在nacos⾃⾏添加 @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); //如果是白名单里面的,则跳过身份认证 } //从http请求头中获取token String token = getToken(request); //判断token是否为空,如果为空说明以前未登录或者登录超市; if (StrUtil.isEmpty(token)) { return unauthorizedResponse(exchange, "令牌不能为空"); } Claims claims; try { claims = JwtUtils.parseToken(token, secret); //获取令牌中信息 解析payload中信息 if (claims == null) { return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); } } catch (Exception e) { return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); } String userKey = JwtUtils.getUserKey(claims); //获取jwt中的key boolean isLogin = redisService.hasKey(getTokenKey(userKey));//用得到的key去Redis中查找,如果找不到说明过期了 if (!isLogin) { return unauthorizedResponse(exchange, "登录状态已过期"); } String userid = JwtUtils.getUserId(claims); //判断jwt中的信息是否完整 if (StrUtil.isEmpty(userid)) { return unauthorizedResponse(exchange, "令牌验证失败"); } //c端用户只能请求C端功能,B端用户只能请求管理端功能 LoginUser user = redisService.getCacheObject(getTokenKey(userKey), LoginUser.class);//通过key去Redis里面查找value(存着用户是管理员还是用户) 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); } /** * 查找指定url是否匹配指定匹配规则链表中的任意⼀个字符串 * * @param url 指定url * @param patternList 需要检查的匹配规则链表 * @return 是否匹配 */ 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; } /** * 判断url是否与规则匹配 * 匹配规则中: * ? 表⽰单个字符; * * 表⽰⼀层路径内的任意字符串,不可跨层级; * ** 表⽰任意层路径; * * @param pattern 匹配规则 * @param url 需要匹配的url * @return 是否匹配 */ private boolean isMatch(String pattern, String url) { AntPathMatcher matcher = new AntPathMatcher(); //将pattern作为匹配规则 return matcher.match(pattern, url); } /** * 获取缓存key */ private String getTokenKey(String token) { return CacheConstants.LOGIN_TOKEN_KEY + token; } /** * 从请求头中获取请求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()); } //拼装webflux模型响应 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只对springweb起作用,而springcloud gataway属于webflux


RedisConfig修改需要增加@AutoConfigureBefore(RedisAutoConfiguration.class)注解否则会报错如下:

让spring优先注册我们这个RedisTemplate
配置secret

配置redis

然后我们测试一下:


