27-AuthService登录链路
27-AuthService登录的完整链路密码登录不只是比对密码——查用户、禁用检查、双源失败计数、锁定判断、SM3salt哈希、会话数限制、踢最早会话、签发双令牌、记登录日志。245行AuthService把这些串成一条链。这篇逐段拆loginByPassword和它的七个辅助方法。文章目录27-AuthService登录的完整链路一、loginByPassword主链二、双源失败计数内存数据库三、SM3salt哈希四、validatePasswordPolicy五项复杂度五、enforceSessionLimit会话数与踢最早六、refreshToken三查旋转七、四种登录方式源码browise-platform/src/main/java/com/browise/platform/auth/AuthService.java245行一、loginByPassword主链publicResultloginByPassword(Stringaccount,Stringpassword,Stringip){// ①查用户SysUseruseruserMapper.selectByAccount(account);if(usernull)returnResult.error(账号不存在);// ②禁用检查if(0.equals(user.getEnabled()))returnResult.error(账号已禁用);// ③双源失败计数intdbFailscountRecentFailures(user.getPsnId());// 库里最近N分钟inttotalFailsMath.max(dbFails,loginFailCount.getOrDefault(failKey,0));// 内存累计if(totalFailsauthProperties.getMaxLoginAttempts()){returnResult.error(账号已锁定请X分钟后重试);}// ④SM3salt哈希比对StringhashedInputhashPassword(password,user.getSalt());if(!hashedInput.equalsIgnoreCase(user.getPsnPwd())){loginFailCount.put(failKey,totalFails1);// 内存1recordLoginAttempt(user,ip,false);// 库里记失败returnResult.error(密码错误);}// ⑤成功——清计数记日志发令牌loginFailCount.remove(failKey);recordLoginAttempt(user,ip,true);returnbuildLoginResult(user,ip);}二、双源失败计数内存数据库privatefinalMapString,IntegerloginFailCountnewConcurrentHashMap();// 内存privateintcountRecentFailures(StringpsnId){// 数据库longsinceSystem.currentTimeMillis()-failLockMinutes*60000L;SELECTCOUNT(*)FROMSYS_LOGIN_LOGWHEREPSN_ID?ANDONLINE_FLAG0ANDLOGIN_TIME?// 失败记录时间窗口内}为什么要两个源——源计数快重启不丢跨实例内存Map✓微秒✗清零✗各自计数SYS_LOGIN_LOG查询慢一次SQL✓✓Math.max(dbFails, 内存值)——取大者。攻击者碰上重启内存清零库里的失败记录还在锁多实例部署时各自内存不共享但都查同一个库。双源互为补丁成本是每次登录多一次COUNT查询——登录是低频操作这点开销无所谓。锁定时间窗failLockMinutes——只在窗口内的失败计数超窗自动解锁不用显式解锁操作时间到了count自然归零。三、SM3salt哈希publicStringhashPassword(StringplainPassword,Stringsalt){if(salt!null!salt.isEmpty()){returnsm3Hash.hashHex(plainPasswordsalt);// 拼盐后SM3}returnsm3Hash.hashHex(plainPassword);// 无盐兼容}publicStringgenerateSalt(){byte[]bytesnewbyte[16];// 16字节随机盐RANDOM.nextBytes(bytes);// SecureRandom——密码学安全// 转hex存库}SM3是国密摘要算法对标SHA-256——政务合规要求。拼接顺序固定passwordsalt——验证侧必须一致hashPassword同时服务注册/重置/验证三个场景拼法写死在一处。无盐分支是兼容旧数据——系统迭代前期的用户密码没盐迁移过来直接比对无盐哈希。新密码resetPwd/新增用户全部generateSalt带盐哈希。渐进迁移不强制全员改密。equalsIgnoreCase——hex大小写都认SM3实现可能输出大写hex库里存小写。四、validatePasswordPolicy五项复杂度if(password.length()p.getMinLength())return密码长度不能少于X位;if(p.isRequireUpper()!password.matches(.*[A-Z].*))return密码必须包含大写字母;if(p.isRequireLower()!password.matches(.*[a-z].*))return密码必须包含小写字母;if(p.isRequireDigit()!password.matches(.*\\d.*))return密码必须包含数字;if(p.isRequireSpecial()!password.matches(.*[!#$%...].*))return密码必须包含特殊字符;五项全部可配置开关AuthProperties.PasswordPolicybrowise.auth.password-policy.*前缀——政务等保要求与用户体验之间按项目调。返回错误消息字符串null通过而不是boolean——调用方直接把消息显示给用户不用再查是哪项失败。五、enforceSessionLimit会话数与踢最早privatevoidenforceSessionLimit(StringpsnId,intmaxSessions,booleankickOldest){// 在线数 SYS_LOGIN_LOG里ONLINE_FLAG1且LOGOUT_TIME IS NULL的记录数if(在线数maxSessions){if(kickOldest){// 踢最早登录的会话——按LOGIN_TIME ASC取第一条置离线UPDATESYS_LOGIN_LOGSETONLINE_FLAG0,LOGOUT_TIME?WHERELOG_ID(SELECTLOG_IDFROM(SELECTLOG_IDFROMSYS_LOGIN_LOGWHEREPSN_ID?ANDONLINE_FLAG1ANDLOGOUT_TIMEISNULLORDERBYLOGIN_TIMEASC)WHEREROWNUM1)}else{thrownewRuntimeException(登录设备已达上限maxSessions);}}}会话的载体是SYS_LOGIN_LOG表——ONLINE_FLAG‘1’LOGOUT_TIME空一个活会话。不是内存SessionRegistry——重启不丢会话记录虽然token本身是无状态的会话数限制需要状态落库最稳。踢最早的ROWNUM子查询——Oracle取最早一条的三层写法内层排序中层取ROWNUM1外层UPDATE。注意这是Oracle方言——MySQL部署时这条SQL要适配ROW_NUMBER() OVER。会话踢除只改数据库标记——被踢设备的token依然有效JWT无状态要等它下次请求时黑名单校验/过期才真失效。完整踢除置离线iat时间点拉黑当前实现靠token自然过期兜底access最多2小时。kickOldestfalse时抛异常——拒绝第N个设备登录的严格模式。两种模式配置切换session-policy.kick-oldest。六、refreshToken三查旋转publicResultrefreshToken(StringoldRefreshToken){// ①格式与类型if(!jwtUtil.validate(oldRefreshToken)||!jwtUtil.isRefreshToken(oldRefreshToken))returnResult.error(refresh token无效或已过期);// ②用户实时状态refresh 7天里用户可能被禁用SysUseruseruserMapper.selectById(psnId);if(usernull||0.equals(user.getEnabled()))returnResult.error(...);// ③旧refresh进黑名单——一次性jwtBlacklist.blockRefreshToken(oldRefreshToken);// ④签发全新的accessrefresh对return新双令牌;}③是旋转rotation——每个refresh只用一次用完拉黑发新的。防的是refresh被盗后的无限续期攻击者偷到refresh①用户本人也在用——先到者刷新成功后到者拿着已拉黑的refresh被拒——被盗这件事在下次合法使用时自动暴露用户刷新失败有异常。第25篇说当前实现refresh用满7天——看了AuthService的完整实现要修正refreshToken端点确实是旋转模式blockRefreshToken双新令牌。JwtUtil.refreshAccessToken不拉黑的简版是留给内部信任场景的工具方法。七、四种登录方式loginByPassword(account,password,ip)// 密码——完整链路loginByPhone(phone,smsCode,ip)// 手机号——短信码码校验在Controller层loginBySso(account,ssoToken,ip)// SSO——对接统一门户loginByCa(certificateId,ip)// CA证书——预留后三种跳过密码与失败计数——认证凭据在外部系统短信网关/门户/CA这里只做凭据对应用户存在且启用。四种方式汇入同一个buildLoginResult——会话限制、双令牌签发、登录日志对四种登录一视同仁。✅ 亮点loginByPassword主链的五步、双源失败计数互为补丁的设计、SM3salt与无盐渐进迁移、会话数以SYS_LOGIN_LOG为载体重启不丢、refreshToken旋转模式防被盗无限续期、四种登录方式汇入同一签发出口。适合做认证模块的人。扩展方向第25/26篇令牌与过滤器、第28篇JwtBlacklist、第34篇安全评审。