Przeglądaj źródła

Merge remote-tracking branch 'origin/master'

Qchen 3 miesięcy temu
rodzic
commit
8ecb30baff

+ 191 - 0
commons/src/main/java/com/jzg/commons/util/UserContextUtil.java

@@ -0,0 +1,191 @@
+package com.jzg.commons.util;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用户上下文工具类 —— 从网关透传的请求头中读取用户信息
+ * <p>
+ * 网关层 {@code AuthGlobalFilter} 在 Token 校验通过后,
+ * 会将用户信息注入到以下请求头中,下游微服务可直接读取,无需重复查询 Redis:
+ * <ul>
+ *   <li>{@code X-User-Id} —— 用户ID</li>
+ *   <li>{@code X-Username} —— 用户名/手机号</li>
+ *   <li>{@code X-User-Name} —— 姓名</li>
+ *   <li>{@code X-System-Code} —— 登录系统编码</li>
+ *   <li>{@code X-Dept-Id} —— 部门ID</li>
+ *   <li>{@code X-Work-Number} —— 工号</li>
+ *   <li>{@code X-Province} —— 省份</li>
+ *   <li>{@code X-Auths} —— 权限列表(JSON数组)</li>
+ *   <li>{@code X-Data-Scope} —— 数据权限(JSON对象)</li>
+ *   <li>{@code X-Head-Sculpture} —— 头像URL</li>
+ * </ul>
+ * </p>
+ *
+ * @author lipf
+ * @date 2026/6/16
+ */
+public class UserContextUtil {
+
+    private UserContextUtil() {
+        // 工具类,禁止实例化
+    }
+
+    /**
+     * 获取当前 HTTP 请求(可能为 null,非 Web 上下文时)
+     */
+    private static HttpServletRequest getRequest() {
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes == null) {
+            return null;
+        }
+        return attributes.getRequest();
+    }
+
+    /**
+     * 从请求头获取用户ID
+     *
+     * @return 用户ID,未获取到时返回空字符串
+     */
+    public static String getUserId() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-User-Id");
+    }
+
+    /**
+     * 从请求头获取用户名
+     *
+     * @return 用户名,未获取到时返回空字符串
+     */
+    public static String getUsername() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-Username");
+    }
+
+    /**
+     * 从请求头获取姓名
+     *
+     * @return 姓名,未获取到时返回空字符串
+     */
+    public static String getUserName() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-User-Name");
+    }
+
+    /**
+     * 从请求头获取系统编码
+     *
+     * @return 系统编码,未获取到时返回空字符串
+     */
+    public static String getSystemCode() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-System-Code");
+    }
+
+    /**
+     * 从请求头获取部门ID
+     *
+     * @return 部门ID,未获取到时返回空字符串
+     */
+    public static String getDeptId() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-Dept-Id");
+    }
+
+    /**
+     * 从请求头获取工号
+     *
+     * @return 工号,未获取到时返回空字符串
+     */
+    public static String getWorkNumber() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-Work-Number");
+    }
+
+    /**
+     * 从请求头获取省份
+     *
+     * @return 省份,未获取到时返回空字符串
+     */
+    public static String getProvince() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-Province");
+    }
+
+    /**
+     * 从请求头获取头像URL
+     *
+     * @return 头像URL,未获取到时返回空字符串
+     */
+    public static String getHeadSculpture() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "";
+        return headerOrEmpty(request, "X-Head-Sculpture");
+    }
+
+    /**
+     * 从请求头获取用户权限列表
+     *
+     * @return 权限标识列表,未获取到时返回空列表
+     */
+    public static List<String> getPerms() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return new ArrayList<>();
+
+        String authsJson = headerOrEmpty(request, "X-Auths");
+        if (authsJson.isEmpty()) return new ArrayList<>();
+
+        List<String> perms = new ArrayList<>();
+        try {
+            JSONArray auths = JSONArray.parseArray(authsJson);
+            for (int i = 0; i < auths.size(); i++) {
+                JSONObject auth = auths.getJSONObject(i);
+                String perm = auth.getString("perms");
+                if (perm != null) {
+                    perms.add(perm);
+                }
+            }
+        } catch (Exception e) {
+            // 解析失败,返回空列表
+        }
+        return perms;
+    }
+
+    /**
+     * 从请求头获取数据权限
+     *
+     * @return 数据权限 JSON 字符串,未获取到时返回 "{}"
+     */
+    public static String getDataScope() {
+        HttpServletRequest request = getRequest();
+        if (request == null) return "{}";
+        return headerOrEmpty(request, "X-Data-Scope");
+    }
+
+    /**
+     * 判断用户信息是否来自网关透传(即请求头中是否存在 X-User-Id)
+     *
+     * @return true 表示本次请求已经过网关鉴权
+     */
+    public static boolean isFromGateway() {
+        return !getUserId().isEmpty();
+    }
+
+    private static String headerOrEmpty(HttpServletRequest request, String name) {
+        String value = request.getHeader(name);
+        return value != null ? value : "";
+    }
+}

+ 202 - 0
gateway/src/main/java/com/jzg/filter/AuthGlobalFilter.java

@@ -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));
+    }
+}

+ 1 - 1
tenant/insurance/quotation-taishan/src/main/java/com/jzg/quotation/taishan/crawler/build/TaiShanCrawlerQuoteRequestVoBuild.java

@@ -256,7 +256,7 @@ public class TaiShanCrawlerQuoteRequestVoBuild {
 
             // 跳过保额为空的险种,避免数值解析时空指针异常
             if (infoVo.getAmount() == null) {
-                log.warn("泰山报价-跳过保额为空的险种: platformKindCode={}, kindCode={}", kindCode, infoVo.getKindCode());
+                log.info("泰山报价-跳过保额为空的险种: platformKindCode={}, kindCode={}", kindCode, infoVo.getKindCode());
                 continue;
             }