Procházet zdrojové kódy

登录接口问题

wwh před 3 týdny
rodič
revize
4a164660d2

+ 2 - 0
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/AuthController.java

@@ -57,6 +57,7 @@ public class AuthController {
57 57
     @PostMapping("/sms/login")
58 58
     @Operation(summary = "短信验证码登录",
59 59
             description = "校验验证码后登录或自动注册,返回 JWT 及用户基本信息。"
60
+                    + "userType 仅在首次注册时写入,老用户须从对应入口(enterprise/worker)登录。"
60 61
                     + "登录成功后建议调用 GET /api/v1/mp/getInfo 查询登记状态。")
61 62
     public R<LoginResponse> loginBySms(@Validated @RequestBody SmsLoginRequest request) {
62 63
         return R.ok(authService.loginBySms(request));
@@ -65,6 +66,7 @@ public class AuthController {
65 66
     @PostMapping("/wechat/login")
66 67
     @Operation(summary = "微信小程序 code 登录",
67 68
             description = "使用 wx.login 获取的 code 换取 openid 并登录。"
69
+                    + "userType 仅在首次注册时写入,老用户须从对应入口登录。"
68 70
                     + "Mock 模式(fe.wechat.miniapp.mock-enabled=true)下 code 可为任意非空字符串。"
69 71
                     + "短信用户与微信用户一期未做账号合并,openid 与手机号分别建号。")
70 72
     public R<LoginResponse> loginByWechat(@Validated @RequestBody WechatLoginRequest request) {

+ 1 - 1
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/ProfileController.java

@@ -69,6 +69,6 @@ public class ProfileController {
69 69
     @Operation(summary = "查询当前用户登记状态与详情")
70 70
     public R<MpUserInfoResponse> getInfo() {
71 71
         LoginUser loginUser = LoginUserHolder.require();
72
-        return R.ok(profileService.getInfo(loginUser.getUserId(), loginUser.getUserType()));
72
+        return R.ok(profileService.getInfo(loginUser.getUserId()));
73 73
     }
74 74
 }

+ 9 - 2
huimv-employment/fe-api/src/main/java/com/huimv/employment/security/JwtAuthFilter.java

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
4 4
 import com.huimv.employment.common.exception.BizException;
5 5
 import com.huimv.employment.common.exception.ErrorCode;
6 6
 import com.huimv.employment.common.web.R;
7
+import com.huimv.employment.service.auth.AuthUserValidator;
7 8
 import com.huimv.employment.service.auth.JwtTokenService;
8 9
 import org.springframework.core.Ordered;
9 10
 import org.springframework.core.annotation.Order;
@@ -22,6 +23,7 @@ import java.nio.charset.StandardCharsets;
22 23
 
23 24
 /**
24 25
  * JWT 鉴权过滤器:保护 /api/v1/mp/** 接口(auth、health 等匿名路径除外)。
26
+ * <p>解析 JWT 后查库校验账号状态,并以数据库 {@code user_type} 作为鉴权身份。</p>
25 27
  */
26 28
 @Component
27 29
 @Order(Ordered.HIGHEST_PRECEDENCE + 20)
@@ -40,10 +42,14 @@ public class JwtAuthFilter extends OncePerRequestFilter {
40 42
     };
41 43
 
42 44
     private final JwtTokenService jwtTokenService;
45
+    private final AuthUserValidator authUserValidator;
43 46
     private final ObjectMapper objectMapper;
44 47
 
45
-    public JwtAuthFilter(JwtTokenService jwtTokenService, ObjectMapper objectMapper) {
48
+    public JwtAuthFilter(JwtTokenService jwtTokenService,
49
+                         AuthUserValidator authUserValidator,
50
+                         ObjectMapper objectMapper) {
46 51
         this.jwtTokenService = jwtTokenService;
52
+        this.authUserValidator = authUserValidator;
47 53
         this.objectMapper = objectMapper;
48 54
     }
49 55
 
@@ -72,7 +78,8 @@ public class JwtAuthFilter extends OncePerRequestFilter {
72 78
                 return;
73 79
             }
74 80
             JwtTokenService.TokenClaims claims = jwtTokenService.parseToken(authorization.substring(7));
75
-            LoginUserHolder.set(new LoginUser(claims.getUserId(), claims.getUserType()));
81
+            AuthUserValidator.ValidatedUser user = authUserValidator.validate(claims.getUserId());
82
+            LoginUserHolder.set(new LoginUser(user.getUserId(), user.getUserType()));
76 83
             filterChain.doFilter(request, response);
77 84
         } catch (BizException ex) {
78 85
             writeError(response, ex.getCode(), ex.getMessage());

+ 1 - 1
huimv-employment/fe-api/src/main/java/com/huimv/employment/security/LoginUser.java

@@ -1,7 +1,7 @@
1 1
 package com.huimv.employment.security;
2 2
 
3 3
 /**
4
- * 当前登录用户上下文,由 JWT 过滤器解析后写入。
4
+ * 当前登录用户上下文,由 JWT 过滤器解析 Token 查库写入。
5 5
  */
6 6
 public class LoginUser {
7 7
 

+ 1 - 0
huimv-employment/fe-api/src/main/resources/application.yml

@@ -44,6 +44,7 @@ fe:
44 44
     mock-code: "123456"
45 45
     code-ttl-seconds: 300
46 46
     send-cooldown-seconds: 60
47
+    max-verify-attempts: 5
47 48
     aliyun:
48 49
       access-key-id: ${ALIYUN_SMS_ACCESS_KEY_ID:}
49 50
       access-key-secret: ${ALIYUN_SMS_ACCESS_KEY_SECRET:}

+ 10 - 0
huimv-employment/fe-common/src/main/java/com/huimv/employment/common/constant/RedisKeys.java

@@ -26,6 +26,9 @@ public final class RedisKeys {
26 26
     /** 短信发送冷却 Key 前缀,完整 Key:fe:sms:cd:{mobileHash} */
27 27
     public static final String SMS_SEND_COOLDOWN = "fe:sms:cd:";
28 28
 
29
+    /** 短信验证码校验失败计数 Key 前缀,完整 Key:fe:sms:attempts:{mobileHash} */
30
+    public static final String SMS_VERIFY_ATTEMPTS = "fe:sms:attempts:";
31
+
29 32
     /** 账密登录失败计数 Key 前缀,完整 Key:fe:login:fail:{accountKey} */
30 33
     public static final String LOGIN_FAIL = "fe:login:fail:";
31 34
 
@@ -49,6 +52,13 @@ public final class RedisKeys {
49 52
         return SMS_SEND_COOLDOWN + mobileHash;
50 53
     }
51 54
 
55
+    /**
56
+     * 组装验证码校验失败计数 Redis Key。
57
+     */
58
+    public static String smsVerifyAttemptKey(String mobileHash) {
59
+        return SMS_VERIFY_ATTEMPTS + mobileHash;
60
+    }
61
+
52 62
     /**
53 63
      * 组装账密登录失败计数 Redis Key。
54 64
      *

+ 3 - 0
huimv-employment/fe-common/src/main/java/com/huimv/employment/common/exception/ErrorCode.java

@@ -78,6 +78,9 @@ public enum ErrorCode {
78 78
     /** 账密:登录失败次数过多已锁定 */
79 79
     LOGIN_LOCKED(40304, "登录失败次数过多,请稍后再试"),
80 80
 
81
+    /** 登录:请求身份与账号已绑定类型不一致 */
82
+    USER_TYPE_MISMATCH(40305, "账号类型不匹配,请从正确的入口登录"),
83
+
81 84
     /** 通用:未预期的系统异常 */
82 85
     INTERNAL_ERROR(500, "系统繁忙,请稍后再试");
83 86
 

+ 11 - 0
huimv-employment/fe-integration/src/main/java/com/huimv/employment/integration/sms/SmsProperties.java

@@ -45,6 +45,9 @@ public class SmsProperties {
45 45
     /** 非 Mock 模式下随机验证码的位数,默认 6 位纯数字 */
46 46
     private int codeLength = 6;
47 47
 
48
+    /** 同一验证码允许的最大校验失败次数,超出后作废验证码 */
49
+    private int maxVerifyAttempts = 5;
50
+
48 51
     /** 阿里云短信配置,仅 mock-enabled=false 时生效 */
49 52
     private Aliyun aliyun = new Aliyun();
50 53
 
@@ -88,6 +91,14 @@ public class SmsProperties {
88 91
         this.codeLength = codeLength;
89 92
     }
90 93
 
94
+    public int getMaxVerifyAttempts() {
95
+        return maxVerifyAttempts;
96
+    }
97
+
98
+    public void setMaxVerifyAttempts(int maxVerifyAttempts) {
99
+        this.maxVerifyAttempts = maxVerifyAttempts;
100
+    }
101
+
91 102
     public Aliyun getAliyun() {
92 103
         return aliyun;
93 104
     }

+ 11 - 6
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/auth/AuthService.java

@@ -67,9 +67,7 @@ public class AuthService {
67 67
 
68 68
         FeUser user = findOrCreateByMobile(mobileEnc, mobileMask, request.getUserType());
69 69
         ensureUserActive(user);
70
-        if (!request.getUserType().equals(user.getUserType())) {
71
-            user.setUserType(request.getUserType());
72
-        }
70
+        ensureUserTypeMatch(user, request.getUserType());
73 71
         user.setMobileMask(mobileMask);
74 72
         touchLogin(user);
75 73
         feUserMapper.updateById(user);
@@ -83,9 +81,7 @@ public class AuthService {
83 81
 
84 82
         FeUser user = findOrCreateByOpenid(session, request.getUserType());
85 83
         ensureUserActive(user);
86
-        if (!request.getUserType().equals(user.getUserType())) {
87
-            user.setUserType(request.getUserType());
88
-        }
84
+        ensureUserTypeMatch(user, request.getUserType());
89 85
         user.setUnionid(session.getUnionid());
90 86
         user.setSessionKey(session.getSessionKey());
91 87
         touchLogin(user);
@@ -192,6 +188,15 @@ public class AuthService {
192 188
         }
193 189
     }
194 190
 
191
+    /**
192
+     * 用户身份类型仅在首次创建时确定,后续登录不可切换企业主/临时工。
193
+     */
194
+    private void ensureUserTypeMatch(FeUser user, String requestedUserType) {
195
+        if (!requestedUserType.equals(user.getUserType())) {
196
+            throw new BizException(ErrorCode.USER_TYPE_MISMATCH);
197
+        }
198
+    }
199
+
195 200
     private LoginResponse buildLoginResponse(FeUser user) {
196 201
         JwtTokenService.TokenIssueResult token = jwtTokenService.issueToken(user.getId(), user.getUserType());
197 202
         LoginResponse response = new LoginResponse();

+ 4 - 5
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/auth/PasswordAuthService.java

@@ -23,6 +23,8 @@ import java.util.regex.Pattern;
23 23
 public class PasswordAuthService {
24 24
 
25 25
     private static final String STATUS_DISABLED = "disabled";
26
+    private static final String RESET_PASSWORD_DENIED_MSG =
27
+            "无法重置密码,请确认手机号与验证码,或使用验证码登录后在个人中心设置";
26 28
     private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
27 29
 
28 30
     private final FeUserMapper feUserMapper;
@@ -114,13 +116,10 @@ public class PasswordAuthService {
114 116
         FeUser user = feUserMapper.selectOne(new LambdaQueryWrapper<FeUser>()
115 117
                 .eq(FeUser::getMobileEnc, mobileEnc)
116 118
                 .last("LIMIT 1"));
117
-        if (user == null) {
118
-            throw new BizException(ErrorCode.BAD_REQUEST, "该手机号尚未注册,请先使用验证码登录");
119
+        if (user == null || !StringUtils.hasText(user.getPasswordHash())) {
120
+            throw new BizException(ErrorCode.BAD_REQUEST, RESET_PASSWORD_DENIED_MSG);
119 121
         }
120 122
         ensureUserActive(user);
121
-        if (!StringUtils.hasText(user.getPasswordHash())) {
122
-            throw new BizException(ErrorCode.PASSWORD_NOT_SET, "您尚未设置登录密码,请先验证码登录后在个人中心设置");
123
-        }
124 123
 
125 124
         String rawPassword = decryptPassword(request.getEncryptedPassword());
126 125
         String rawConfirm = decryptPassword(request.getEncryptedConfirmPassword());

+ 2 - 1
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/auth/ProfileService.java

@@ -32,12 +32,13 @@ public class ProfileService {
32 32
         this.workerService = workerService;
33 33
     }
34 34
 
35
-    public MpUserInfoResponse getInfo(Long userId, String userType) {
35
+    public MpUserInfoResponse getInfo(Long userId) {
36 36
         FeUser user = feUserMapper.selectById(userId);
37 37
         if (user == null) {
38 38
             throw new BizException(ErrorCode.UNAUTHORIZED);
39 39
         }
40 40
 
41
+        String userType = user.getUserType();
41 42
         MpUserInfoResponse response = new MpUserInfoResponse();
42 43
         response.setUserType(userType);
43 44
         response.setMobileMask(user.getMobileMask());

+ 24 - 1
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/sms/SmsCodeService.java

@@ -89,12 +89,35 @@ public class SmsCodeService {
89 89
 
90 90
         String mobileHash = MobileUtils.hash(mobile);
91 91
         String codeKey = RedisKeys.smsCodeKey(mobileHash);
92
+        String attemptKey = RedisKeys.smsVerifyAttemptKey(mobileHash);
93
+
94
+        int maxAttempts = Math.max(1, smsProperties.getMaxVerifyAttempts());
95
+        String attemptValue = redisTemplate.opsForValue().get(attemptKey);
96
+        int failedAttempts = 0;
97
+        if (StringUtils.hasText(attemptValue)) {
98
+            try {
99
+                failedAttempts = Integer.parseInt(attemptValue);
100
+            } catch (NumberFormatException ignored) {
101
+                failedAttempts = 0;
102
+            }
103
+        }
104
+        if (failedAttempts >= maxAttempts) {
105
+            redisTemplate.delete(codeKey);
106
+            redisTemplate.delete(attemptKey);
107
+            throw new BizException(ErrorCode.SMS_CODE_INVALID);
108
+        }
109
+
92 110
         String cachedCode = redisTemplate.opsForValue().get(codeKey);
93 111
         if (!StringUtils.hasText(cachedCode) || !cachedCode.equals(code.trim())) {
112
+            redisTemplate.opsForValue().set(
113
+                    attemptKey,
114
+                    String.valueOf(failedAttempts + 1),
115
+                    smsProperties.getCodeTtlSeconds(),
116
+                    TimeUnit.SECONDS);
94 117
             throw new BizException(ErrorCode.SMS_CODE_INVALID);
95 118
         }
96
-        // 一次性验证码:校验通过后删除
97 119
         redisTemplate.delete(codeKey);
120
+        redisTemplate.delete(attemptKey);
98 121
     }
99 122
 
100 123
     /**