在开发集群式或分布式服务时,鉴权是最重要的一步,为了方便对请求统一鉴权,一般都是会放在网关中进行处理。目前非常流行的一种方案是使用JWT,详细的使用说明,可以找相关的资料查阅,这里先不进行深入的引用了。主要使用它下面的特性:
具体的流程如下:
需要处理的另一个问题是JWT Token 失败的问题,比如用户修改了密码,原来的JWT Token就不能再被使用了,一般是做法是添加JWT Token的黑名单,直到JWT Token失败。毕竟触发某些事件让JWT Token失效还是低概率事件。
做法如下:
实现方式
io.jsonwebtoken
jjwt
0.9.1
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 {
Jws headerClaimsJwt = Jwts.parser().requireSubject(JWT_SUBJECT).setSigningKey(key).parseClaimsJws(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不合法");
}
}
}
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;
}
}