Forráskód Böngészése

fix:浪潮云高危漏洞修复

fantingwei 1 éve
szülő
commit
53c1abdf03

+ 29 - 0
src/main/java/com/inspur/smsb/gateway/dto/KeycloakGroupsDto.java

@@ -0,0 +1,29 @@
+package com.inspur.smsb.gateway.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author fantingwei
+ * @date 2024/4/7
+ */
+@Data
+public class KeycloakGroupsDto implements Serializable {
+    private static final long serialVersionUID = 7074906195276434617L;
+
+    /**
+     * keycloak的用户id
+     */
+    private String id;
+
+    /**
+     * 租户名称
+     */
+    private String name;
+
+    /**
+     * 租户路径
+     */
+    private String path;
+}

+ 162 - 8
src/main/java/com/inspur/smsb/gateway/filter/WebFluxUserRequestInfoFilter.java

@@ -1,9 +1,12 @@
 package com.inspur.smsb.gateway.filter;
 
+import com.alibaba.cola.dto.Response;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Strings;
+import com.inspur.smsb.gateway.dto.KeycloakGroupsDto;
 import com.inspur.smsb.gateway.dto.KeycloakUserDto;
 import com.inspur.smsb.gateway.utils.HttpClientUtil;
 import com.nimbusds.jose.JWSObject;
@@ -12,7 +15,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
 import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
 import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
@@ -21,9 +28,7 @@ import reactor.core.publisher.Mono;
 
 import javax.annotation.Resource;
 import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -46,6 +51,8 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
     private String clientSecret;
     @Value("${keycloak.adminUserId}")
     private String adminUserId;
+    private static final String ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN";
+    private static final String ROLE_ADMIN = "ROLE_ADMIN";
 
     @Resource
     private ObjectMapper objectMapper;
@@ -53,18 +60,23 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
     @Override
     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
         String wxAppletId = exchange.getRequest().getHeaders().getFirst("WxAppletId");
+        Map<String, String> queryParams = exchange.getRequest().getQueryParams().toSingleValueMap();
+        String urlTenant = queryParams.get("tenant");
+        String urlOrg = queryParams.get("org");
         if (!Strings.isNullOrEmpty(wxAppletId)) {
             // search for userid
             String tokenUrl = keyCloakServiceUrl + "realms/" + realm + "/protocol/openid-connect/token";
             String token = getToken(tokenUrl, clientId, clientSecret);
             if (StringUtils.hasText(token)) {
                 String url = keyCloakServiceUrl + "admin/realms/" + realm + "/users";
+                // 查找所有的用户信息
                 List<KeycloakUserDto> list = queryUsers(url, token);
                 list = list.stream().filter(keycloakUserDto -> {
                     Map attributes = keycloakUserDto.getAttributes();
                     if (attributes == null) {
                         return false;
                     }
+                    // 获取用户attributes中的 wechat-applet-openid ,找到与请求头中一致的用户信息
                     JSONArray array = (JSONArray) attributes.get("wechat-applet-openid");
                     String openIdAttr = "";
                     if (array != null && !array.isEmpty()) {
@@ -73,12 +85,45 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
                     return StringUtils.hasText(openIdAttr) && openIdAttr.equals(wxAppletId);
                 }).collect(Collectors.toList());
                 if (!list.isEmpty()) {
-                    String userId = list.get(0).getId();
-
+                    KeycloakUserDto user = list.get(0);
+                    String userId = user.getId();
+                    log.info("userId:{}", userId);
                     ServerHttpRequest request = exchange.getRequest()
                         .mutate()
                         .header("userId", userId)
                         .build();
+
+                    if (StringUtils.hasText(urlTenant) || StringUtils.hasText(urlOrg)) {
+                        // 1、获取用户id、用户部门(测试没有path、org时的情况)、用户租户;
+                        JSONArray org = (JSONArray) user.getAttributes().get("org");
+                        String userOrg = Objects.isNull(org) ? null : org.get(0).toString();
+                        String userTenant = getGroupsByUserId(userId);
+                        String userRealTenant = StringUtils.hasText(userTenant) ? userTenant :
+                            "/" +  Arrays.stream(userOrg.split("/")).collect(Collectors.toList()).get(1);
+                        String userRealOrg = Objects.isNull(userOrg) ? userTenant : userOrg;
+                        log.info("[微信小程序] userId:'{}, userOrg:{}, userTenant:{}, userRealTenant:{}, userRealOrg:{}", userId, userOrg, userTenant, userRealTenant,
+                            userRealOrg);
+
+                        // 2、有urlOrg或者urlTenant时,才去做过滤,获取真实的urlRealTenant和urlRealOrg
+                        String urlRealTenant = Objects.isNull(urlTenant) ?
+                            "/" + Arrays.stream(urlOrg.split("/")).filter(s -> !s.isEmpty()).collect(Collectors.toList()).get(0) : urlTenant;
+                        String urlRealOrg = Objects.isNull(urlOrg) ? urlTenant : urlOrg;
+                        log.info("[url信息] urlTenant:{}, urlOrg:{}, urlRealTenant:{}, urlRealOrg:{}", urlTenant, urlOrg, urlRealTenant, urlRealOrg);
+
+                        // 3、根据用户角色去做判断
+                        if (!userHasRole(userId, ROLE_SUPER_ADMIN) && userHasRole(userId, ROLE_ADMIN)) {
+                            if (!urlRealTenant.equalsIgnoreCase(userRealTenant)) {
+                                return getErrorResponse(exchange);
+                            }
+                        } else if (!userHasRole(userId, ROLE_SUPER_ADMIN) && !userHasRole(userId, ROLE_ADMIN)) {
+                            String[] userArray = userRealOrg.split("/");
+                            String[] urlOrgArray = urlRealOrg.split("/");
+                            if (urlOrgArray.length < userArray.length || !isLegal(userArray, urlOrgArray)) {
+                                return getErrorResponse(exchange);
+                            }
+                        }
+                    }
+
                     // 把新的 exchange 放回到过滤链
                     return chain.filter(exchange.mutate().request(request).build());
                 }
@@ -113,7 +158,6 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
 
                 String realToken = token.replace("Bearer ", "");
                 JWSObject jwsObject = JWSObject.parse(realToken);
-
                 if (adminUserId.equals(String.valueOf(jwsObject.getPayload().toJSONObject().get("sub")))) {
                     //just skip this adapter
                     return chain.filter(exchange);
@@ -121,12 +165,52 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
                 //租户转换类型
                 List<String> tenantMap = (List<String>) jwsObject.getPayload().toJSONObject().get("tenant");
                 String tenant = CollectionUtils.isEmpty(tenantMap) ? "unknown" : tenantMap.get(0);
+                String userId = String.valueOf(jwsObject.getPayload().toJSONObject().get("sub"));
+                String userName = String.valueOf(jwsObject.getPayload().toJSONObject().get("preferred_username"));
                 ServerHttpRequest request = exchange.getRequest()
                     .mutate()
-                    .header("userId", String.valueOf(jwsObject.getPayload().toJSONObject().get("sub")))
-                    .header("userName", String.valueOf(jwsObject.getPayload().toJSONObject().get("preferred_username")))
+                    // 对应 smsb_department_user 的 user_id 字段
+                    .header("userId", userId)
+                    .header("userName", userName)
                     .header("tenant", String.valueOf(tenant))
                     .build();
+
+                if (StringUtils.hasText(urlTenant) || StringUtils.hasText(urlOrg)) {
+                    // 1、有urlOrg或者urlTenant时,才去做过滤
+                    String urlRealTenant = Objects.isNull(urlTenant) ?
+                        "/" + Arrays.stream(urlOrg.split("/")).filter(s -> !s.isEmpty()).collect(Collectors.toList()).get(0) : urlTenant;
+                    String urlRealOrg = Objects.isNull(urlOrg) ? urlTenant : urlOrg;
+                    log.info("[url信息] urlTenant:{}, urlOrg:{}, urlRealTenant:{}, urlRealOrg:{}", urlTenant, urlOrg, urlRealTenant, urlRealOrg);
+
+                    // 根据token, 获取当前用户的角色  ROLE_SUPER_ADMIN -> 超管, ROLE_ADMIN -> 租户管理员,tenant已经获取,再获取org
+                    com.nimbusds.jose.shaded.json.JSONObject realmAccess = (com.nimbusds.jose.shaded.json.JSONObject)jwsObject.getPayload().toJSONObject().get("realm_access");
+                    com.nimbusds.jose.shaded.json.JSONArray rolesArray = (com.nimbusds.jose.shaded.json.JSONArray) realmAccess.get("roles");
+                    List<String> roles = new ArrayList<>();
+                    for (Object role : rolesArray) {
+                        roles.add((String) role);
+                    }
+                    // userOrg可能为空,userTenant应该是必有的
+                    String userOrg = String.valueOf(jwsObject.getPayload().toJSONObject().get("org"));
+                    String userTenant = tenant;
+                    String userRealTenant = Objects.isNull(userTenant) ?
+                        "/" +  Arrays.stream(userOrg.split("/")).filter(s -> !s.isEmpty()).collect(Collectors.toList()).get(0) : userTenant;
+                    String userRealOrg = "null".equalsIgnoreCase(userOrg) ? userTenant : userOrg ;
+                    log.info("[用户信息] userOrg:{}, userTenant:{}, userRealTenant:{}, userRealOrg:{}", userOrg, userTenant, userRealTenant, userRealOrg);
+
+                    // 2、根据用户角色去做判断(tenant一定有,org不一定有)
+                    if (Boolean.FALSE.equals(roles.contains(ROLE_SUPER_ADMIN)) && Boolean.TRUE.equals(roles.contains(ROLE_ADMIN))) {
+                        if (!urlRealTenant.equalsIgnoreCase(userRealTenant)) {
+                            return getErrorResponse(exchange);
+                        }
+                    } else if (Boolean.FALSE.equals(roles.contains(ROLE_SUPER_ADMIN)) && Boolean.FALSE.equals(roles.contains(ROLE_ADMIN))) {
+                        String[] userArray = userRealOrg.split("/");
+                        String[] urlOrgArray = urlRealOrg.split("/");
+                        if (urlOrgArray.length < userArray.length || !isLegal(userArray, urlOrgArray)) {
+                            return getErrorResponse(exchange);
+                        }
+                    }
+                }
+
                 // 把新的 exchange 放回到过滤链
                 return chain.filter(exchange.mutate().request(request).build());
             } catch (ParseException e) {
@@ -136,6 +220,40 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
         }
     }
 
+    /**
+     * 无权限访问
+     */
+    private Mono<Void> getErrorResponse(ServerWebExchange exchange){
+        ServerHttpResponse response = exchange.getResponse();
+        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+        return response.writeWith(Mono.fromSupplier(() -> {
+            DataBufferFactory bufferFactory = response.bufferFactory();
+            try {
+                return bufferFactory.wrap(objectMapper.writeValueAsBytes(Response.buildFailure(String.valueOf(HttpStatus.PRECONDITION_FAILED.value()), "无访问权限")));
+            } catch (JsonProcessingException e) {
+                log.error("Error writing response", e);
+                return bufferFactory.wrap(new byte[0]);
+            }
+        }));
+    }
+
+    /**
+     * 判断部门路径是否合法
+     * @param userRealOrg 用户真实的部门路径
+     * @param urlRealOrg url请求参数的真实路径
+     * @return 是否合法,true为合法,false为不合法
+     */
+    private boolean isLegal(String[] userArray, String[] urlArray){
+        boolean flag = true;
+        for (int i = 0; i < userArray.length; i++) {
+            if (!userArray[i].equalsIgnoreCase(urlArray[i])) {
+                flag = false;
+                break;
+            }
+        }
+        return flag;
+    }
+
 
     public String getToken(String tokenUrl, String clientId, String clientSecret) {
         try {
@@ -160,4 +278,40 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
         }
         return userList;
     }
+
+    /**
+     * 根据userId获取到租户路径
+     * @param userId 用户id
+     * @return 该用户的部门路径
+     */
+    private String getGroupsByUserId (String userId) {
+        String url = keyCloakServiceUrl + "admin/realms/" + realm + "/users/" + userId + "/groups";
+        String tokenUrl = keyCloakServiceUrl + "realms/" + realm + "/protocol/openid-connect/token";
+        String token = getToken(tokenUrl, clientId, clientSecret);
+        String group = HttpClientUtil.sendGet(url, "Bearer " + token);
+        KeycloakGroupsDto[] groupDto = JSONObject.parseObject(group, KeycloakGroupsDto[].class);
+        return groupDto.length > 0 ? groupDto[0].getPath() : "";
+    }
+
+    /**
+     * 根据userId判断该用户是否含有roleName
+     * @param userId 用户id
+     * @param roleName 角色名
+     * @return 是否包含
+     */
+    private Boolean userHasRole (String userId, String roleName) {
+        String url = keyCloakServiceUrl + "admin/realms/" + realm + "/roles/" + roleName + "/users";
+        String tokenUrl = keyCloakServiceUrl + "realms/" + realm + "/protocol/openid-connect/token";
+        String token = getToken(tokenUrl, clientId, clientSecret);
+        // 获取某个角色的所有用户
+        String userMap = HttpClientUtil.sendGet(url, "Bearer " + token);
+        KeycloakUserDto[] result = JSONObject.parseObject(userMap, KeycloakUserDto[].class);
+        // 判断userId是否在用户中
+        for (KeycloakUserDto dto : result) {
+            if (userId.equalsIgnoreCase(dto.getId())) {
+                return true;
+            }
+        }
+        return false;
+    }
 }