AuthzService.java 6.59 KB
package com.xly.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * xlyAi 侧身份与授权层(架构 §7)。
 *
 * <p><b>身份唯一来源 = {@link #resolveIdentity}</b>:透传的用户 token 经 ERP {@code /ai/whoami}
 * **服务端内省**换取真实的 userId/租户/用户类型——客户端自报的 userid/brandsid/usertype 一概不信。
 * token 缺失或无效时:仅本地开发({@code erp.dev-login.enabled=true})回落 dev 身份;
 * 生产一律返回 null,调用方必须拒绝(fail-closed,绝不降级成 dev/管理员)。
 *
 * <p>授权 = 表单级白名单:后端逐用户表单权限被关(只验登录 + 租户 + 行级),所以在 agent 侧按用户
 * 实际授权(`sysjurisdiction`,即 `getsAuthsIdNew` 的逻辑)限制可碰的表单/菜单。
 * 管理员(sysadmin/admin) = 全部权限(返回 null);Read / Write 共用此边界。
 */
@Service
public class AuthzService {

    private final JdbcTemplate jdbc;
    private final ErpClient erp;

    @Value("${erp.dev-login.username:}")
    private String devUserNo;
    @Value("${erp.dev-login.brand:}")
    private String devBrand;
    @Value("${erp.dev-login.subsidiary:}")
    private String devSub;
    @Value("${erp.dev-login.usertype:sysadmin}")
    private String devUserType;

    @Value("${erp.dev-login.userid:}")
    private String devUserIdOverride;

    public AuthzService(JdbcTemplate jdbc, ErpClient erp) {
        this.jdbc = jdbc;
        this.erp = erp;
    }

    /**
     * 服务端身份解析(所有端点的唯一入口)。token 有效 → 内省出的真实身份;
     * token 无效/过期 → null(**不**回落 dev,防止过期用户被静默提权);
     * token 缺失 → dev-login 开启时 dev 身份,否则 null。
     */
    public com.xly.agent.AgentIdentity resolveIdentity(String authorization) {
        String token = authorization == null ? "" : authorization.trim();
        if (!token.isBlank()) {
            com.fasterxml.jackson.databind.JsonNode w = erp.whoami(token);
            if (w == null) {
                return null;
            }
            String userId = w.path("sId").asText("");
            String brandsId = w.path("sBrandsId").asText("");
            String subsidiaryId = w.path("sSubsidiaryId").asText("");
            String userType = w.path("sType").asText("");
            Set<String> granted = grantedIds(userId, userType, brandsId, subsidiaryId);
            return new com.xly.agent.AgentIdentity(token, userId, brandsId, subsidiaryId, granted);
        }
        return erp.devLoginEnabled() ? devIdentity() : null;
    }

    /**
     * dev-login(仅本地开发)身份:token 为空 → ErpClient 回退 dev-login;权限集按 dev 账号解析
     * (admin → null = 全部)。生产不可达(resolveIdentity 已按开关拦截)。
     */
    public com.xly.agent.AgentIdentity devIdentity() {
        String uid = devUserIdOverride != null && !devUserIdOverride.isBlank() ? devUserIdOverride : resolveDevUserId();
        Set<String> granted = grantedIds(uid, devUserType, devBrand, devSub);
        return new com.xly.agent.AgentIdentity(null, uid, devBrand, devSub, granted);
    }

    /** null = 全部(管理员);否则 = 有权的 id 集合。 */
    private Set<String> grantedIds(String userId, String userType, String brandsId, String subsidiaryId) {
        if (isAdmin(userType)) {
            return null; // 超管全部权限
        }
        Set<String> ids = new HashSet<>();
        if (userId == null) {
            return ids;
        }
        List<Map<String, Object>> rows;
        Integer direct = safeCount(
                "SELECT COUNT(*) FROM sysjurisdiction WHERE sBrandsId=? AND sSubsidiaryId=? AND sUserId=?",
                brandsId, subsidiaryId, userId);
        if (direct != null && direct > 0) {
            rows = jdbc.queryForList(
                    "SELECT J.sKey AS sKey FROM sysjurisdiction J WHERE J.sBrandsId=? AND J.sSubsidiaryId=? AND J.sUserId=?",
                    brandsId, subsidiaryId, userId);
        } else {
            String groups = queryStr(
                    "SELECT GROUP_CONCAT(sJurisdictionClassifyId) FROM sftlogininfojurisdictiongroup " +
                            "WHERE sBrandsId=? AND sSubsidiaryId=? AND sParentId=? " +
                            "AND sJurisdictionClassifyId IN (SELECT sId FROM sisjurisdictionclassify WHERE bLogininfoShow=0)",
                    brandsId, subsidiaryId, userId);
            if (groups == null || groups.isBlank()) {
                return ids;
            }
            List<String> gl = Arrays.asList(groups.split(","));
            String in = gl.stream().map(g -> "?").collect(Collectors.joining(","));
            List<Object> args = new ArrayList<>();
            args.add(brandsId);
            args.add(subsidiaryId);
            args.addAll(gl);
            rows = jdbc.queryForList(
                    "SELECT J.sKey AS sKey FROM sysjurisdiction J WHERE J.sBrandsId=? AND J.sSubsidiaryId=? " +
                            "AND J.sJurisdictionClassifyId IN (" + in + ")", args.toArray());
        }
        for (Map<String, Object> r : rows) {
            Object k = r.get("sKey");
            if (k != null) {
                for (String part : k.toString().split("-")) {
                    if (!part.isBlank()) {
                        ids.add(part);
                    }
                }
            }
        }
        return ids;
    }

    private String resolveDevUserId() {
        return queryStr("SELECT sId FROM gdslogininfo WHERE sUserNo=? AND sBrandsId=? LIMIT 1", devUserNo, devBrand);
    }

    private boolean isAdmin(String userType) {
        return userType != null && (userType.equalsIgnoreCase("sysadmin") || userType.equalsIgnoreCase("admin"));
    }

    private Integer safeCount(String sql, Object... args) {
        try {
            return jdbc.queryForObject(sql, Integer.class, args);
        } catch (Exception e) {
            return 0;
        }
    }

    private String queryStr(String sql, Object... args) {
        try {
            List<Map<String, Object>> r = jdbc.queryForList(sql, args);
            if (!r.isEmpty()) {
                Object v = r.get(0).values().iterator().next();
                return v == null ? null : v.toString();
            }
        } catch (Exception ignore) {
        }
        return null;
    }
}