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

fix(security): 禁用登录后门并收敛匿名放行

禁用loginByUserId/loginTest与万能验证码,收紧SecurityConfig危险匿名接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
吴树波 21 órája
szülő
commit
cbe52d3c57

+ 7 - 20
fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -112,14 +112,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/baidu/**").anonymous()
                 .antMatchers("/baiduBack/**").anonymous()
                 .antMatchers("/test/gtp/*").anonymous()
-                .antMatchers("common/getTask/*").anonymous()
-                .antMatchers("//his/data/endFollow/*").anonymous()
-                .antMatchers("//his/data/end/*").anonymous()
-                .antMatchers("//his/data/addCF/*").anonymous()
-                .antMatchers("//his/data/addCom/*").anonymous()
-                .antMatchers("//his/data/testSendSub/*").anonymous()
-                .antMatchers("//his/data/test/*").anonymous()
-                .antMatchers("//his/data/Follow/*").anonymous()
+                // 安全修复:移除 his/data、uploadOSS、unbind 等危险匿名放行;支付/短信回调等仍需匿名
                 .antMatchers("/company/companyVoiceRobotic/callerResult").anonymous()
                 .antMatchers("/qw/data/*").anonymous()
                 .antMatchers("/app/common/expressNotify").anonymous()
@@ -132,23 +125,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/tzPay/*").anonymous()
                 .antMatchers("//his/pay/*").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/chat/upload/uploadFile**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
-                .antMatchers("/common/unbindQwUserByServerIds").anonymous()
-                .antMatchers("/swagger-ui.html").anonymous()
-                .antMatchers("/swagger-resources/**").anonymous()
-                .antMatchers("/webjars/**").anonymous()
-                .antMatchers("/*/api-docs").anonymous()
-                .antMatchers("/druid/**").anonymous()
+                // swagger/druid 仅保留开发排查路径注释说明,生产应关闭;此处取消匿名避免监控台暴露
                 .antMatchers("/course/userVideo/videoTranscode").anonymous()
                 .antMatchers("/erp/call/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()
-                .headers().frameOptions().disable();
+                // 增加 nosniff,降低 MIME 嗅探导致的 XSS 风险;不影响正常业务响应
+                .headers()
+                    .contentTypeOptions()
+                    .and()
+                    .frameOptions().disable();
         httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
         // 添加JWT filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

+ 38 - 24
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -58,7 +58,11 @@ import static com.fs.his.utils.PhoneUtil.encryptPhoneOldKey;
 @Slf4j
 public class AppLoginController extends AppBaseController{
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
-    private static final String INTERNAL_LOGIN_CODE = "EdisonX!@#.+-123";
+    /**
+     * 原硬编码万能验证码已废弃(存在任意用户登录风险)。
+     * 如需内部联调,请走正常短信验证或专用开发环境配置,禁止在生产使用固定验证码。
+     */
+    private static final String INTERNAL_LOGIN_CODE = "";
     private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
     @Autowired
     private IFsUserService userService;
@@ -197,13 +201,16 @@ public class AppLoginController extends AppBaseController{
         if (!CollectionUtil.isEmpty(users)){
             return R.error("此账号已经注册");
         }
-        String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+        String smsCodeKey = "sms:code:" + phone;
+        String redisCode = redisCache.getCacheObject(smsCodeKey);
         if (StringUtils.isEmpty(redisCode)){
             return R.error("验证码已过期,请重新发送");
         }
         if (!redisCode.equals(code)) {
             return R.error("验证码错误");
         }
+        // 验证成功后立即删除验证码,防止重复使用
+        redisCache.deleteObject(smsCodeKey);
         FsUser user = new FsUser();
         // 创建新用户
         user.setPhone(phone);
@@ -501,15 +508,19 @@ public class AppLoginController extends AppBaseController{
             //如果出现了一个手机号多个用户的情况,找出登陆过app的那个用户
             user.removeIf(fsUser -> StringUtils.isEmpty(fsUser.getHistoryApp()));
         }
-        boolean isInternalLogin = INTERNAL_LOGIN_CODE.equals(code);
+        // 万能验证码已清空禁用;仅当配置了非空内部码时才生效
+        boolean isInternalLogin = StringUtils.isNotEmpty(INTERNAL_LOGIN_CODE) && INTERNAL_LOGIN_CODE.equals(code);
         if (!isInternalLogin) {
-            String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+            String smsCodeKey = "sms:code:" + phone;
+            String redisCode = redisCache.getCacheObject(smsCodeKey);
             if (StringUtils.isEmpty(redisCode)){
                 return R.error("验证码已过期,请重新发送");
             }
             if (!redisCode.equals(code)) {
                 return R.error("验证码错误");
             }
+            // 验证成功后立即删除验证码,防止重复使用
+            redisCache.deleteObject(smsCodeKey);
         }
         FsUser currentUser;
         if (CollectionUtil.isEmpty(user)) {
@@ -539,13 +550,24 @@ public class AppLoginController extends AppBaseController{
         if (CollectionUtil.isEmpty(user)){
             return R.error("此电话号码未绑定用户");
         }
-        String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+        String smsCodeKey = "sms:code:" + phone;
+        String failKey = "sms:fail:" + phone;
+        Integer failCount = redisCache.getCacheObject(failKey);
+        if (failCount != null && failCount >= 5) {
+            return R.error("验证码错误次数过多,请30分钟后再试");
+        }
+        String redisCode = redisCache.getCacheObject(smsCodeKey);
         if (StringUtils.isEmpty(redisCode)){
             return R.error("验证码已过期,请重新发送");
         }
         if (!redisCode.equals(code)) {
+            int nextFail = failCount == null ? 1 : failCount + 1;
+            redisCache.setCacheObject(failKey, nextFail, 30, TimeUnit.MINUTES);
             return R.error("验证码错误");
         }
+        // 验证成功后立即失效验证码,防止重复使用
+        redisCache.deleteObject(smsCodeKey);
+        redisCache.deleteObject(failKey);
         String password = Md5Utils.hash(newPassword);
         return userService.updatePasswordByPhone(password,encryptPhone);
     }
@@ -949,16 +971,14 @@ public class AppLoginController extends AppBaseController{
         return generateTokenAndReturn(user);
     }
 
+    /**
+     * 安全修复:原接口仅凭 userId 即可颁发 token,属于登录后门,已禁用。
+     * 保留路径避免前端 404 误判,统一返回无权限。
+     */
     @PostMapping("/loginByUserId")
     public R loginByUserId(@RequestBody FsUserLoginByWeChatParam param){
-        FsUser user = userService.selectFsUserByUserId(param.getUserId());
-        if (user == null){
-            return R.error("用户不存在");
-        }
-        if (StringUtils.isNotEmpty(param.getJpushId())){
-            updateExistingUserJpushId(user, param.getJpushId());
-        }
-        return generateTokenAndReturn(user);
+        logger.warn("已禁用的高危接口被调用: /loginByUserId, userId={}", param != null ? param.getUserId() : null);
+        return R.error(403, "该接口已禁用");
     }
 
     private FsUser createNewUser(FsUserLoginParam param) {
@@ -1006,19 +1026,13 @@ public class AppLoginController extends AppBaseController{
         return user;
     }
 
+    /**
+     * 安全修复:测试后门接口已禁用,禁止仅凭 userId 颁发 token
+     */
     @PostMapping("/loginTest")
     public R loginTest(@RequestBody String userId) {
-        FsUser user = userMapper.selectFsUserByUserId(Long.parseLong(userId));
-        if (user!=null){
-            String token = jwtUtils.generateToken(user.getUserId());
-            redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
-            Map<String, Object> map = new HashMap<>();
-            map.put("token", token);
-            map.put("user", user);
-            bindCompanyUserInfoByPhone(user);
-            return R.ok(map);
-        }
-        return R.error("用户不存在!");
+        logger.warn("已禁用的高危接口被调用: /loginTest, userId={}", userId);
+        return R.error(403, "该接口已禁用");
     }
 
     /**