Эх сурвалжийг харах

feat(security): 新增微信小程序手机号码认证功能

- 添加了基于微信手机号 Code 的认证 Provider 和 Token 类
- 实现了通过微信服务获取手机号并完成用户注册或登录逻辑
- 集成了微信 SDK 并处理了相关异常情况
- 支持用户状态检查及禁用用户的处理机制
- 提供了认证成功后构建 Spring Security 认证对象的功能

fix(util): 完善 SecurityUtils 的安全检测方法

- 增加了对 XSS 攻击字符串的识别测试用例
- 补充了 SQL 注入检测在宽松和严格模式下的验证逻辑
- 添加了 isSafeInput 方法用于综合判断输入安全性
- 编写了单元测试确保 containsXss 和 containsSqlInjection 方法正确运行

feat(controller): 增加企业信息列表查询接口

- 在 FirmInfoController 中新增 /list 接口支持获取所有企业信息
- 使用 converter 将实体列表转换为 VO 列表返回给前端
- 添加了权限注解以限制访问该接口所需权限

refactor(converter): 扩展 FirmInfoConverter 转换能力

- 新增将
wzq 16 цаг өмнө
parent
commit
4352027e94

+ 12 - 0
src/main/java/com/zsElectric/boot/business/controller/FirmInfoController.java

@@ -1,6 +1,7 @@
 package com.zsElectric.boot.business.controller;
 
 import com.zsElectric.boot.business.service.FirmInfoService;
+import com.zsElectric.boot.business.converter.FirmInfoConverter;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -18,6 +19,8 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import jakarta.validation.Valid;
 
+import java.util.List;
+
 /**
  * 企业信息前端控制层
  *
@@ -31,6 +34,7 @@ import jakarta.validation.Valid;
 public class FirmInfoController  {
 
     private final FirmInfoService firmInfoService;
+    private final FirmInfoConverter firmInfoConverter;
 
     @Operation(summary = "企业信息分页列表")
     @GetMapping("/page")
@@ -40,6 +44,14 @@ public class FirmInfoController  {
         return PageResult.success(result);
     }
 
+    @Operation(summary = "企业信息列表")
+    @GetMapping("/list")
+    @PreAuthorize("@ss.hasPerm('business:firmInfo:query')")
+    public Result<List<FirmInfoVO>> getFirmInfoList(FirmInfoQuery queryParams ) {
+        List<FirmInfoVO> result = firmInfoConverter.toVO(firmInfoService.list());
+        return Result.success(result);
+    }
+
     @Operation(summary = "新增企业信息")
     @PostMapping
     @PreAuthorize("@ss.hasPerm('business:firmInfo:add')")

+ 19 - 0
src/main/java/com/zsElectric/boot/business/converter/FirmInfoConverter.java

@@ -4,6 +4,9 @@ import org.mapstruct.Mapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zsElectric.boot.business.model.entity.FirmInfo;
 import com.zsElectric.boot.business.model.form.FirmInfoForm;
+import com.zsElectric.boot.business.model.vo.FirmInfoVO;
+
+import java.util.List;
 
 /**
  * 企业信息对象转换器
@@ -17,4 +20,20 @@ public interface FirmInfoConverter{
     FirmInfoForm toForm(FirmInfo entity);
 
     FirmInfo toEntity(FirmInfoForm formData);
+
+    /**
+     * 实体转换为VO
+     *
+     * @param entity 实体对象
+     * @return VO对象
+     */
+    FirmInfoVO toVO(FirmInfo entity);
+
+    /**
+     * 实体列表转换为VO列表
+     *
+     * @param entities 实体对象列表
+     * @return VO对象列表
+     */
+    List<FirmInfoVO> toVO(List<FirmInfo> entities);
 }

+ 64 - 0
src/main/java/com/zsElectric/boot/security/model/WxMiniAppPhoneCodeAuthenticationToken.java

@@ -0,0 +1,64 @@
+package com.zsElectric.boot.security.model;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.io.Serial;
+import java.util.Collection;
+
+/**
+ * 微信小程序手机号Code认证Token(新版接口)
+ *
+ * @author Ray.Hao
+ * @since 2.0.0
+ */
+public class WxMiniAppPhoneCodeAuthenticationToken extends AbstractAuthenticationToken {
+    @Serial
+    private static final long serialVersionUID = 623L;
+    
+    private final Object principal; // phoneCode
+
+    /**
+     * 微信小程序手机号Code认证Token (未认证)
+     *
+     * @param phoneCode 微信手机号code
+     */
+    public WxMiniAppPhoneCodeAuthenticationToken(String phoneCode) {
+        super(null);
+        this.principal = phoneCode;
+        this.setAuthenticated(false);
+    }
+
+    /**
+     * 微信小程序手机号Code认证Token (已认证)
+     *
+     * @param principal 用户信息
+     * @param authorities 授权信息
+     */
+    public WxMiniAppPhoneCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
+        super(authorities);
+        this.principal = principal;
+        super.setAuthenticated(true);
+    }
+
+    /**
+     * 创建已认证的Token
+     *
+     * @param principal   用户信息
+     * @param authorities 授权信息
+     * @return 已认证的Token
+     */
+    public static WxMiniAppPhoneCodeAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
+        return new WxMiniAppPhoneCodeAuthenticationToken(principal, authorities);
+    }
+
+    @Override
+    public Object getCredentials() {
+        return null;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return this.principal;
+    }
+}

+ 94 - 0
src/main/java/com/zsElectric/boot/security/provider/WxMiniAppPhoneCodeAuthenticationProvider.java

@@ -0,0 +1,94 @@
+package com.zsElectric.boot.security.provider;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.zsElectric.boot.security.model.SysUserDetails;
+import com.zsElectric.boot.security.model.UserAuthCredentials;
+import com.zsElectric.boot.security.model.WxMiniAppPhoneCodeAuthenticationToken;
+import com.zsElectric.boot.system.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+/**
+ * 微信小程序手机号Code认证Provider(新版接口)
+ *
+ * @author Ray.Hao
+ * @since 2.0.0
+ */
+@Slf4j
+public class WxMiniAppPhoneCodeAuthenticationProvider implements AuthenticationProvider {
+
+    private final UserService userService;
+    private final WxMaService wxMaService;
+
+    public WxMiniAppPhoneCodeAuthenticationProvider(UserService userService, WxMaService wxMaService) {
+        this.userService = userService;
+        this.wxMaService = wxMaService;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        WxMiniAppPhoneCodeAuthenticationToken authenticationToken = (WxMiniAppPhoneCodeAuthenticationToken) authentication;
+        String phoneCode = (String) authenticationToken.getPrincipal();
+
+        // 1. 通过phoneCode获取手机号信息(新版接口)
+        WxMaPhoneNumberInfo phoneNumberInfo;
+        try {
+            phoneNumberInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode);
+        } catch (WxErrorException e) {
+            log.error("获取微信手机号失败", e);
+            throw new CredentialsExpiredException("获取手机号失败,code无效或已过期");
+        }
+
+        if (phoneNumberInfo == null || StrUtil.isBlank(phoneNumberInfo.getPhoneNumber())) {
+            throw new CredentialsExpiredException("获取手机号失败");
+        }
+
+        String phoneNumber = phoneNumberInfo.getPhoneNumber();
+        log.info("通过code获取到手机号: {}", phoneNumber);
+
+        // 2. 根据手机号查询用户,不存在则创建新用户
+        UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber);
+
+        if (userAuthCredentials == null) {
+            // 用户不存在,注册新用户(仅手机号,不绑定openId)
+            boolean registered = userService.registerUserByMobileAndOpenId(phoneNumber, null);
+            if (!registered) {
+                throw new UsernameNotFoundException("用户注册失败");
+            }
+            // 重新获取用户信息
+            userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber);
+            
+            if (userAuthCredentials == null) {
+                throw new UsernameNotFoundException("用户注册失败");
+            }
+        }
+
+        // 3. 检查用户状态
+        if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
+            throw new DisabledException("用户已被禁用");
+        }
+
+        // 4. 构建认证后的用户详情
+        SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
+
+        // 5. 创建已认证的Token
+        return WxMiniAppPhoneCodeAuthenticationToken.authenticated(
+                userDetails,
+                userDetails.getAuthorities()
+        );
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return WxMiniAppPhoneCodeAuthenticationToken.class.isAssignableFrom(authentication);
+    }
+}

+ 60 - 0
src/test/java/com/zsElectric/boot/common/util/SecurityUtilsTest.java

@@ -0,0 +1,60 @@
+package com.zsElectric.boot.common.util;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * SecurityUtils 测试类
+ */
+class SecurityUtilsTest {
+
+    @Test
+    void testContainsXss() {
+        // 测试明显的 XSS 攻击
+        assertTrue(SecurityUtils.containsXss("<script>alert('XSS')</script>"));
+        assertTrue(SecurityUtils.containsXss("javascript:alert('XSS')"));
+        assertTrue(SecurityUtils.containsXss("onerror=alert('XSS')"));
+        
+        // 测试正常输入不应该被误判
+        assertFalse(SecurityUtils.containsXss("username"));
+        assertFalse(SecurityUtils.containsXss("select-user"));
+        assertFalse(SecurityUtils.containsXss("user selection"));
+    }
+
+    @Test
+    void testContainsSqlInjection() {
+        // 测试宽松模式下的 SQL 注入检测
+        SecurityUtils.setSqlStrictMode(false);
+        
+        // 明显的 SQL 注入应该被检测到
+        assertTrue(SecurityUtils.containsSqlInjection("select * from users"));
+        assertTrue(SecurityUtils.containsSqlInjection("insert into users"));
+        assertTrue(SecurityUtils.containsSqlInjection("delete from users"));
+        assertTrue(SecurityUtils.containsSqlInjection("union select"));
+        
+        // 包含 SQL 关键词但不是攻击的正常输入不应该被误判
+        assertFalse(SecurityUtils.containsSqlInjection("username")); // 包含 "select" 的用户名
+        assertFalse(SecurityUtils.containsSqlInjection("user selection")); // 包含 "select" 的正常文本
+        assertFalse(SecurityUtils.containsSqlInjection("inserted value")); // 包含 "insert" 的正常文本
+        assertFalse(SecurityUtils.containsSqlInjection("deleted item")); // 包含 "delete" 的正常文本
+        
+        // 测试严格模式下的 SQL 注入检测
+        SecurityUtils.setSqlStrictMode(true);
+        
+        // 在严格模式下,即使是正常输入也可能被误判
+        // 但我们主要关注的是默认的宽松模式能正常工作
+        SecurityUtils.setSqlStrictMode(false);
+    }
+
+    @Test
+    void testIsSafeInput() {
+        // 安全的输入
+        assertTrue(SecurityUtils.isSafeInput("normal_username"));
+        assertTrue(SecurityUtils.isSafeInput("user123"));
+        assertTrue(SecurityUtils.isSafeInput("test@example.com"));
+        
+        // 不安全的输入
+        assertFalse(SecurityUtils.isSafeInput("<script>alert('XSS')</script>"));
+        assertFalse(SecurityUtils.isSafeInput("select * from users"));
+    }
+}