Spring Gateway 微服务网关 统一JWT鉴权 开发实例
简介
基于spring cloud gateway,微服务网关,注册中心使用nacos, 具备微服务动态路由,jwt token鉴权功能CheckTokenFilter, 路径白名单配置。
架构图
网关作为所有服务的请求入口,鉴权token 用的jwt , 好处是jwt本身携带用户身份信息,网关校验通过后,可以直接把用户信息发在请求头传递给上游服务,不要额外存储。
交互时序图
实现
设计介绍完了就,开始实现
- 创建工程pom文件添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
- 实现
GlobalFilter
接口, jwt 鉴权的主要逻辑
@Component
@RequiredArgsConstructor
public class CheckTokenFilter implements GlobalFilter, Ordered {
public static final String AUTHHEADER = "authorization";
public static final String USER_ID_KEY = "userId";
public static final String USER_NAME_KEY = "userName";
public static final String TRACE_ID = "traceId";
static final String BODY_401 = " {\n" +
" \"code\": 401,\n" +
" \"message\": \"Unauthorized\"\n" +
"}";
static final String BODY_403 = " {\n" +
" \"code\": 403,\n" +
" \"message\": \"token expired\"\n" +
"}";
private final CheckTokenUtil checkTokenUtil;
private final TokenParse tokenParse;
//布隆过滤器,对校验过的token过滤避免重复校验,提高性能
private final CircleBloomFilter passedCircleBloomFilter;
private final CircleBloomFilter stopedCircleBloomFilter;
private final MyFilterConfiguration myFilterConfiguration;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (request.getMethod() == HttpMethod.OPTIONS){
return chain.filter(exchange);
}
//请求路径白名单 判断
if (checkWhitePath(request.getPath().value())){
return chain.filter(exchange);
}
//token
String token = request.getHeaders().getFirst(AUTHHEADER);
if(token == null){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(response, request, BODY_401);
}
if (stopedCircleBloomFilter.exists(token)){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(response, request, BODY_401);
}
Claims claims = null;
if (passedCircleBloomFilter.exists(token)){
claims = tokenParse.parseToken(token);
setHeaders(claims, request.mutate());
}else {
try {
//jwt token 校验
claims = checkTokenUtil.check(token);
passedCircleBloomFilter.put(token);
//重写请求头,带上 userId
setHeaders(claims, request.mutate());
} catch (ExpiredJwtException e) {
stopedCircleBloomFilter.put(token);
response.setStatusCode(HttpStatus.FORBIDDEN);
return getVoidMono(response, request, BODY_403);
} catch (Exception e){
stopedCircleBloomFilter.put(token);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return getVoidMono(response, request, BODY_401);
}
}
return chain.filter(exchange);
}
private Mono<Void> getVoidMono(ServerHttpResponse serverHttpResponse, ServerHttpRequest httpRequest, String body) {
HttpHeaders headers = serverHttpResponse.getHeaders();
headers.add("Content-Type", "application/json;charset=UTF-8");
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(body.getBytes());
return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
@Override
public int getOrder() {
return -100;
}
private boolean checkWhitePath(String reqPath){
AntPathMatcher pathMatcher = new AntPathMatcher();
for (String white : myFilterConfiguration.getWhiteList()) {
if (pathMatcher.match(white, reqPath)) {
return true;
}
}
return false;
}
private void setHeaders(Claims claims, ServerHttpRequest.Builder builder){
String traceId = UUID.randomUUID().toString();
builder.header(TRACE_ID, traceId);
builder.header(USER_ID_KEY, claims.get("uid").toString());
try {
builder.header(USER_NAME_KEY, URLEncoder.encode(claims.getSubject(), "utf-8"));
} catch (UnsupportedEncodingException e) {
builder.header(USER_NAME_KEY, claims.getSubject());
}
builder.header(Claims.AUDIENCE, claims.getAudience());
Object eid = claims.get("eid");
builder.header("employeeId", eid != null ? String.valueOf(eid) : null);
Object admin = claims.get("admin");
builder.header("admin", admin != null ? String.valueOf(admin) : null);
}
}
3.网关配置
spring:
cloud:
nacos:
discovery:
#nacos 地址
server-addr: 127.0.0.1:8848
namespace:
ip:
gateway:
default-filters:
- DedupeResponseHeader=Vary Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_UNIQUE
- DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_FIRST
- RemoveRequestHeader=Authorization
discovery:
locator:
enabled: true
globalcors:
# 跨域配置
cors-configurations:
'[/**]':
maxAge: 3600
allowedOrigins: "*"
allowedHeaders: "Origin, X-Requested-With, Content-Type, Accept, Authorization, *"
allowCredentials: true
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
httpclient:
pool:
max-connections: 2000
connect-timeout: 3000
response-timeout: 15000
routes:
- id: test
uri: https://httpbin.org
predicates:
- Path=/test/{segment}
filters:
- SetPath=/{segment}
application:
name: gateway
server:
port: 9000
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
工程源码: springGateway