在开发集群式或分布式服务时,鉴权是最重要的一步,为了方便对请求统一鉴权,一般都是会放在网关中进行处理。目前非常流行的一种方案是使用JWT,详细的使用说明,可以找相关的资料查阅,这里先不进行深入的引用了。主要使用它下面的特性:
- 它的数据使用JSON格式封装。所以JWT是可以在不同的开发语音中传递。
- 在payload可以加载部分业务数据,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
- 它不需要在服务端保存会话信息, 减少了内存占用,也不需要落地存储,提升了检查效率。
- JWT 使用的密钥都是在服务器端,不会暴露到客户端,所以是安全的。
具体的流程如下:
- 用户先访问登陆授权服务器,授权验证通过之后,返回给客户端授权服务器生成的JWT Token字符串
- 客户端再访问后面的接口时,将授权服务器返回的JWT Token添加到header中
- 服务器网关收到客户端请求时,检测JWT Token是否合法,如果不合法,拒绝访问,返回错误。
需要处理的另一个问题是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
实现方式文章来源:https://www.toymoban.com/news/detail-659582.html
- 在项目pom.xml中添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 创建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<String, Object> 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> T checkTokenAndGet(String token, byte[] key, Class<T> clazz) throws JwtTokenExpiredException, JwtTokenErrorException {
try {
Jws<Claims> 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不合法");
}
}
}
- 在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文章来源地址https://www.toymoban.com/news/detail-659582.html
到了这里,关于Spring Gateway使用JWT实现统一身份认证的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!