@@ -41,6 +41,7 @@ public class AuthService {
4141 private final CurrentUser currentUser ;
4242 private final SliderVerificationService sliderVerificationService ;
4343 private final LoginLockoutService loginLockoutService ;
44+ private final AccountDeviceService accountDeviceService ;
4445 private final MediaService mediaService ;
4546 private final EmailValidator emailValidator ;
4647 private final EmailCodeService emailCodeService ;
@@ -53,6 +54,7 @@ public AuthService(UserMapper userMapper,
5354 CurrentUser currentUser ,
5455 SliderVerificationService sliderVerificationService ,
5556 LoginLockoutService loginLockoutService ,
57+ AccountDeviceService accountDeviceService ,
5658 MediaService mediaService ,
5759 EmailValidator emailValidator ,
5860 EmailCodeService emailCodeService ) {
@@ -64,6 +66,7 @@ public AuthService(UserMapper userMapper,
6466 this .currentUser = currentUser ;
6567 this .sliderVerificationService = sliderVerificationService ;
6668 this .loginLockoutService = loginLockoutService ;
69+ this .accountDeviceService = accountDeviceService ;
6770 this .mediaService = mediaService ;
6871 this .emailValidator = emailValidator ;
6972 this .emailCodeService = emailCodeService ;
@@ -119,7 +122,10 @@ public AuthResponse register(RegisterRequest req) {
119122
120123 public AuthResponse login (LoginRequest req ) {
121124 String ipSeg = currentIpKeySegment ();
125+ // 设备维度(指纹经网关格式校验透传;缺失/非法 → null 不参与设备锁,仅 IP 兜底)
126+ String fp = currentDeviceFingerprint ();
122127 loginLockoutService .assertNotLocked (ipSeg );
128+ loginLockoutService .assertNotDeviceLocked (fp );
123129 sliderVerificationService .verifyAndConsume (req .getSliderChallengeId ());
124130
125131 User user = userMapper .selectOne (Wrappers .lambdaQuery (User .class ).eq (User ::getUsername , req .getAccount ()));
@@ -128,6 +134,7 @@ public AuthResponse login(LoginRequest req) {
128134 }
129135 if (user == null ) {
130136 loginLockoutService .recordPasswordFailure (ipSeg );
137+ loginLockoutService .recordDevicePasswordFailure (fp );
131138 throw new BizException (clientErrorCode (), "账号或密码错误" );
132139 }
133140
@@ -136,14 +143,19 @@ public AuthResponse login(LoginRequest req) {
136143 }
137144 if ("BANNED" .equals (user .getStatus ())) {
138145 loginLockoutService .recordPasswordFailure (ipSeg );
146+ loginLockoutService .recordDevicePasswordFailure (fp );
139147 throw new BizException (4011 , "账号已被封禁" );
140148 }
141149 if (!passwordEncoder .matches (req .getPassword (), user .getPasswordHash ())) {
142150 loginLockoutService .recordPasswordFailure (ipSeg );
151+ loginLockoutService .recordDevicePasswordFailure (fp );
143152 throw new BizException (clientErrorCode (), "账号或密码错误" );
144153 }
145154
146155 loginLockoutService .clearFailures (ipSeg );
156+ loginLockoutService .clearDeviceFailures (fp );
157+ // 第 3 层:指纹记为该账号的已知设备(fail-open,Redis 故障不阻断登录)
158+ accountDeviceService .recordLogin (user .getId (), fp );
147159
148160 String accessToken = jwtService .generateAccessToken (user .getId (), user .getRole ().name ());
149161 String refreshToken = jwtService .generateRefreshToken (user .getId ());
@@ -314,5 +326,21 @@ private String currentIpKeySegment() {
314326 String ip = ClientIpResolver .resolve (request );
315327 return ClientIpResolver .toRedisKeySegment (ip );
316328 }
329+
330+ /** 设备指纹 key 片段:网关已正则校验并透传 X-Device-Fingerprint;此处防御性复检,
331+ * 缺失/非法一律返回 null(该请求不参与设备锁 / 设备记录,仅 IP 维度兜底)。 */
332+ private String currentDeviceFingerprint () {
333+ var attrs = RequestContextHolder .getRequestAttributes ();
334+ if (!(attrs instanceof ServletRequestAttributes sra )) {
335+ return null ;
336+ }
337+ String raw = sra .getRequest ().getHeader ("X-Device-Fingerprint" );
338+ if (raw == null ) {
339+ return null ;
340+ }
341+ String fp = raw .trim ();
342+ // 与网关一致:32 位 hex 为常态;限 16~64 位字母数字-,防注入与超长 Redis key
343+ return fp .matches ("^[A-Za-z0-9-]{16,64}$" ) ? fp : null ;
344+ }
317345}
318346
0 commit comments