做后端开发这些年,登录功能几乎成了每个Java项目绕不开的标配,但每次写都会有人踩坑。最近我在一个新的Spring Boot项目里又完整撸了一遍login流程,从密码加密、登录校验、Token生成到拦截器权限控制,再配合前后端联调,踩了好几个之前没遇到过的问题。索性把这套过程完整记录下来,从设计思路到具体代码,再到常见的失败场景排查,一次讲透。这篇内容适合刚上手Spring Boot的开发者,也适合写过登录但没有系统整理过的朋友。我用的版本是Spring Boot 2.7.18 + JDK 1.8,这个组合目前在生产环境里还是相当稳的,尤其很多公司还在JDK 8部署,照着做完全没问题。
1. 登录功能整体设计:核心思路与方案选型
1.1 需求拆解:登录不只是“验个密码”
登录功能乍一看很简单,无非是拿到用户名和密码,匹配上就通过,匹配不上就拒绝。但真到了实际项目里,要承接的问题远不止这些。第一个是登录入口,后台管理系统、手机App、小程序可能都要登录;第二个是会话保持,登录成功后用户再访问其他接口,系统得知道“你是谁”;第三个是接口管控,有的接口登录后才能调,有的接口必须匿名开放,比如登录接口本身、刷新Token的接口、注册接口;第四个是密码安全,数据库里不能存明文,传输过程中也不能裸奔。
这几条需求如果不在一开始想清楚,后面返工是必然的。之前我就见过一个项目,为了省事直接用HttpSession存用户信息,再用Filter做拦截。这种方案放在单体应用里确实跑得动,但一旦要适配移动端、小程序,或者走向前后端分离,Session那套就非常别扭。跨域环境下Cookie携带麻烦,分布式部署时Session共享又是一堆事,前端还得处理各种兼容。所以这次我干脆选择用JWT(JSON Web Token)做身份凭证,配合自定义拦截器做登录控制,从单体到微服务都能平滑过渡。
还有一个容易被忽略的点是权限扩展。登录做完之后,紧接着就是角色权限、按钮权限。如果一开始就把登录设计成可扩展的,后续加权限功能就很轻松;如果登录模块跟业务代码揉成一团,后面再想拆就难了。这也是我在设计方案时把用户身份、Token凭证、访问控制分开考虑的原因。
1.2 技术选型:Spring Boot + JWT + BCrypt,为什么不引全家桶
这次的技术栈整体是Spring Boot 2.7.18 + MyBatis-Plus 3.5.x + JJWT + Spring Security Crypto里的BCryptPasswordEncoder。先说BCrypt加密。很多初学者喜欢用MD5加盐,但MD5的运算速度太快了,GPU一秒钟能跑几亿次,加了固定盐也防不住撞库,一旦盐泄露就是全军覆没。BCrypt的底层是Blowfish算法,它刻意把加密过程设计得很慢,一次加密大概要几十毫秒,攻击者想靠穷举破解,成本会高很多。同时BCrypt会把随机生成的盐直接混进最终结果串里,我们不需要额外建一个字段存盐,这一点非常省事。
Spring Security Crypto这个库把BCrypt封装得相当好用,单独引进来用就行,没必要把整个Spring Security框架引入。因为Spring Security自带的那套登录过滤链其实很重,如果你只想用它的加密工具,却把整套安全管理引入,反而会干扰自己实现的登录逻辑,配置上也容易眼花缭乱。只加一个依赖,声明一个Bean就可以。
JWT这边我选了io.jsonwebtoken的jjwt 0.9.1版本。0.9.1是经典稳定版,资料多、问题好查,配合JDK 1.8没有任何兼容压力。JWT的核心思路就是服务端把用户身份信息、过期时间、签名一起打包成一个字符串,服务端不保存会话状态,客户端拿到之后每次请求带回来,服务端验签通过就信任它。这个无状态特性对水平扩容特别友好,不需要在多个节点之间同步Session。
但也要清楚,JWT本质上是签名而不是加密,它保证的是内容不被篡改,不是内容不可见。所以不要把密码、手机号、身份证这类敏感信息直接塞进Token里,Token中放userId、username这些非敏感标识就够了。我在设计的时候就牢记了这一点。
依赖配置如下:
xml复制<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
第二个依赖我故意没写版本号,让Spring Boot的dependencyManagement帮我们管理版本,这样能避免和Boot的版本打架。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与密码加密落地
2.1 用户表设计:密码字段和状态字段不能少
登录功能涉及用户信息,表结构设计看着简单,但有几个字段不能省。先看一个可用的基础DDL:
sql复制CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(64) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码',
`nickname` varchar(64) DEFAULT NULL COMMENT '昵称',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1启用 0禁用',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里有个设计细节:password字段的长度设置成100而不是传统的64或32,是因为BCrypt加密后的字符串通常是60个字符左右,但考虑到未来算法升级可能生成的哈希更长,留够余量是稳妥的。username加上唯一索引,防止重复注册。
status字段很多人容易忽视,但它做账号禁用、停用非常有用。登录校验时判断一下status就能轻松实现“封号”操作,不用把用户记录删掉,这样也保留审计痕迹。last_login_time虽然看起来不过是一个时间字段,但运维排查用户活跃度时非常有用,登录成功后顺手更新一下。
2.2 BCrypt加密原理与配置
在Spring Boot里,先用一个配置类把PasswordEncoder声明成Bean:
java复制@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder pass
