一、自动登录原理
自动登录的原理就是首次认证成功后,服务端给浏览器和数据库存储了一份token,浏览器将token存储在cookie上,下次访问时,浏览器会自动携带cookie到服务端,服务端拿着浏览器的token去数据库查找,如果找到了就认为认证成功,否则失败。


二、创建持久化表结构
spring security 提供了 jdbctokenrepositoryimpl 来持久化 remember-me token,需要一张 persistent_logins 表:
create table persistent_logins (
username varchar(64) not null comment '用户名',
series varchar(64) not null comment '唯一序列号,用于标识一次登录会话',
token varchar(64) not null comment '登录令牌,每次自动登录后更新',
last_used timestamp not null default current_timestamp on update current_timestamp comment '最后使用时间',
primary key (series)
) engine=innodb default charset=utf8 comment='记住我功能持久化登录信息表';三、添加 maven 依赖
确保 pom.xml 中包含 spring security 和 spring boot starter web 依赖:
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-security</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-jdbc</artifactid>
</dependency>
<dependency>
<groupid>mysql</groupid>
<artifactid>mysql-connector-java</artifactid>
<scope>runtime</scope>
</dependency>四、配置数据源
在 application.yml 中配置数据库连接:
spring:
datasource:
url: jdbc:mysql://localhost:3306/security_demo?usessl=false&servertimezone=utc
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver五、spring security 配置类
核心配置类,开启 remember-me 功能并指定 token 持久化方式:
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.security.config.annotation.web.builders.httpsecurity;
import org.springframework.security.config.annotation.web.configuration.enablewebsecurity;
import org.springframework.security.core.userdetails.userdetailsservice;
import org.springframework.security.crypto.bcrypt.bcryptpasswordencoder;
import org.springframework.security.crypto.password.passwordencoder;
import org.springframework.security.web.securityfilterchain;
import org.springframework.security.web.authentication.rememberme.jdbctokenrepositoryimpl;
import org.springframework.security.web.authentication.rememberme.persistenttokenrepository;
import javax.sql.datasource;
@configuration
@enablewebsecurity
public class securityconfig {
private final datasource datasource;
private final userdetailsservice userdetailsservice;
// 通过构造器注入 datasource 和 userdetailsservice
public securityconfig(datasource datasource, userdetailsservice userdetailsservice) {
this.datasource = datasource;
this.userdetailsservice = userdetailsservice;
}
@bean
public passwordencoder passwordencoder() {
// 使用 bcrypt 加密密码
return new bcryptpasswordencoder();
}
@bean
public persistenttokenrepository persistenttokenrepository() {
// 创建 jdbctokenrepositoryimpl 实例,用于将 token 持久化到数据库
jdbctokenrepositoryimpl repo = new jdbctokenrepositoryimpl();
repo.setdatasource(datasource);
// 首次运行时设置为 true 可自动建表,生产环境建议手动建表后设为 false
// repo.setcreatetableonstartup(true);
return repo;
}
@bean
public securityfilterchain securityfilterchain(httpsecurity http) throws exception {
http
// 配置表单登录
.formlogin(form -> form
.loginpage("/login") // 自定义登录页
.loginprocessingurl("/dologin") // 登录表单提交地址
.defaultsuccessurl("/home") // 登录成功跳转
.permitall()
)
// 配置 remember-me 自动登录
.rememberme(remember -> remember
.tokenrepository(persistenttokenrepository()) // 持久化 token 到数据库
.tokenvalidityseconds(7 * 24 * 60 * 60) // token 有效期:7 天(单位秒)
.userdetailsservice(userdetailsservice) // 用于根据用户名加载用户信息
.remembermeparameter("remember-me") // 登录表单中记住我复选框的 name 属性
.remembermecookiename("remember-me-cookie") // 浏览器中存储的 cookie 名称
)
// 配置请求授权
.authorizehttprequests(auth -> auth
.requestmatchers("/login", "/css/", "/js/").permitall() // 公开资源
.anyrequest().authenticated() // 其他请求需认证
)
// 退出登录配置
.logout(logout -> logout
.logoutsuccessurl("/login?logout")
.deletecookies("remember-me-cookie") // 退出时清除 remember-me cookie
.permitall()
);
return http.build();
}
}六、自定义 userdetailsservice
用于从数据库加载用户信息,remember-me 自动登录时需要根据用户名查询用户:
import org.springframework.security.core.authority.simplegrantedauthority;
import org.springframework.security.core.userdetails.user;
import org.springframework.security.core.userdetails.userdetails;
import org.springframework.security.core.userdetails.userdetailsservice;
import org.springframework.security.core.userdetails.usernamenotfoundexception;
import org.springframework.stereotype.service;
import java.util.collections;
@service
public class customuserdetailsservice implements userdetailsservice {
// 模拟从数据库查询用户,实际项目中请注入 usermapper/userrepository
@override
public userdetails loaduserbyusername(string username) throws usernamenotfoundexception {
// 这里模拟一个用户,实际应从数据库查询
if (!"admin".equals(username)) {
throw new usernamenotfoundexception("用户不存在: " + username);
}
// 密码必须是 bcrypt 加密后的密文,明文 "123456" 对应的 bcrypt 密文
string encodedpassword = "$2a$10$n9qo8uloickgx2zmrzomyeijzagcfl7p92ldgxad68ljzdl17lhwy";
return new user(
"admin",
encodedpassword,
true, // 账号是否可用
true, // 账号未过期
true, // 凭证未过期
true, // 账号未锁定
collections.singletonlist(new simplegrantedauthority("role_user"))
);
}
}七、登录页面(thymeleaf 模板)
在 src/main/resources/templates/login.html 中创建登录页,注意 remember-me 复选框的 name 必须与配置中的 remembermeparameter 一致:
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>登录</title>
</head>
<body>
<h2>用户登录</h2>
<form th:action="@{/dologin}" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required />
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required />
</div>
<div>
<!-- 记住我复选框,name 必须与 securityconfig 中的 remembermeparameter 一致 -->
<label>
<input type="checkbox" name="remember-me" value="true" /> 记住我(自动登录)
</label>
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
</body>
</html>八、controller 控制器
提供登录页和首页的请求映射:
import org.springframework.stereotype.controller;
import org.springframework.web.bind.annotation.getmapping;
@controller
public class homecontroller {
@getmapping("/login")
public string loginpage() {
// 返回登录页面
return "login";
}
@getmapping("/home")
public string homepage() {
// 返回首页,只有登录成功后才能访问
return "home";
}
}九、首页模板
在 src/main/resources/templates/home.html 中:
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>首页</title>
</head>
<body>
<h1>欢迎登录!</h1>
<p>你已经成功登录系统。</p>
<p>如果勾选了"记住我",关闭浏览器后再次访问本页面将自动登录。</p>
<a th:href="@{/logout}" rel="external nofollow" >退出登录</a>
</body>
</html>十、验证自动登录
- 启动项目,访问
http://localhost:8080/login。 - 输入用户名
admin,密码123456,勾选"记住我"复选框,点击登录。 - 登录成功后,浏览器会收到名为
remember-me-cookie的 cookie。 - 关闭浏览器,重新打开并访问
http://localhost:8080/home,无需再次登录即可进入首页。 - 查看数据库
persistent_logins表,会看到一条包含用户名、series 和 token 的记录。
十一、核心原理总结
- 首次登录:用户勾选"记住我"提交登录 →
remembermeauthenticationfilter拦截成功认证 → 生成 series + token 存入数据库,同时将 series 写入 cookie 返回浏览器。 - 后续访问:浏览器携带 cookie 访问 →
remembermeauthenticationfilter从 cookie 中提取 series → 去数据库查询匹配的 series 和 token → 验证通过后自动创建认证对象,用户无需输入密码。 - 安全机制:每次自动登录成功后,服务端会更新 token 值(防止 token 被窃取后重复使用)。如果数据库中的 token 与 cookie 中的 token 不匹配,说明 token 可能被盗用,系统会清除该用户的所有 remember-me 记录。
常见疑问解答
q1:token 过期了也能查到数据,岂不是不安全?
a:实际上,remember-me 的安全性不仅依赖 token 过期时间,更依赖 series(唯一序列号) + token 双字段机制。每次自动登录成功后,服务端都会生成一个新的 token 并更新到数据库和 cookie 中,旧的 token 立即失效。如果攻击者窃取了 cookie 并在合法用户之前使用,服务端会发现数据库中的 token 与 cookie 携带的 token 不一致,此时系统会判定 token 可能被盗用,直接删除该 series 对应的整条记录,让攻击者和合法用户都无法再自动登录,从而保护账户安全。
q2:唯一序列号(series)的作用是什么?浏览器不是携带 token 进行访问吗?
a:浏览器 cookie 中存储的是 series + token 的组合,而非单纯的 token。series 作为数据库 persistent_logins 表的主键,用于唯一标识一次登录会话。服务端根据 cookie 中的 series 去数据库查找对应的记录,再比对 token 是否匹配。这样设计的好处是:同一个用户可以拥有多个 series(多次登录产生多个会话),每个会话独立管理;当某个 series 对应的 token 被盗用时,只需删除该 series 的记录,不影响其他正常会话。
q3:token 在数据库中会自动更新吗?客户端(浏览器)怎么更新?
a:是的,token 在数据库中会自动更新,但这个过程是由 spring security 框架自动完成的,不需要开发者手动编写更新逻辑。具体流程如下:
- 服务端自动更新数据库:当用户通过 remember-me cookie 自动登录成功后,
persistenttokenbasedremembermeservices会调用persistenttokenrepository.updatetoken()方法,生成一个新的随机 token 值,并更新到数据库persistent_logins表的 token 字段中(series 保持不变)。 - 客户端(浏览器)自动更新 cookie:服务端更新数据库后,会通过响应头
set-cookie将新的series + token组合写入浏览器的remember-me-cookie中。浏览器收到响应后自动覆盖旧的 cookie,整个过程对用户完全透明。
简单来说,每次自动登录都是一次"一换一"的更新:服务端更新数据库中的 token,同时通过响应让浏览器更新 cookie 中的 token,两者始终保持一致。这样即使旧的 token 被窃取,在合法用户下一次自动登录后也会立即失效。
q4:remember-me-cookie 必须这样命名吗?设置成其他的命名可以吗?
a:完全可以自定义。在 spring security 配置中,.remembermecookiename("remember-me-cookie") 这一行就是用来指定 cookie 名称的,你可以改成任意合法的 cookie 名称,例如:
.remembermecookiename("my-app-remember-token") // 自定义 cookie 名称需要注意的是,修改 cookie 名称后,退出登录时 deletecookies() 中的名称也要同步修改,否则退出时无法清除该 cookie:
.logout(logout -> logout
.logoutsuccessurl("/login?logout")
.deletecookies("my-app-remember-token") // 与 remembermecookiename 保持一致
.permitall()
)另外,如果前端 javascript 需要读取或操作这个 cookie,也要使用自定义后的名称。总之,remember-me-cookie 只是 spring security 的默认名称,你可以根据项目规范随意命名,只要保证配置中所有引用该名称的地方保持一致即可。
q5:series 字段存储在哪?浏览器是如何把它发送到服务端的?
a:series 字段存储在以下两个位置:
- 数据库:series 是
persistent_logins表的主键,首次登录勾选"记住我"时由服务端生成并写入该表。 - 浏览器 cookie:series 与 token 拼接成
series:token格式,存储在名为remember-me-cookie的 cookie 中(cookie 名称可自定义)。
浏览器发送 series 到服务端的过程完全自动:
- 用户首次登录勾选"记住我"后,服务端通过响应头
set-cookie将series:token写入浏览器 cookie。 - 后续浏览器访问同一站点的任何请求,都会自动携带该 cookie(受同源策略保护)。
- 服务端的
remembermeauthenticationfilter拦截请求,从 cookie 中提取series:token,解析出 series 后去数据库persistent_logins表查询对应记录,再比对 token 是否匹配。
整个过程对开发者完全透明,无需手动编写 cookie 读写代码。spring security 的 persistenttokenbasedremembermeservices 负责 cookie 的解析和 token 的比对工作。
a:cookie 中存储的格式为 series:token,中间用英文冒号(:)作为分隔符。例如一个实际的 cookie 值长这样:
cookie 名称: remember-me
cookie 值: series + ":" + token (base64编码)
示例: mmfjm2q0ztu2nzg5mgfiyw==:zgvmndu2nzg5mgfiy2rlzg==
其中:
- 冒号前的部分是 series(唯一序列号),用于在
persistent_logins表中定位记录。 - 冒号后的部分是 token(登录令牌),用于验证本次自动登录的合法性。
服务端解析时,persistenttokenbasedremembermeservices 会按第一个冒号将 cookie 值拆分为两部分:前半部分作为 series,后半部分作为 token。如果 cookie 值中没有冒号,或者拆分后任一部分为空,服务端会判定该 cookie 无效,拒绝自动登录。
这种 series:token 的格式设计,使得服务端可以仅凭一个 cookie 值同时获取到定位记录的主键(series)和验证凭据(token),无需额外的请求参数或数据库全表扫描,既高效又安全。
以上就是基于spring security实现自动登录的示例代码的详细内容,更多关于spring security自动登录的资料请关注代码网其它相关文章!
发表评论