提交 | 用户 | 时间
|
e7c126
|
1 |
package com.iailab.gateway.filter.security; |
H |
2 |
|
|
3 |
import cn.hutool.core.util.StrUtil; |
|
4 |
import com.iailab.framework.common.core.KeyValue; |
|
5 |
import com.iailab.framework.common.pojo.CommonResult; |
325d2f
|
6 |
import com.iailab.framework.common.util.date.LocalDateTimeUtils; |
e7c126
|
7 |
import com.iailab.framework.common.util.json.JsonUtils; |
H |
8 |
import com.iailab.gateway.util.SecurityFrameworkUtils; |
|
9 |
import com.iailab.gateway.util.WebFrameworkUtils; |
|
10 |
import com.iailab.module.system.api.oauth2.OAuth2TokenApi; |
|
11 |
import com.iailab.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; |
|
12 |
import com.fasterxml.jackson.core.type.TypeReference; |
|
13 |
import com.google.common.cache.CacheLoader; |
|
14 |
import com.google.common.cache.LoadingCache; |
|
15 |
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction; |
|
16 |
import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
|
17 |
import org.springframework.cloud.gateway.filter.GlobalFilter; |
|
18 |
import org.springframework.core.Ordered; |
|
19 |
import org.springframework.http.HttpStatus; |
|
20 |
import org.springframework.stereotype.Component; |
|
21 |
import org.springframework.web.reactive.function.client.WebClient; |
|
22 |
import org.springframework.web.server.ServerWebExchange; |
|
23 |
import reactor.core.publisher.Mono; |
|
24 |
|
|
25 |
import java.time.Duration; |
|
26 |
import java.util.Objects; |
|
27 |
import java.util.function.Function; |
|
28 |
|
|
29 |
import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; |
|
30 |
|
|
31 |
/** |
|
32 |
* Token 过滤器,验证 token 的有效性 |
|
33 |
* 1. 验证通过时,将 userId、userType、tenantId 通过 Header 转发给服务 |
|
34 |
* 2. 验证不通过,还是会转发给服务。因为,接口是否需要登录的校验,还是交给服务自身处理 |
|
35 |
* |
|
36 |
* @author iailab |
|
37 |
*/ |
|
38 |
@Component |
|
39 |
public class TokenAuthenticationFilter implements GlobalFilter, Ordered { |
|
40 |
|
|
41 |
/** |
|
42 |
* CommonResult<OAuth2AccessTokenCheckRespDTO> 对应的 TypeReference 结果,用于解析 checkToken 的结果 |
|
43 |
*/ |
|
44 |
private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE |
|
45 |
= new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {}; |
|
46 |
|
|
47 |
/** |
|
48 |
* 空的 LoginUser 的结果 |
|
49 |
* |
|
50 |
* 用于解决如下问题: |
|
51 |
* 1. {@link #getLoginUser(ServerWebExchange, String)} 返回 Mono.empty() 时,会导致后续的 flatMap 无法进行处理的问题。 |
|
52 |
* 2. {@link #buildUser(String)} 时,如果 Token 已经过期,返回 LOGIN_USER_EMPTY 对象,避免缓存无法刷新 |
|
53 |
*/ |
|
54 |
private static final LoginUser LOGIN_USER_EMPTY = new LoginUser(); |
|
55 |
|
|
56 |
private final WebClient webClient; |
|
57 |
|
|
58 |
/** |
|
59 |
* 登录用户的本地缓存 |
|
60 |
* |
|
61 |
* key1:多租户的编号 |
|
62 |
* key2:访问令牌 |
|
63 |
*/ |
|
64 |
private final LoadingCache<KeyValue<Long, String>, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1), |
|
65 |
new CacheLoader<KeyValue<Long, String>, LoginUser>() { |
|
66 |
|
|
67 |
@Override |
|
68 |
public LoginUser load(KeyValue<Long, String> token) { |
|
69 |
String body = checkAccessToken(token.getKey(), token.getValue()).block(); |
|
70 |
return buildUser(body); |
|
71 |
} |
|
72 |
|
|
73 |
}); |
|
74 |
|
|
75 |
public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) { |
|
76 |
// Q:为什么不使用 OAuth2TokenApi 进行调用? |
|
77 |
// A1:Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support |
|
78 |
// A2:校验 Token 的 API 需要使用到 header[tenant-id] 传递租户编号,暂时不想编写 RequestInterceptor 实现 |
|
79 |
// 因此,这里采用 WebClient,通过 lbFunction 实现负载均衡 |
|
80 |
this.webClient = WebClient.builder().filter(lbFunction).build(); |
|
81 |
} |
|
82 |
|
|
83 |
@Override |
|
84 |
public Mono<Void> filter(final ServerWebExchange exchange, GatewayFilterChain chain) { |
|
85 |
// 移除 login-user 的请求头,避免伪造模拟 |
|
86 |
SecurityFrameworkUtils.removeLoginUser(exchange); |
|
87 |
|
|
88 |
// 情况一,如果没有 Token 令牌,则直接继续 filter |
|
89 |
String token = SecurityFrameworkUtils.obtainAuthorization(exchange); |
|
90 |
if (StrUtil.isEmpty(token)) { |
|
91 |
return chain.filter(exchange); |
|
92 |
} |
|
93 |
|
|
94 |
// 情况二,如果有 Token 令牌,则解析对应 userId、userType、tenantId 等字段,并通过 通过 Header 转发给服务 |
|
95 |
// 重要说明:defaultIfEmpty 作用,保证 Mono.empty() 情况,可以继续执行 `flatMap 的 chain.filter(exchange)` 逻辑,避免返回给前端空的 Response!! |
|
96 |
return getLoginUser(exchange, token).defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> { |
|
97 |
// 1. 无用户,直接 filter 继续请求 |
325d2f
|
98 |
if (user == LOGIN_USER_EMPTY || // 下面 expiresTime 的判断,为了解决 token 实际已经过期的情况 |
H |
99 |
user.getExpiresTime() == null || LocalDateTimeUtils.afterNow(user.getExpiresTime())) { |
e7c126
|
100 |
return chain.filter(exchange); |
H |
101 |
} |
|
102 |
|
|
103 |
// 2.1 有用户,则设置登录用户 |
|
104 |
SecurityFrameworkUtils.setLoginUser(exchange, user); |
|
105 |
// 2.2 将 user 并设置到 login-user 的请求头,使用 json 存储值 |
|
106 |
ServerWebExchange newExchange = exchange.mutate() |
|
107 |
.request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user)).build(); |
|
108 |
return chain.filter(newExchange); |
|
109 |
}); |
|
110 |
} |
|
111 |
|
|
112 |
private Mono<LoginUser> getLoginUser(ServerWebExchange exchange, String token) { |
|
113 |
// 从缓存中,获取 LoginUser |
|
114 |
Long tenantId = WebFrameworkUtils.getTenantId(exchange); |
|
115 |
KeyValue<Long, String> cacheKey = new KeyValue<Long, String>().setKey(tenantId).setValue(token); |
|
116 |
LoginUser localUser = loginUserCache.getIfPresent(cacheKey); |
|
117 |
if (localUser != null) { |
|
118 |
return Mono.just(localUser); |
|
119 |
} |
|
120 |
|
|
121 |
// 缓存不存在,则请求远程服务 |
|
122 |
return checkAccessToken(tenantId, token).flatMap((Function<String, Mono<LoginUser>>) body -> { |
|
123 |
LoginUser remoteUser = buildUser(body); |
|
124 |
if (remoteUser != null) { |
|
125 |
// 非空,则进行缓存 |
|
126 |
loginUserCache.put(cacheKey, remoteUser); |
|
127 |
return Mono.just(remoteUser); |
|
128 |
} |
|
129 |
return Mono.empty(); |
|
130 |
}); |
|
131 |
} |
|
132 |
|
|
133 |
private Mono<String> checkAccessToken(Long tenantId, String token) { |
|
134 |
return webClient.get() |
|
135 |
.uri(OAuth2TokenApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build()) |
|
136 |
.headers(httpHeaders -> WebFrameworkUtils.setTenantIdHeader(tenantId, httpHeaders)) // 设置租户的 Header |
|
137 |
.retrieve().bodyToMono(String.class); |
|
138 |
} |
|
139 |
|
|
140 |
private LoginUser buildUser(String body) { |
|
141 |
// 处理结果,结果不正确 |
|
142 |
CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE); |
|
143 |
if (result == null) { |
|
144 |
return null; |
|
145 |
} |
|
146 |
if (result.isError()) { |
|
147 |
// 特殊情况:令牌已经过期(code = 401),需要返回 LOGIN_USER_EMPTY,避免 Token 一直因为缓存,被误判为有效 |
|
148 |
if (Objects.equals(result.getCode(), HttpStatus.UNAUTHORIZED.value())) { |
|
149 |
return LOGIN_USER_EMPTY; |
|
150 |
} |
|
151 |
return null; |
|
152 |
} |
|
153 |
|
|
154 |
// 创建登录用户 |
|
155 |
OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData(); |
|
156 |
return new LoginUser().setId(tokenInfo.getUserId()).setUserType(tokenInfo.getUserType()) |
|
157 |
.setInfo(tokenInfo.getUserInfo()) // 额外的用户信息 |
325d2f
|
158 |
.setTenantId(tokenInfo.getTenantId()).setScopes(tokenInfo.getScopes()) |
H |
159 |
.setExpiresTime(tokenInfo.getExpiresTime()); |
e7c126
|
160 |
} |
H |
161 |
|
|
162 |
@Override |
|
163 |
public int getOrder() { |
|
164 |
return -100; // 和 Spring Security Filter 的顺序对齐 |
|
165 |
} |
|
166 |
|
|
167 |
} |