27-AuthService登录链路

27-AuthService:登录的完整链路

密码登录不只是"比对密码"------查用户、禁用检查、双源失败计数、锁定判断、SM3+salt哈希、会话数限制、踢最早会话、签发双令牌、记登录日志。245行AuthService把这些串成一条链。这篇逐段拆loginByPassword和它的七个辅助方法。

文章目录

源码:browise-platform/src/main/java/com/browise/platform/auth/AuthService.java(245行)


一、loginByPassword主链

java 复制代码
public Result loginByPassword(String account, String password, String ip) {
    // ①查用户
    SysUser user = userMapper.selectByAccount(account);
    if (user == null) return Result.error("账号不存在");

    // ②禁用检查
    if ("0".equals(user.getEnabled())) return Result.error("账号已禁用");

    // ③双源失败计数
    int dbFails = countRecentFailures(user.getPsnId());               // 库里最近N分钟
    int totalFails = Math.max(dbFails,
            loginFailCount.getOrDefault(failKey, 0));                 // 内存累计
    if (totalFails >= authProperties.getMaxLoginAttempts()) {
        return Result.error("账号已锁定,请X分钟后重试");
    }

    // ④SM3+salt哈希比对
    String hashedInput = hashPassword(password, user.getSalt());
    if (!hashedInput.equalsIgnoreCase(user.getPsnPwd())) {
        loginFailCount.put(failKey, totalFails + 1);   // 内存+1
        recordLoginAttempt(user, ip, false);             // 库里记失败
        return Result.error("密码错误");
    }

    // ⑤成功------清计数+记日志+发令牌
    loginFailCount.remove(failKey);
    recordLoginAttempt(user, ip, true);
    return buildLoginResult(user, ip);
}

二、双源失败计数:内存+数据库

java 复制代码
private final Map<String, Integer> loginFailCount = new ConcurrentHashMap<>();  // 内存

private int countRecentFailures(String psnId) {   // 数据库
    long since = System.currentTimeMillis() - failLockMinutes * 60000L;
    "SELECT COUNT(*) FROM SYS_LOGIN_LOG
      WHERE PSN_ID=? AND ONLINE_FLAG='0' AND LOGIN_TIME>?"   // 失败记录+时间窗口内
}

为什么要两个源------

计数快 重启不丢 跨实例
内存Map ✓微秒 ✗清零 ✗各自计数
SYS_LOGIN_LOG查询 慢一次SQL

Math.max(dbFails, 内存值) ------取大者。攻击者碰上重启(内存清零),库里的失败记录还在锁;多实例部署时各自内存不共享,但都查同一个库。双源互为补丁,成本是每次登录多一次COUNT查询------登录是低频操作,这点开销无所谓。

锁定时间窗failLockMinutes------只在窗口内的失败计数,超窗自动解锁(不用显式解锁操作,时间到了count自然归零)。


三、SM3+salt哈希

java 复制代码
public String hashPassword(String plainPassword, String salt) {
    if (salt != null && !salt.isEmpty()) {
        return sm3Hash.hashHex(plainPassword + salt);   // 拼盐后SM3
    }
    return sm3Hash.hashHex(plainPassword);               // 无盐兼容
}

public String generateSalt() {
    byte[] bytes = new byte[16];       // 16字节随机盐
    RANDOM.nextBytes(bytes);           // SecureRandom------密码学安全
    // 转hex存库
}

SM3是国密摘要算法 (对标SHA-256)------政务合规要求。拼接顺序固定password+salt------验证侧必须一致(hashPassword同时服务注册/重置/验证三个场景,拼法写死在一处)。

无盐分支是兼容旧数据 ------系统迭代前期的用户密码没盐,迁移过来直接比对无盐哈希。新密码(resetPwd/新增用户)全部generateSalt+带盐哈希。渐进迁移不强制全员改密

equalsIgnoreCase------hex大小写都认(SM3实现可能输出大写hex,库里存小写)。


四、validatePasswordPolicy:五项复杂度

java 复制代码
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.PasswordPolicy,browise.auth.password-policy.*前缀)------政务等保要求与用户体验之间按项目调。返回错误消息字符串(null=通过)而不是boolean------调用方直接把消息显示给用户,不用再查是哪项失败。


五、enforceSessionLimit:会话数与踢最早

java 复制代码
private void enforceSessionLimit(String psnId, int maxSessions, boolean kickOldest) {
    // 在线数 = SYS_LOGIN_LOG里ONLINE_FLAG='1'且LOGOUT_TIME IS NULL的记录数
    if (在线数 >= maxSessions) {
        if (kickOldest) {
            // 踢最早登录的会话------按LOGIN_TIME ASC取第一条置离线
            "UPDATE SYS_LOGIN_LOG SET ONLINE_FLAG='0', LOGOUT_TIME=?
             WHERE LOG_ID = (SELECT LOG_ID FROM
                (SELECT LOG_ID FROM SYS_LOGIN_LOG
                  WHERE PSN_ID=? AND ONLINE_FLAG='1' AND LOGOUT_TIME IS NULL
                  ORDER BY LOGIN_TIME ASC) WHERE ROWNUM=1)"
        } else {
            throw new RuntimeException("登录设备已达上限" + maxSessions);
        }
    }
}

会话的载体是SYS_LOGIN_LOG表 ------ONLINE_FLAG='1'+LOGOUT_TIME空=一个活会话。不是内存SessionRegistry------重启不丢会话记录(虽然token本身是无状态的,会话数限制需要状态,落库最稳)。

踢最早的ROWNUM子查询 ------Oracle取"最早一条"的三层写法(内层排序+中层取ROWNUM=1+外层UPDATE)。注意这是Oracle方言------MySQL部署时这条SQL要适配(ROW_NUMBER() OVER)。会话踢除只改数据库标记------被踢设备的token依然有效(JWT无状态),要等它下次请求时黑名单校验/过期才真失效。完整踢除=置离线+iat时间点拉黑,当前实现靠token自然过期兜底(access最多2小时)。

kickOldest=false时抛异常------"拒绝第N个设备登录"的严格模式。两种模式配置切换(session-policy.kick-oldest)。


六、refreshToken:三查+旋转

java 复制代码
public Result refreshToken(String oldRefreshToken) {
    // ①格式与类型
    if (!jwtUtil.validate(oldRefreshToken) || !jwtUtil.isRefreshToken(oldRefreshToken))
        return Result.error("refresh token无效或已过期");
    // ②用户实时状态(refresh 7天里用户可能被禁用)
    SysUser user = userMapper.selectById(psnId);
    if (user == null || "0".equals(user.getEnabled())) return Result.error(...);
    // ③旧refresh进黑名单------一次性
    jwtBlacklist.blockRefreshToken(oldRefreshToken);
    // ④签发全新的access+refresh对
    return 新双令牌;
}

③是旋转(rotation) ------每个refresh只用一次,用完拉黑,发新的。防的是refresh被盗后的无限续期 :攻击者偷到refresh①,用户本人也在用------先到者刷新成功,后到者拿着已拉黑的refresh被拒------被盗这件事在下次合法使用时自动暴露(用户刷新失败=有异常)。

第25篇说"当前实现refresh用满7天"------看了AuthService的完整实现要修正:refreshToken端点确实是旋转模式(blockRefreshToken+双新令牌)。JwtUtil.refreshAccessToken(不拉黑的简版)是留给内部信任场景的工具方法。


七、四种登录方式

java 复制代码
loginByPassword(account, password, ip)      // 密码------完整链路
loginByPhone(phone, smsCode, ip)            // 手机号------短信码(码校验在Controller层)
loginBySso(account, ssoToken, ip)           // SSO------对接统一门户
loginByCa(certificateId, ip)                // CA证书------预留

后三种跳过密码与失败计数 ------认证凭据在外部系统(短信网关/门户/CA),这里只做"凭据对应用户存在且启用"。四种方式汇入同一个buildLoginResult------会话限制、双令牌签发、登录日志对四种登录一视同仁。


✅ 亮点:loginByPassword主链的五步、双源失败计数互为补丁的设计、SM3+salt与无盐渐进迁移、会话数以SYS_LOGIN_LOG为载体重启不丢、refreshToken旋转模式防被盗无限续期、四种登录方式汇入同一签发出口。适合做认证模块的人。扩展方向:第25/26篇令牌与过滤器、第28篇JwtBlacklist、第34篇安全评审。

相关推荐
泡海椒1 小时前
JQuick-Curl 性能分析:并发场景下的性能表现与调优,第三方接口调用不只要快写也要稳跑
java·开发语言·okhttp
IT小白杨2 小时前
Facebook广告账户环境隔离实战:从指纹自洽到代理固定的完整工程方案
大数据·经验分享·架构·facebook·指纹浏览器
Rain的Java大神之路2 小时前
JavaWeb开发如何解决跨域问题
java·前端·后端·nginx·web安全·面试·运维开发
自动化监测Learner2 小时前
Navicat Premium 17 中文版安装教程(2026最新版)
java·linux·数据库
江畔柳前堤2 小时前
前台·中台·后台:2026年AI原生时代的架构全景图
开发语言·人工智能·算法·机器学习·架构·scala·ai-native
csdn_aspnet2 小时前
ZLibrary类项目合规避坑指南,从版权风控、CDN架构到反爬策略的IT合规实践分享
架构·知识库·电子书·资源·zlibrary
那个松鼠很眼熟w2 小时前
4.Spring-Ai入门案例
java·人工智能·spring
计算机毕设定制辅导-无忧学长2 小时前
《基于SpringBoot青年公寓出租管理系统的设计与实现》
java·vue.js·spring boot·青年公寓出租管理系统
Dawson Zhu3 小时前
知识图谱与 Palantir Ontology:同一套「实体—关系」表象下,两种截然不同的工程范式
人工智能·语言模型·架构·aigc·agi