Spring Gateway使用JWT实现统一身份认证

在开发集群式或分布式服务时,鉴权是最重要的一步,为了方便对请求统一鉴权,一般都是会放在网关中进行处理。目前非常流行的一种方案是使用JWT,详细的使用说明,可以找相关的资料查阅,这里先不进行深入的引用了。主要使用它下面的特性:

  • 它的数据使用JSON格式封装。所以JWT是可以在不同的开发语音中传递。
  • 在payload可以加载部分业务数据,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
  • 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
  • 它不需要在服务端保存会话信息, 减少了内存占用,也不需要落地存储,提升了检查效率。
  • JWT 使用的密钥都是在服务器端,不会暴露到客户端,所以是安全的。

具体的流程如下:

  1. 用户先访问登陆授权服务器,授权验证通过之后,返回给客户端授权服务器生成的JWT Token字符串
  2. 客户端再访问后面的接口时,将授权服务器返回的JWT Token添加到header中
  3. 服务器网关收到客户端请求时,检测JWT Token是否合法,如果不合法,拒绝访问,返回错误。

Spring Gateway使用JWT实现统一身份认证_第1张图片
需要处理的另一个问题是JWT Token 失败的问题,比如用户修改了密码,原来的JWT Token就不能再被使用了,一般是做法是添加JWT Token的黑名单,直到JWT Token失败。毕竟触发某些事件让JWT Token失效还是低概率事件。
做法如下:

  • 当JWT Token失效事件发生时,将原来的JWT TOKEN 加入的黑名单中,黑名单,可以存到Redis或数据库中。
  • 为了提升处理效率,网关服务定时从授权服务刷新黑名单到网关服务内存中,这样检测JWT Token是否在黑名单中效率比较高
  • 在黑名单中的JWT Token 过期后,自动从黑名单中删除,防止黑名单数量堆积。
  • 为了防止用户JWT Token扩展,用户登陆之后检测,如果已存在JWT Token 且过期时间大于1天,就返回旧的JWT Token,否则自动延期,返回新的JWT Token

实现方式

  • 在项目pom.xml中添加依赖
 		
            io.jsonwebtoken
            jjwt
            0.9.1
        

  • 创建JWT Token的管理类
package com.xinyue.game.jwt;

import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import com.alibaba.fastjson.JSON;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
 * @author 王广帅
 * @since 2023/5/23 22:27
 **/
public class GameJwtService {

    private final static String JWT_SUBJECT = "game_token";
    private final static String TOKEN_EXTRA_KEY = "token_extra_key";

    /**
     * 创建一个Jwt token
     *
     * @param data 需要携带的业务数据,这里不要放置敏感信息,因为它是明文传输的
     * @param key  HS512的签名密钥
     * @return
     */
    public String createJwtToken(Object data, Duration expire, byte[] key) {
        Date expireDate = new Date(System.currentTimeMillis() + expire.toMillis());
        JwtBuilder jwtBuilder = Jwts.builder().setSubject(JWT_SUBJECT);
        if (data != null) {
            Map claims = new HashMap<>();
            claims.put(TOKEN_EXTRA_KEY, JSON.toJSONString(data));
            jwtBuilder.addClaims(claims);
        }
        String token = jwtBuilder.setExpiration(expireDate).compressWith(CompressionCodecs.DEFLATE).signWith(SignatureAlgorithm.HS512, key).compact();
        return token;
    }

    /**
     * 检查 jwt token并获取token携带的业务数据
     *
     * @param token
     * @param key
     * @return
     * @throws JwtTokenExpiredException
     * @throws JwtTokenErrorException
     */
    public  T checkTokenAndGet(String token, byte[] key, Class clazz) throws JwtTokenExpiredException, JwtTokenErrorException {
        try {
            Jwt headerClaimsJwt = Jwts.parser().requireSubject(JWT_SUBJECT).setSigningKey(key).parseClaimsJwt(token);
            Claims body = headerClaimsJwt.getBody();
            String value = (String) body.get(TOKEN_EXTRA_KEY);
            return JSON.parseObject(value, clazz);
        } catch (ExpiredJwtException e) {
            throw new JwtTokenExpiredException("token 已过期");
        } catch (Throwable e) {
            throw new JwtTokenErrorException("token不合法");
        }
    }
}

  • 在Spring Cloud Gateway中添加全局过滤器
    使用全局过滤器,我们检测所有的请求是否合法,这里需要一个配置,因为有些请求是不需要检测token的,比如登陆和注册等,
package com.xinyue.game.web.gateway.access;

import java.nio.charset.StandardCharsets;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;

import com.alibaba.fastjson.JSONObject;
import com.xinyue.game.jwt.GameJwtService;
import com.xinyue.game.jwt.GameUserToken;
import com.xinyue.game.jwt.JwtTokenErrorException;
import com.xinyue.game.jwt.JwtTokenExpiredException;
import com.xinyue.game.web.gateway.common.XinYueWebGatewaySystemConfig;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.ByteBufFlux;

/**
 * 访问授权过滤器,如果访问的地址,不在忽略名单内,则必须经过授权检测才可以访问
 *
 * @author 王广帅
 * @since 2023/5/23 21:12
 **/
@Service
public class AccessAuthVerifyFilter implements GlobalFilter {

    private Logger logger = LoggerFactory.getLogger(AccessAuthVerifyFilter.class);
    @Autowired
    private XinYueWebGatewaySystemConfig webGatewaySystemConfig;


    private GameJwtService gameJwtService = new GameJwtService();


    private boolean isIgnoreCheckUri(String uri) {
        return webGatewaySystemConfig.getUriAuthIgnoreList().contains(uri);
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String userToken = exchange.getRequest().getHeaders().getFirst("Authorization");
        // 获取请求的路径
        String uri = exchange.getRequest().getPath().value();
        logger.debug("收到请求:{}", uri);
        if (ObjectUtils.isEmpty(userToken)) {
            if (isIgnoreCheckUri(uri)) {
                // 如果是不需要检测的请求,直接返回成功
                return chain.filter(exchange);
            }else {
                return this.responseError(exchange, 5001, "未登陆成功,请重新登陆之后再重试");
            }
        } else {
            byte[] key = webGatewaySystemConfig.getTokenAesKey().getBytes(StandardCharsets.UTF_8);
            try {
                GameUserToken gameRoleToken = gameJwtService.checkTokenAndGet(userToken, key, GameUserToken.class);
                if (gameRoleToken == null || gameRoleToken.getUserId() == null) {
                    return this.responseError(exchange, 5001, "登陆数据不正确,请重新登陆");
                }
            } catch (JwtTokenExpiredException e) {
                return this.responseError(exchange, 5002, "登陆已过期,请重新登陆");
            } catch (JwtTokenErrorException e) {
                return this.responseError(exchange, 5003, "非法登陆,请重新登陆");
            }
        }
        // 如果没有异常,继续往下传递
        return chain.filter(exchange);
    }

    /**
     * 统一响应错误提示
     *
     * @param exchange
     * @param code
     * @param msg
     * @return
     */
    private Mono<Void> responseError(ServerWebExchange exchange, int code, String msg) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        JSONObject data = new JSONObject();
        data.put("code", code);
        data.put("msg", msg);
        byte[] dataBytes = data.toJSONString().getBytes(StandardCharsets.UTF_8);
        Mono<Void> ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(dataBytes))));
        return ret;
    }
}

实现源码地址:https://gitee.com/wgslucky/xinyue-game-frame

你可能感兴趣的:(集群式游戏服务器开发记录,Spring,Boot,&,Spring,Cloud,游戏服务器,JWT,检测,Spring,gateway)