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

创建token

在oj-common-security中引入依赖

创建jwt⼯具类:

java 复制代码
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,这里由于是实例,所以密钥随便即可

java 复制代码
  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)

java 复制代码
    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

创建常量类

java 复制代码
public class CacheConstants {
    public final static String LOGIN_TOKEN_KEY = "login_tokens:";
    public final static long EXPIRATION = 720;
}
java 复制代码
public class JwtContains {
    public static final String LOGIN_USER_ID="userId";
    public static final String LOGIN_USER_KEY="userKey";
}

定义登录用户

java 复制代码
@Data
public class LoginUser {
    private Integer identity;
    //普通用户1 管理员2
}

引入依赖

java 复制代码
       <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类

java 复制代码
@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

java 复制代码
@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

自定义过滤器

java 复制代码
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

然后我们测试一下:

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔4 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说9 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json