提交 | 用户 | 时间
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 }