1
zj
2024-06-13 a4662cc65a02f258062bf6cc392ceb1017db9292
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
package com.yami.trading.security.common.filter;
 
import java.io.IOException;
import java.net.URLDecoder;
import java.time.ZoneId;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import cn.hutool.core.util.CharsetUtil;
import com.yami.trading.bean.model.RiskClient;
import com.yami.trading.bean.model.User;
import com.yami.trading.common.exception.BusinessException;
import com.yami.trading.common.util.*;
import com.yami.trading.security.common.util.RiskClientUtil;
import com.yami.trading.service.user.RiskClientService;
import com.yami.trading.service.user.UserService;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
 
import com.yami.trading.common.constants.RedisKeys;
import com.yami.trading.common.exception.YamiShopBindException;
import com.yami.trading.common.handler.HttpHandler;
import com.yami.trading.security.common.adapter.AuthConfigAdapter;
import com.yami.trading.security.common.bo.UserInfoInTokenBO;
import com.yami.trading.security.common.enums.SysTypeEnum;
import com.yami.trading.security.common.manager.TokenStore;
import com.yami.trading.security.common.util.AuthUserContext;
import com.yami.trading.security.common.util.ItemRedisKeys;
import com.yami.trading.security.common.util.MD5;
 
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
 
/**
 * 授权过滤,只要实现AuthConfigAdapter接口,添加对应路径即可:
 *
 * @author 菠萝凤梨
 * @date 2022/3/25 17:33
 */
@Component
public class AuthFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
 
    @Value("${sign.encryption-key}")
    private String ENCRYPTION_KEY;
 
    @Value("${sign.version-number}")
    private String VERSION_NUMBER;
 
    @Autowired
    private TokenStore tokenStore;
 
    @Autowired
    private HttpHandler httpHandler;
 
    @Autowired
    private AuthConfigAdapter authConfigAdapter;
 
    @Autowired
    private RiskClientService riskClientService;
 
    @Autowired
    UserService userService;
 
    /**
     * 白名单URL
     */
    private static final HashSet<String> WHITE_URLS2 = new HashSet<String>();
 
    /**
     * 随机数与时间戳字典
     */
    private static final Map<String, Long> ADMINCACHEMAP = new ConcurrentHashMap<>();
 
    AntPathMatcher pathMatcher = new AntPathMatcher();
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        String requestUri = req.getRequestURI();
        String servletPath = req.getServletPath();
 
        boolean servletPathWhiteUri = false;
        if (WHITE_URLS2.contains(servletPath)) {
            servletPathWhiteUri = true;
        }
 
        // 无需识别 token 的白名单
        boolean ignoreTokenUri = false;
        List<String> excludePathPatterns = authConfigAdapter.excludePathPatterns();
        // 如果匹配不需要授权的路径,就不需要校验是否需要授权
        if (CollectionUtil.isNotEmpty(excludePathPatterns)) {
            for (String excludePathPattern : excludePathPatterns) {
                if (pathMatcher.match(excludePathPattern, requestUri)) {
                    ignoreTokenUri = true;
                }
            }
        }
 
        // 如果 uri 请求携带 token,则校验 token,如果未携带 token,则不校验 token
        boolean optionalTokenUri = false; // 其他接口,都务必需要携带 token,并且需要成功校验 token
        List<String> maybeAuthUris = authConfigAdapter.maybeAuthUri();
        if (CollectionUtil.isNotEmpty(maybeAuthUris)) {
            for (String maybe : maybeAuthUris) {
                if (pathMatcher.match(maybe, requestUri)) {
                    optionalTokenUri = true;
                    break;
                }
            }
        }
 
        String token = req.getHeader("token");
        String accessToken = req.getHeader("Authorization");
        if (StrUtil.isBlank(accessToken) || Objects.equals(accessToken, "null")) {
            accessToken = token;
        }
        if (StrUtil.isBlank(accessToken) || Objects.equals(accessToken, "null")) {
            accessToken = req.getParameter("token");
        }
        if (StrUtil.isBlank(accessToken) || Objects.equals(accessToken, "null")) {
            accessToken = "";
        }
        if (requestUri.contains("adminLogin")) {
            logger.info("---> requestUri:{}, accessToken:*{}*", requestUri, accessToken);
        }
 
        UserInfoInTokenBO userInfoInToken = null;
        String clientIp = IPHelper.getIpAddr();
        String userCode = "";
        // 解析token的异常对象
        YamiShopBindException tokenErr = null;
        if (StrUtil.isNotBlank(accessToken)) {
            try {
                userInfoInToken = tokenStore.getUserInfoByAccessToken(accessToken, true);
                if (userInfoInToken.getSysType().intValue() == SysTypeEnum.ORDINARY.value().intValue()) {
                    String userId = userInfoInToken.getUserId();
                    // 缓存优化 TODO
                    User userEntity = userService.cacheUserBy(userId);
                    if (userEntity != null) {
                        userCode = userEntity.getUserCode();
                    }
                }
            } catch (Exception e) {
                if (e instanceof YamiShopBindException) {
                    logger.error("---> AuthFilter doFilter 处理 uri:{}, accessToken:{} 报 YamiShopBindException 异常:{}", requestUri, accessToken, e.getMessage());
                    tokenErr = (YamiShopBindException)e;
                } else {
                    logger.error("---> AuthFilter doFilter 处理 uri:{}, accessToken:{} 报错:", requestUri, accessToken, e);
                    throw e;
                }
            }
        }
        // 处理黑名单访问,断网逻辑
        if (checkBlackRequest(req, resp, clientIp, userCode)) {
            return;
        }
 
        try {
            // 识别时区信息
            processTimezone(req);
 
            // 白名单
            if (servletPathWhiteUri) {
                chain.doFilter(req, resp);
                return;
            }
 
            //
            if (ObjectUtils.isNotEmpty(VERSION_NUMBER)) {
                // 验证时间戳签名
                if (checkSign(req,response)) {
                    return;
                }
            }
 
            // 当前 uri 不用检查是否携带 token,直接执行对应的接口
            if (ignoreTokenUri) {
                chain.doFilter(req, resp);
                return;
            }
 
            // 有 token 就用,没 token 也无所谓的 api
            if (userInfoInToken != null) {
                // 如果有 token,并且解析成功,则走以下处理逻辑
                if (userInfoInToken.getSysType().intValue() == SysTypeEnum.ADMIN.value().intValue()) {
                    if (!pathMatcher.match("/updateCheckIp", requestUri)) {
                        Object loginIP = RedisUtil.get(RedisKeys.ACCESS_IP + userInfoInToken.getUserId());
                        if (null != loginIP && !IPHelper.equalIpSegment(loginIP.toString(), clientIp)) {
                            logger.error("The Login IP Is Inconsistent With The Operation IP! Login-IP:{} Access-IP:{} Servlet-Path:{}", loginIP, clientIp, servletPath);
                            httpHandler.printServerResponseToWeb("", 1001);
                            return;
                        }
                    }
                }
 
                // 保存上下文
                AuthUserContext.set(userInfoInToken);
            } else if (!optionalTokenUri) {
                // token 必填的路径
                // 如果没有 token,或者 token 解析失败/过期,但是当前请求 uri 又不是一个可选 token 的uri,则报错,提示 token 无效
                // 返回前端401
                logger.error("---> requestUri:{} 未配置 optional 白名单", requestUri);
                httpHandler.printServerResponseToWeb("您的账号已过期或已经在其他地方登录,请重新登录", 403);
                return;
            }
            if (tokenErr != null) {
                // 前面解析 token 报错,此处抛出
                throw tokenErr;
            }
 
            // token 逻辑校验顺利
            chain.doFilter(req, resp);
        } catch (Exception e) {
            // 手动捕获下非controller异常
            if (e instanceof YamiShopBindException) {
                httpHandler.printServerResponseToWeb("您的账号已经在其他地方登录", 403);
                return;
            } else {
                logger.error("---> AuthFilter requestUri:{}, accessToken:{}, 请求处理报错: ", requestUri, accessToken, e);
                throw e;
            }
        } finally {
            AuthUserContext.clean();
            TimeZoneContext.clientTimeZoneId.remove();
            TimeZoneContext.showTimeZoneId.remove();
        }
    }
 
    /**
     * 普通请求处理处理
     * @throws IOException
     */
    public boolean checkSign(HttpServletRequest request, ServletResponse response) throws IOException {
        String servletPath2 = request.getServletPath();
        // 响应请求前参数校验
        // 获取请求头中的时间戳参数
        String timestamp = request.getHeader("tissuePaper");
        String systemRandom = request.getHeader("systemRandom");
        String waitSign = ENCRYPTION_KEY+timestamp;
 
        if (timestamp == null) {
            // 没有时间戳参数返回验签失败
            logger.error("---> AuthFilter checkSign 时间戳为空:{} ", servletPath2);
            ((HttpServletResponse)response).sendError(201, "时间戳为空");
            return true;
        }
 
        long currDate = System.currentTimeMillis()/1000L;
        long oldDate = currDate - 60;
        if (ADMINCACHEMAP.size() > 500) {
            for (Map.Entry<String, Long> entry : ADMINCACHEMAP.entrySet()) {
                if (entry.getValue().longValue() < oldDate) {
                    ADMINCACHEMAP.remove(entry.getKey());
                }
            }
        }
        if (ObjectUtils.isEmpty(systemRandom)) {
            // 没有时间戳参数返回验签失败
            logger.error("---> AuthFilter checkSign 时间戳和随机数为空:{}", servletPath2);
            ((HttpServletResponse)response).sendError(207, "时间戳和随机数为空");
            return true;
        }
        String key = ItemRedisKeys.ITEM_ADMIN_SYSTEM_RANDOM;
        if(ObjectUtils.isNotEmpty(ADMINCACHEMAP)) {
            if(ObjectUtils.isNotEmpty(ADMINCACHEMAP.get(key+systemRandom))) {
                //请求时间一致
                logger.error("---> AuthFilter checkSign 当前请求时间戳和随机数和上次一样,请求拒绝:{} ", servletPath2);
                ((HttpServletResponse)response).sendError(208, "当前请求时间戳和随机数和上次一样,请求拒绝");
                return true;
            }
        }
        ADMINCACHEMAP.put(key+systemRandom,Long.parseLong(timestamp));
        waitSign = ENCRYPTION_KEY+timestamp+systemRandom;
 
 
        try {
            // 20秒内有效
            long timestampDate = Long.parseLong(timestamp) + (60 * 2);
            if (timestampDate < currDate) {
                // 请求过期
                logger.error("---> AuthFilter checkSign 请求过期:{} ", servletPath2);
                ((HttpServletResponse)response).sendError(202, "请求过期");
                return true;
            }
        } catch (NumberFormatException e) {
            assert response != null;
            logger.error("---> AuthFilter checkSign 请求异常:{}", servletPath2);
            ((HttpServletResponse)response).sendError(204, "请求异常");
            return true;
        }
 
        String sign = request.getHeader("sign");
        if (sign == null || "".equals(sign.trim())) {
            // 没有签名返回验签失败
            assert response != null;
            logger.error("---> AuthFilter checkSign 签名为空:{}", servletPath2);
            ((HttpServletResponse)response).sendError(205, "签名为空");
            return true;
        }
 
        // 验签, 根据时间戳生成签名加盐值反复加密两次, 对比是否一致
        // 第一个参数为加密内容, 第二个参数为加密时的盐值
        // 获取后台管理MD5盐值
        String md5_result = MD5.sign(waitSign).toUpperCase();
        if (!md5_result.equals(sign)) {
            // 验签失败
            logger.error("---> AuthFilter checkSign 签名失败:{}", servletPath2);
            ((HttpServletResponse)response).sendError(206, "签名失败");
            return true;
        }
 
        return false;
    }
 
    private void processTimezone(HttpServletRequest req) throws IOException {
        // 前端定制了展示数据使用的时区
        String showTimeZone = req.getParameter("timezone");
        if (StrUtil.isBlank(showTimeZone)) {
            showTimeZone = req.getHeader("x-api-timezone");
        }
        if (StrUtil.isBlank(showTimeZone)) {
            // 默认使用配置的时区
            showTimeZone = ApplicationUtil.getProperty("config.timezone.show", "");
        } else {
            if (showTimeZone.contains("%")) {
                showTimeZone = URLDecoder.decode(showTimeZone, "UTF-8");
            }
        }
        if (StrUtil.isBlank(showTimeZone) || Objects.equals(showTimeZone, "default")) {
            // 默认使用存储数据使用的时区,内部 feign 调用时,需要传固定值:default,代表不执行时间转换
            showTimeZone = ZoneId.systemDefault().getId();
        }
 
        // 校验传入的 tz 格式是否正确
        ZoneId.of(showTimeZone);
        TimeZoneContext.showTimeZoneId.set(showTimeZone);
 
        // 前端告知本地使用的时区
        String clientTimeZone = req.getHeader("x-api-client-timezone");
        if (StrUtil.isBlank(clientTimeZone)) {
            clientTimeZone = ZoneId.systemDefault().getId();
        }
        // 校验传入的 tz 格式是否正确
        ZoneId.of(clientTimeZone);
        TimeZoneContext.clientTimeZoneId.set(clientTimeZone);
    }
 
    private boolean checkBlackRequest(HttpServletRequest req, HttpServletResponse resp, String clientIp, String userCode) {
        String requestUri = req.getRequestURI();
        if (requestUri.contains("riskclient/save") || requestUri.contains("demo/checkip")) {
            return false;
        }
        //logger.info("---> AuthFilter.checkBlackRequest 中的 urlBuffer = {}, clientIp:{}, userId:{}", requestUri, clientIp, userId);
        if (StrUtil.isNotBlank(userCode) && !Objects.equals(userCode, "0")) {
            // 先处理断网逻辑
            List<RiskClient> riskList = RiskClientUtil.getRiskInfoByUserCode(userCode, "badnetwork");
            if (CollectionUtil.isNotEmpty(riskList)) {
                logger.info("---> AuthFilter.checkBlackRequest 当前用户断网 requestUri = {}, clientIp:{}, userCode:{}", requestUri, clientIp, userCode);
                // 模拟基于用户断网
                //closeAction(requestUri, resp);
                return true;
            }
 
            // 处理黑名单逻辑
            riskList = RiskClientUtil.getRiskInfoByUserCode(userCode, "black");
            if (CollectionUtil.isNotEmpty(riskList)) {
                try {
                    logger.info("---> AuthFilter.checkBlackRequest 当前用户断网 requestUri = {}, clientIp:{}, userCode:{}", requestUri, clientIp, userCode);
                    resp.setCharacterEncoding(CharsetUtil.UTF_8);
                    resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    resp.setStatus(200);
                    //req.getRequestDispatcher("/html/404").forward(req, resp); // 断网展示网页,使用 forward 请求转发
                    String responseJson = "{\"code\":1,\"msg\":\"Forbidden\"}";
                    resp.getWriter().write(responseJson);
                } catch (Exception e) {
                    logger.error("---> AuthFilter.checkBlackRequest requestUri:{} 访问报错: ", requestUri, e);
                }
                return true;
            }
        }
 
        // 未能基于用户来识别,则继续尝试基于 ip 判断
        List<RiskClient> riskList = RiskClientUtil.getRiskInfoByIp(clientIp, "badnetwork");
        if (CollectionUtil.isNotEmpty(riskList)) {
            logger.info("---> AuthFilter.checkBlackRequest 当前IP断网 requestUri = {}, clientIp:{}", requestUri, clientIp);
            // 模拟基于客户端ip断网
            //closeAction(requestUri, resp);
            return true;
        }
 
        // 处理黑名单逻辑
        riskList = RiskClientUtil.getRiskInfoByIp(clientIp, "black");
        if (CollectionUtil.isNotEmpty(riskList)) {
            try {
                logger.info("---> AuthFilter.checkBlackRequest 当前IP断网 requestUri = {}, clientIp:{}", requestUri, clientIp);
                resp.setCharacterEncoding(CharsetUtil.UTF_8);
                resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
                resp.setStatus(200);
                //req.getRequestDispatcher("/html/404").forward(req, resp); // 断网展示网页,使用 forward 请求转发
                String responseJson = "{\"code\":1,\"msg\":\"Forbidden\"}";
                resp.getWriter().write(responseJson);
            } catch (Exception e) {
                logger.error("---> AuthFilter.checkBlackRequest requestUri:{} 访问报错: ", requestUri, e);
            }
            return true;
        }
 
        return false;
    }
 
    private static void closeAction(String requestUri, HttpServletResponse resp) {
        if (requestUri.contains("!close.action")) {
            ThreadUtils.sleep(3000);
            resp.setCharacterEncoding(CharsetUtil.UTF_8);
            resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
            resp.setStatus(200);
            String responseJson = "{\"code\":1,\"msg\":\"Network Unavailable\"}";
            try {
                resp.getWriter().write(responseJson);
            } catch (IOException e) {
                throw new BusinessException(e);
            }
        }
    }
}