Преглед изворни кода

东莞妇幼漏洞扫描处理

WuYi пре 2 дана
родитељ
комит
78445afb1a

+ 1 - 4
ibps-basic-root/modules/basic-response/src/main/java/com/lc/ibps/cloud/config/AuthorizationConfig.java

@@ -133,7 +133,6 @@ public class AuthorizationConfig {
 		ignoreUrls.add("/**/auth/api/grant/has/anonymous/api/grant/full");
 		ignoreUrls.add("/**/auth/api/grant/has/api/grant/full");
 		ignoreUrls.add("/**/service/demo/query");
-		ignoreUrls.add("/**/employee/create");
 
 		ignoreUrls.add("/**/csrf");
 		ignoreUrls.add("/**/health");
@@ -170,7 +169,6 @@ public class AuthorizationConfig {
 		ignoreUrls.add("/auth/api/grant/has/anonymous/api/grant/full");
 		ignoreUrls.add("/auth/api/grant/has/api/grant/full");
 		ignoreUrls.add("/service/demo/query");
-		ignoreUrls.add("/employee/create");
 		//签到路径放行
 		ignoreUrls.add("/ibps/business/v3//wx/openid");//1
 		ignoreUrls.add("/wx/openid");//2
@@ -178,7 +176,6 @@ public class AuthorizationConfig {
 		ignoreUrls.add("/ibps/business/v3//wx/adduser");//1
 		ignoreUrls.add("/wx/adduser");//2
 		ignoreUrls.add("/**/wx/adduser");
-		ignoreUrls.add("/employee/query");//1
 		ignoreUrls.add("/ibps/business/v3//wx/query");
 		ignoreUrls.add("/wx/query");
 		ignoreUrls.add("/**/wx/query");
@@ -197,7 +194,7 @@ public class AuthorizationConfig {
 		ignoreUrls.add("/employee/signInformation/save");
 		ignoreUrls.add("/employee/qRcode/query");
 		ignoreUrls.add("/employee/registrationOutsiders/save");
-		ignoreUrls.add("/employee/registrationOutsiders/query");
+		ignoreUrls.add("/employee/registrationOutsiders/checkUnfinished");
 		ignoreUrls.add("/employee/registrationOutsiders/getRegistrationOutsidersFileConfig");
 		ignoreUrls.add("/employee/satisfaction/save");
 		ignoreUrls.add("/employee/satisfaction/getQuestionnaireByQrCodeId");

+ 10 - 3
ibps-provider-root/modules/provider-business/src/main/java/com/lc/ibps/business/controller/ShortActingTokenController.java

@@ -5,13 +5,18 @@ import com.lc.ibps.base.core.util.I18nUtil;
 import com.lc.ibps.business.service.ShortActingTokenService;
 import com.lc.ibps.cloud.entity.APIResult;
 import com.lc.ibps.cloud.provider.GenericProvider;
+import com.lc.ibps.cloud.utils.RequestUtil;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.servlet.http.HttpServletRequest;
+
 @Api(tags = "获取短时效token")
 @RequestMapping("/short")
 @RestController
@@ -22,11 +27,13 @@ public class ShortActingTokenController extends GenericProvider {
 
     @ApiOperation("获取短时效token")
     @PostMapping("/apply")
-    //@RateLimiter(value = 5, timeUnit = TimeUnit.MINUTES) // 每分钟最多5次
-    APIResult<String> apply(){
+    APIResult<String> apply(
+            @ApiParam(name = "scene", value = "场景标识:registration 外来人员登记/sign 签到/satisfaction 满意度调查,不传则签发通用令牌")
+            @RequestParam(name = "scene", required = false) String scene,
+            HttpServletRequest request){
         APIResult<String> result = new APIResult<>();
         try {
-            result = shortActingTokenService.apply();
+            result = shortActingTokenService.apply(scene, RequestUtil.getIpAddr(request));
         } catch (Exception e) {
             setExceptionResult(result, StateEnum.ILLEGAL_REQUEST.getCode(), I18nUtil.getMessage(StateEnum.ILLEGAL_REQUEST.getCode() + ""), e);
         }

+ 95 - 39
ibps-provider-root/modules/provider-business/src/main/java/com/lc/ibps/business/intercept/TokenAuthInterceptor.java

@@ -13,24 +13,42 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.Map;
 
 @Component
 public class TokenAuthInterceptor extends AbstractFilter  {
 
-    private static final Set<String> NAMES = new HashSet<>(
-            Arrays.asList(
-                    "/employee/signInformation/query",
-                    "/employee/signInformation/save",
-                    "/employee/qRcode/query",
-                    "/employee/registrationOutsiders/query",
-                    "/employee/registrationOutsiders/save",
-                    "/employee/satisfaction/save",
-                    "/employee/satisfaction/getQuestionnaireByQrCodeId"
-            )
-    );
+    /** 旧版 /short/apply 签发的令牌值(未绑定场景),保留兼容 */
+    private static final String LEGACY_TOKEN_VALUE = "short-acting-token";
+
+    private static final String TOKEN_KEY_PREFIX = "white_list_access_token:";
+
+    private static final String TOKEN_HEADER = "x-authorization-access_token";
+
+    /** 场景前缀:按场景签发的令牌值形如 scene:<场景名> */
+    private static final String SCENE_VALUE_PREFIX = "scene:";
+
+    /**
+     * 受短令牌保护的接口及所属场景。
+     * 按场景签发的令牌只能访问同场景接口,防止一个令牌跨接口使用。
+     */
+    private static final Map<String, String> PROTECTED_URI_SCENE = new HashMap<>();
+
+    static {
+        // 外来人员登记(扫码登记)
+        PROTECTED_URI_SCENE.put("/employee/registrationOutsiders/save", "registration");
+        PROTECTED_URI_SCENE.put("/employee/registrationOutsiders/checkUnfinished", "registration");
+        PROTECTED_URI_SCENE.put("/employee/registrationOutsiders/getRegistrationOutsidersFileConfig", "registration");
+        // 签到
+        PROTECTED_URI_SCENE.put("/employee/signInformation/query", "sign");
+        PROTECTED_URI_SCENE.put("/employee/signInformation/save", "sign");
+        PROTECTED_URI_SCENE.put("/employee/qRcode/query", "sign");
+        // 满意度调查
+        PROTECTED_URI_SCENE.put("/employee/satisfaction/save", "satisfaction");
+        PROTECTED_URI_SCENE.put("/employee/satisfaction/getQuestionnaireByQrCodeId", "satisfaction");
+    }
 
     @Override
     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
@@ -39,33 +57,71 @@ public class TokenAuthInterceptor extends AbstractFilter  {
 
         httpRequest.setCharacterEncoding(StringPool.UTF_8);
         httpResponse.setCharacterEncoding(StringPool.UTF_8);
-        String uri = httpRequest.getRequestURI();
-        if (NAMES.contains(uri)){
-            // 1. 获取令牌(支持Header/参数)
-            //String token = httpRequest.getParameter("token");
-            String token = httpRequest.getHeader("x-authorization-access_token");
-            // 2. 令牌不存在
-            if (BeanUtils.isEmpty(token)) {
-                sendError(httpResponse, "访问令牌缺失");
-                return;
-            }
-            // 3. 验证Redis中的令牌
-            String key = "white_list_access_token:" + token;
-            Boolean exists = RedisUtil.redisTemplateString.hasKey(key);
-            if (exists == null || !exists) {
-                sendError(httpResponse, "令牌无效或已过期");
-                return;
+        String uri = normalizeUri(httpRequest.getRequestURI());
+        String scene = PROTECTED_URI_SCENE.get(uri);
+        if (scene == null){
+            filterChain.doFilter(servletRequest, servletResponse);
+            return;
+        }
+        // 1. 获取令牌
+        String token = httpRequest.getHeader(TOKEN_HEADER);
+        // 2. 令牌不存在
+        if (BeanUtils.isEmpty(token)) {
+            sendError(httpResponse, "访问令牌缺失");
+            return;
+        }
+        // 3. 验证Redis中的令牌
+        String key = TOKEN_KEY_PREFIX + token;
+        String value = RedisUtil.redisTemplateString.opsForValue().get(key);
+        if (BeanUtils.isEmpty(value)) {
+            sendError(httpResponse, "令牌无效或已过期");
+            return;
+        }
+        // 4. 场景校验:旧版无场景令牌放行(兼容存量前端);带场景的令牌只能访问同场景接口,其余值一律拒绝
+        boolean tokenAllowed = LEGACY_TOKEN_VALUE.equals(value)
+                || value.equals(SCENE_VALUE_PREFIX + scene);
+        if (!tokenAllowed) {
+            sendError(httpResponse, "令牌与当前接口不匹配");
+            return;
+        }
+        // 5. 删除令牌(一次性使用)
+        Boolean del = RedisUtil.redisTemplateString.delete(key);
+        if(del != null && del){
+            filterChain.doFilter(servletRequest, servletResponse);
+        }else {
+            sendError(httpResponse, "令牌不存在");
+        }
+    }
+
+    /**
+     * 规范化请求路径后再匹配,防止尾斜杠、路径参数、URL编码、连续斜杠等变体绕过精确匹配。
+     */
+    private String normalizeUri(String uri) {
+        if (BeanUtils.isEmpty(uri)) {
+            return "/";
+        }
+        String decoded = uri;
+        try {
+            decoded = URLDecoder.decode(uri, StringPool.UTF_8);
+        } catch (Exception e) {
+            // 解码失败时按原始路径处理
+        }
+        String[] segments = decoded.split("/");
+        StringBuilder normalized = new StringBuilder();
+        for (String segment : segments) {
+            if (segment.isEmpty()) {
+                continue;
             }
-            // 4. 删除令牌(一次性使用)
-            Boolean del = RedisUtil.redisTemplateString.delete(key);
-            if(del != null && del){
-                filterChain.doFilter(servletRequest, servletResponse);
-            }else {
-                sendError(httpResponse, "令牌不存在");
+            int semicolonIndex = segment.indexOf(';');
+            if (semicolonIndex >= 0) {
+                segment = segment.substring(0, semicolonIndex);
+                if (segment.isEmpty()) {
+                    continue;
+                }
             }
-        }else {
-            filterChain.doFilter(servletRequest, servletResponse);
+            normalized.append('/').append(segment);
         }
+        return normalized.length() == 0 ? "/" : normalized.toString();
     }
 
     // 统一错误响应
@@ -77,4 +133,4 @@ public class TokenAuthInterceptor extends AbstractFilter  {
                 String.format("{\"code\":%d,\"message\":\"%s\"}", 401, msg)
         );
     }
-}
+}

+ 8 - 2
ibps-provider-root/modules/provider-business/src/main/java/com/lc/ibps/business/service/ShortActingTokenService.java

@@ -4,7 +4,13 @@ import com.lc.ibps.cloud.entity.APIResult;
 
 public interface ShortActingTokenService {
 
-
-    APIResult<String> apply();
+    /**
+     * 签发一次性短时效令牌
+     *
+     * @param scene 场景标识(registration/sign/satisfaction),可为空兼容存量调用;
+     *              非空时签发的令牌只能访问该场景接口
+     * @param clientIp 调用方IP,用于限流
+     */
+    APIResult<String> apply(String scene, String clientIp);
 
 }

+ 61 - 3
ibps-provider-root/modules/provider-business/src/main/java/com/lc/ibps/business/service/impl/ShortActingTokenServiceImpl.java

@@ -1,26 +1,84 @@
 package com.lc.ibps.business.service.impl;
 
 import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.StrUtil;
+import com.lc.ibps.api.base.constants.StateEnum;
 import com.lc.ibps.business.service.ShortActingTokenService;
 import com.lc.ibps.cloud.entity.APIResult;
 import com.lc.ibps.cloud.redis.utils.RedisUtil;
 import lombok.extern.log4j.Log4j2;
 import org.springframework.stereotype.Service;
 
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 @Log4j2
 @Service
 public class ShortActingTokenServiceImpl implements ShortActingTokenService {
 
+    /** 旧版令牌值(未绑定场景),保留兼容存量前端 */
+    private static final String LEGACY_TOKEN_VALUE = "short-acting-token";
+
+    private static final String TOKEN_KEY_PREFIX = "white_list_access_token:";
+
+    private static final String RATE_KEY_PREFIX = "white_list_access_token:rate:";
+
+    private static final String SCENE_VALUE_PREFIX = "scene:";
+
+    /** 允许签发的场景:registration 外来人员登记、sign 签到、satisfaction 满意度调查 */
+    private static final Set<String> ALLOWED_SCENES = new HashSet<>(Arrays.asList("registration", "sign", "satisfaction"));
+
+    /** 单IP每分钟最多签发次数(正常扫码流程一次2~3次;覆盖集中出口NAT与测试场景) */
+    private static final int APPLY_LIMIT_PER_MINUTE = 30;
+
+    private static final int TOKEN_TTL_MINUTES = 5;
 
     @Override
-    public APIResult<String> apply() {
+    public APIResult<String> apply(String scene, String clientIp) {
         APIResult<String> result = new APIResult<>();
+        if (StrUtil.isNotBlank(scene) && !ALLOWED_SCENES.contains(scene)) {
+            result.setState(StateEnum.ILLEGAL_REQUEST.getCode());
+            result.setMessage("无效的场景标识");
+            return result;
+        }
+        if (isRateLimited(clientIp)) {
+            result.setState(StateEnum.ILLEGAL_PERMISSION_LIMIT.getCode());
+            result.setMessage("签发过于频繁,请稍后再试");
+            return result;
+        }
         String token = UUID.fastUUID().toString(true);
-        String key = "white_list_access_token:" + token;
-        RedisUtil.redisTemplateString.opsForValue().set(key,"short-acting-token",5 , TimeUnit.MINUTES);
+        String key = TOKEN_KEY_PREFIX + token;
+        String value = StrUtil.isBlank(scene) ? LEGACY_TOKEN_VALUE : SCENE_VALUE_PREFIX + scene;
+        RedisUtil.redisTemplateString.opsForValue().set(key, value, TOKEN_TTL_MINUTES, TimeUnit.MINUTES);
         result.setData(token);
         return result;
     }
+
+    /**
+     * 单IP限流:每分钟最多签发 APPLY_LIMIT_PER_MINUTE 次。
+     * Redis 异常时放行,不影响正常业务(令牌本身仍受一次性使用和TTL约束)。
+     */
+    private boolean isRateLimited(String clientIp) {
+        if (StrUtil.isBlank(clientIp)) {
+            return false;
+        }
+        try {
+            String rateKey = RATE_KEY_PREFIX + clientIp;
+            Long count = RedisUtil.redisTemplateString.opsForValue().increment(rateKey, 1L);
+            if (count == null) {
+                return false;
+            }
+            // 首次计数或上次设置过期失败时补上过期时间,避免计数键永不过期导致IP被永久限流
+            Long ttl = RedisUtil.redisTemplateString.getExpire(rateKey, TimeUnit.SECONDS);
+            if (count == 1L || ttl == null || ttl < 0) {
+                RedisUtil.redisTemplateString.expire(rateKey, 60, TimeUnit.SECONDS);
+            }
+            return count > APPLY_LIMIT_PER_MINUTE;
+        } catch (Exception e) {
+            log.warn("短令牌签发限流检查异常,已放行. ip: {}", clientIp, e);
+            return false;
+        }
+    }
 }

+ 53 - 0
ibps-provider-root/modules/provider-business/src/main/java/com/lc/ibps/components/employee/provider/RegistrationOutsidersProvider.java

@@ -1,17 +1,22 @@
 package com.lc.ibps.components.employee.provider;
 
+import java.util.Arrays;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import javax.annotation.Resource;
 
+import com.lc.ibps.base.core.util.string.StringUtil;
 import com.lc.ibps.base.framework.id.UniqueIdUtil;
 import com.lc.ibps.components.employee.persistence.dao.RegistrationOutsidersDao;
 import com.lc.ibps.entrust.dao.RegistrationOutsidersMapperDao;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 
 import com.lc.ibps.api.base.constants.StateEnum;
@@ -20,6 +25,8 @@ import com.lc.ibps.base.core.constants.StringPool;
 import com.lc.ibps.base.core.util.BeanUtils;
 import com.lc.ibps.cloud.entity.APIPageList;
 import com.lc.ibps.cloud.entity.APIRequest;
+import com.lc.ibps.cloud.entity.APIRequestParameter;
+import com.lc.ibps.cloud.entity.APIRequestSort;
 import com.lc.ibps.cloud.entity.APIResult;
 import com.lc.ibps.cloud.provider.GenericProvider;
 import com.lc.ibps.components.employee.api.IRegistrationOutsidersService;
@@ -154,4 +161,50 @@ public class RegistrationOutsidersProvider extends GenericProvider implements IR
 		}
 		return result;
 	}
+
+	/**
+	 * 扫码登记匿名场景专用:仅返回该手机号是否存在未登记离开的记录及最新记录id,
+	 * 不返回登记明细,替代匿名调用 query 造成的数据泄露。
+	 */
+	@ApiOperation(value = "查询手机号是否存在未登记离开的记录(扫码登记匿名场景)", notes = "返回hasUnfinished与latestId")
+	@RequestMapping(value = "/checkUnfinished", method = RequestMethod.POST)
+	public APIResult<Map<String, Object>> checkUnfinished(
+			@ApiParam(name = "shouJiHao", value = "手机号", required = true)
+			@RequestParam(name = "shouJiHao", required = true) String shouJiHao) {
+		APIResult<Map<String, Object>> result = new APIResult<>();
+		Map<String, Object> data = new HashMap<>();
+		data.put("hasUnfinished", false);
+		data.put("latestId", "");
+		try {
+			if (StringUtil.isEmpty(shouJiHao)) {
+				result.setData(data);
+				return result;
+			}
+			APIRequest request = new APIRequest();
+			APIRequestParameter parameter = new APIRequestParameter();
+			parameter.setKey("Q^shou_ji_hao_^S");
+			parameter.setValue(shouJiHao);
+			APIRequestSort sort = new APIRequestSort();
+			sort.setField("create_time_");
+			sort.setOrder("DESC");
+			request.setParameters(Arrays.asList(parameter));
+			request.setSorts(Arrays.asList(sort));
+
+			QueryFilter queryFilter = getQueryFilter(request);
+			List<RegistrationOutsidersPo> list = registrationOutsidersRepository.query(queryFilter);
+			if (BeanUtils.isNotEmpty(list)) {
+				data.put("latestId", list.get(0).getId());
+				for (RegistrationOutsidersPo po : list) {
+					if (BeanUtils.isEmpty(po.getLiKaiShiJian())) {
+						data.put("hasUnfinished", true);
+						break;
+					}
+				}
+			}
+			result.setData(data);
+		} catch (Exception e) {
+			setExceptionResult(result, StateEnum.ERROR.getCode(), StateEnum.ERROR.getText(), e);
+		}
+		return result;
+	}
 }