|
@@ -0,0 +1,202 @@
|
|
|
|
|
+package com.jzg.filter;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.redisson.api.RedissonClient;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
|
|
|
|
+import org.springframework.cloud.gateway.filter.GlobalFilter;
|
|
|
|
|
+import org.springframework.core.Ordered;
|
|
|
|
|
+import org.springframework.core.annotation.Order;
|
|
|
|
|
+import org.springframework.core.io.buffer.DataBuffer;
|
|
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.http.server.reactive.ServerHttpRequest;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+import org.springframework.web.server.ServerWebExchange;
|
|
|
|
|
+import reactor.core.publisher.Mono;
|
|
|
|
|
+
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 全局鉴权过滤器 —— 网关层统一认证入口
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 所有外部请求经过网关时,先在此过滤器校验 Token 的有效性。
|
|
|
|
|
+ * 白名单内的路径直接放行,其他路径必须携带有效的 Authorization 头。
|
|
|
|
|
+ * Token 校验通过后,将用户信息通过 X-* 请求头透传给下游微服务,
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author wh
|
|
|
|
|
+ * @date 2026/6/16
|
|
|
|
|
+ */
|
|
|
|
|
+@Component
|
|
|
|
|
+@Order(Ordered.HIGHEST_PRECEDENCE + 10)
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+public class AuthGlobalFilter implements GlobalFilter {
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private RedissonClient redissonClient;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 无需鉴权的白名单路径(前缀匹配)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final List<String> WHITELIST = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ static {
|
|
|
|
|
+ // ==================== Swagger / API 文档 ====================
|
|
|
|
|
+ WHITELIST.add("/swagger-ui/");
|
|
|
|
|
+ WHITELIST.add("/v3/api-docs");
|
|
|
|
|
+ WHITELIST.add("/swagger-resources/");
|
|
|
|
|
+ WHITELIST.add("/webjars/");
|
|
|
|
|
+ WHITELIST.add("/favicon.ico");
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 登录 & 注册(网页端) ====================
|
|
|
|
|
+ WHITELIST.add("/auth/login");
|
|
|
|
|
+ WHITELIST.add("/auth/captchaById");
|
|
|
|
|
+ WHITELIST.add("/auth/register");
|
|
|
|
|
+ WHITELIST.add("/auth/sendSmsCode");
|
|
|
|
|
+ WHITELIST.add("/auth/forgotPassword");
|
|
|
|
|
+ WHITELIST.add("/auth/publicKeyExchange");
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== APP 登录 ====================
|
|
|
|
|
+ WHITELIST.add("/appAuth/smsCodeLogin");
|
|
|
|
|
+ WHITELIST.add("/appAuth/appLogin");
|
|
|
|
|
+ WHITELIST.add("/appAuth/partnerAppLogin");
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 短信 ====================
|
|
|
|
|
+ WHITELIST.add("/send/sendSmsCode");
|
|
|
|
|
+ WHITELIST.add("/sendSmsCode");
|
|
|
|
|
+ WHITELIST.add("/appLogin");
|
|
|
|
|
+ WHITELIST.add("/smsCodeLogin");
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 其他公开接口 ====================
|
|
|
|
|
+ WHITELIST.add("/system/sysListAccountLinking");
|
|
|
|
|
+ WHITELIST.add("/supplementOrder/getOrderDetail");
|
|
|
|
|
+ WHITELIST.add("/dict/get");
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 本地生活调用的接口 ====================
|
|
|
|
|
+ WHITELIST.add("/signIn/updateSignCompleteRuleIn");
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
|
|
|
|
+ String path = exchange.getRequest().getURI().getPath();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 白名单路径直接放行
|
|
|
|
|
+ if (isWhitelisted(path)) {
|
|
|
|
|
+ return chain.filter(exchange);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 获取 Authorization 头(兼容大小写)
|
|
|
|
|
+ HttpHeaders headers = exchange.getRequest().getHeaders();
|
|
|
|
|
+ String authorization = headers.getFirst("Authorization");
|
|
|
|
|
+ if (authorization == null || authorization.isEmpty()) {
|
|
|
|
|
+ authorization = headers.getFirst("authorization");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. Token 不存在 → 401
|
|
|
|
|
+ if (authorization == null || authorization.isEmpty()) {
|
|
|
|
|
+ log.warn("【网关鉴权】请求缺少 Authorization 头。path=[{}]", path);
|
|
|
|
|
+ return unauthorized(exchange, "未提供认证令牌");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 校验 Token 在 Redis 中是否存在
|
|
|
|
|
+ String userJsonStr;
|
|
|
|
|
+ try {
|
|
|
|
|
+ Object bucketValue = redissonClient.getBucket(authorization).get();
|
|
|
|
|
+ if (bucketValue == null) {
|
|
|
|
|
+ log.warn("【网关鉴权】Token 无效或已过期。path=[{}]", path);
|
|
|
|
|
+ return unauthorized(exchange, "认证令牌无效或已过期");
|
|
|
|
|
+ }
|
|
|
|
|
+ userJsonStr = bucketValue.toString();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("【网关鉴权】Redis 查询异常。path=[{}], error=[{}]", path, e.getMessage(), e);
|
|
|
|
|
+ return unauthorized(exchange, "认证服务异常");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 解析用户信息并透传到下游微服务
|
|
|
|
|
+ try {
|
|
|
|
|
+ JSONObject userInfo = JSONObject.parseObject(userJsonStr);
|
|
|
|
|
+
|
|
|
|
|
+ ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
|
|
|
|
|
+ .header("X-User-Id", safeGet(userInfo, "userId"))
|
|
|
|
|
+ .header("X-Username", safeGet(userInfo, "username"))
|
|
|
|
|
+ .header("X-User-Name", safeGet(userInfo, "name"))
|
|
|
|
|
+ .header("X-System-Code", safeGet(userInfo, "system"))
|
|
|
|
|
+ .header("X-Dept-Id", safeGet(userInfo, "deptId"))
|
|
|
|
|
+ .header("X-Work-Number", safeGet(userInfo, "workNumber"))
|
|
|
|
|
+ .header("X-Province", safeGet(userInfo, "province"))
|
|
|
|
|
+ .header("X-Auths", safeGetJsonArray(userInfo, "auths"))
|
|
|
|
|
+ .header("X-Data-Scope", safeGetJsonObject(userInfo, "dataScope"))
|
|
|
|
|
+ .header("X-Head-Sculpture", safeGet(userInfo, "headSculpture"))
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ log.debug("【网关鉴权】认证通过。path=[{}], user=[{}], system=[{}]",
|
|
|
|
|
+ path, safeGet(userInfo, "username"), safeGet(userInfo, "system"));
|
|
|
|
|
+
|
|
|
|
|
+ return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("【网关鉴权】用户信息解析异常。path=[{}], error=[{}]", path, e.getMessage(), e);
|
|
|
|
|
+ return unauthorized(exchange, "用户信息解析失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 判断路径是否在白名单中(前缀匹配)
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean isWhitelisted(String path) {
|
|
|
|
|
+ for (String pattern : WHITELIST) {
|
|
|
|
|
+ if (path.startsWith(pattern)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全获取字符串字段(null → "")
|
|
|
|
|
+ */
|
|
|
|
|
+ private String safeGet(JSONObject json, String key) {
|
|
|
|
|
+ String value = json.getString(key);
|
|
|
|
|
+ return value != null ? value : "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全获取 JSONArray 字段(转为 JSON 字符串,null → "[]")
|
|
|
|
|
+ */
|
|
|
|
|
+ private String safeGetJsonArray(JSONObject json, String key) {
|
|
|
|
|
+ JSONArray array = json.getJSONArray(key);
|
|
|
|
|
+ return array != null ? array.toJSONString() : "[]";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全获取 JSONObject 字段(转为 JSON 字符串,null → "{}")
|
|
|
|
|
+ */
|
|
|
|
|
+ private String safeGetJsonObject(JSONObject json, String key) {
|
|
|
|
|
+ JSONObject obj = json.getJSONObject(key);
|
|
|
|
|
+ return obj != null ? obj.toJSONString() : "{}";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 返回 401 未授权响应(HTTP 200 + JSON body,与原有风格保持一致)
|
|
|
|
|
+ */
|
|
|
|
|
+ private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
|
|
|
|
|
+ exchange.getResponse().setStatusCode(HttpStatus.OK);
|
|
|
|
|
+ exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("code", 401);
|
|
|
|
|
+ body.put("msg", message);
|
|
|
|
|
+
|
|
|
|
|
+ byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
+ DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
|
|
|
|
|
+ return exchange.getResponse().writeWith(Mono.just(buffer));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|