SpringBoot 多人协作平台实战(8):Cookie 与登录状态维持
一、背景:HTTP 是无状态协议
我们在使用淘宝时,登录一次之后就无需反复登录,即使刷新页面、重新打开浏览器,依然保持登录状态。这背后的实现原理是什么?
根本原因在于:HTTP 协议本身是无状态的 。每一次 HTTP 请求对服务器来说都是全新的,服务器并不会自动"记住"你是谁。因此,需要一种额外机制来维持用户的登录状态,这就是 Cookie。
二、什么是 Cookie?
Cookie 是维持 HTTP 会话状态的一种常见方式,其工作流程如下:
- 用户登录成功后,服务器在 HTTP 响应头中写入
Set-Cookie字段; - 浏览器读取到
Set-Cookie后,会将该信息保存在本地; - 此后,浏览器对同一域名发起的请求,都会自动在请求头中携带该 Cookie;
- 服务器读取 Cookie 中的认证信息,从而识别当前用户身份。
注意 :HttpOnly 属性表示该 Cookie 只有浏览器主动发起 HTTP 请求时才会携带,无法通过 JavaScript 脚本读取,可有效防御 XSS 攻击。
三、实现登录状态判断:/auth 接口
在 Spring Security 中,已认证的用户信息保存在 SecurityContextHolder 上下文中。通过读取上下文来判断当前请求是否已登录:
typescript
@GetMapping("/auth")
public Object auth() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
User loggedInUser = userService.getUserByUsername(userName);
if (loggedInUser == null) {
return new Result("ok", "用户没有登录", false);
} else {
return new Result("ok", null, true, loggedInUser);
}
}
逻辑说明:
- 从
SecurityContextHolder取出当前认证对象的用户名; - 通过
UserService根据用户名查找用户实体; - 若查找结果为
null,则返回未登录状态;否则返回用户信息。
早期版本通过判断用户名是否包含 "anonymous" 字符串来区分,但这种方式不够健壮。改为直接查询用户对象是否为 null 更为严谨。
四、完善 UserService
UserService 需要同时承担两个职责:
- 实现 Spring Security 的
UserDetailsService接口,供认证框架调用; - 提供业务层面的用户查询方法(如
getUserByUsername)。
4.1 完整代码
typescript
@Service
public class UserService implements UserDetailsService {
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final Map<String, User> users = new ConcurrentHashMap<>();
@Inject
public UserService(BCryptPasswordEncoder bCryptPasswordEncoder) {
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
save("admin", "123456");
}
public User getUserByUsername(String username) {
return users.get(username);
}
public void save(String username, String password) {
users.put(username, new User(1, username, bCryptPasswordEncoder.encode(password)));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!users.containsKey(username)) {
throw new UsernameNotFoundException(username + " 不存在!");
}
User user = users.get(username);
return new org.springframework.security.core.userdetails.User(
username,
user.getEncryptedPassword(),
Collections.emptyList()
);
}
}
4.2 方法职责对照
| 方法 | 用途 |
|---|---|
getUserByUsername |
业务层查询,返回自定义 User 实体 |
loadUserByUsername |
Spring Security 框架调用,返回 UserDetails 对象 |
save |
注册新用户,密码经 BCrypt 加密后存储 |
BCryptPasswordEncoder |
Spring Security 推荐的密码哈希算法,不可逆,安全性高 |
为什么需要两个"用户"概念? Spring Security 内部使用 UserDetails 接口进行认证,而业务层通常有自己的 User 实体类(含 id、业务字段等)。loadUserByUsername 负责认证框架的桥接,getUserByUsername 则供业务逻辑使用,两者分工明确。
五、实现登录接口:/auth/login
typescript
@PostMapping("/auth/login")
public Result login(@RequestBody Map<String, String> usernameAndPasswordJson) {
String username = usernameAndPasswordJson.get("username");
String password = usernameAndPasswordJson.get("password");
// 第一步:根据用户名加载用户详情
UserDetails userDetails;
try {
userDetails = userService.loadUserByUsername(username);
} catch (UsernameNotFoundException e) {
return new Result("fail", "用户不存在", false);
}
// 第二步:构造认证 Token 并交由 AuthenticationManager 验证密码
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
try {
authenticationManager.authenticate(token);
// 第三步:认证成功,将用户信息写入 SecurityContext(状态通过 Session/Cookie 维持)
SecurityContextHolder.getContext().setAuthentication(token);
return new Result("ok", "登录成功", true, userService.getUserByUsername(username));
} catch (BadCredentialsException e) {
return new Result("fail", "密码不正确", false);
}
}
登录流程分解:
scss
POST /auth/login
│
▼
loadUserByUsername(username) ← 查用户是否存在
│
▼
AuthenticationManager.authenticate(token) ← 验证密码是否正确
│
▼
SecurityContextHolder.setAuthentication() ← 写入上下文,状态通过 Session/Cookie 维持
│
▼
返回登录成功 + 用户信息
六、统一响应体 Result
为了前后端交互规范,定义统一的响应结构:
typescript
public static class Result {
String status; // "ok" / "fail"
String msg; // 提示信息
boolean isLogin; // 是否已登录
Object data; // 返回数据(可选)
public Result(String status, String msg, boolean isLogin) {
this(status, msg, isLogin, null);
}
public Result(String status, String msg, boolean isLogin, Object data) {
this.status = status;
this.msg = msg;
this.isLogin = isLogin;
this.data = data;
}
public String getStatus() { return status; }
public String getMsg() { return msg; }
public boolean isLogin() { return isLogin; }
public Object getData() { return data; }
}
示例响应 JSON:
json
// 未登录
{
"status": "ok",
"msg": "用户没有登录",
"isLogin": false,
"data": null
}
// 登录成功
{
"status": "ok",
"msg": "登录成功",
"isLogin": true,
"data": { "id": 1, "username": "admin" }
}
七、完整 AuthController
typescript
@RestController
public class AuthController {
private final UserService userService;
private final AuthenticationManager authenticationManager;
public AuthController(UserService userService,
AuthenticationManager authenticationManager) {
this.userService = userService;
this.authenticationManager = authenticationManager;
}
@GetMapping("/auth")
public Object auth() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
User loggedInUser = userService.getUserByUsername(userName);
if (loggedInUser == null) {
return new Result("ok", "用户没有登录", false);
} else {
return new Result("ok", null, true, loggedInUser);
}
}
@PostMapping("/auth/login")
public Result login(@RequestBody Map<String, String> usernameAndPasswordJson) {
String username = usernameAndPasswordJson.get("username");
String password = usernameAndPasswordJson.get("password");
UserDetails userDetails;
try {
userDetails = userService.loadUserByUsername(username);
} catch (UsernameNotFoundException e) {
return new Result("fail", "用户不存在", false);
}
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
try {
authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(token);
return new Result("ok", "登录成功", true, userService.getUserByUsername(username));
} catch (BadCredentialsException e) {
return new Result("fail", "密码不正确", false);
}
}
}
八、阶段小结
本节我们完成了以下内容:
- 理解了 HTTP 无状态特性与 Cookie 的工作原理;
- 实现了
/auth接口,判断用户是否处于登录状态; - 完善了
UserService,整合 Spring Security 的UserDetailsService接口; - 实现了
/auth/login接口,完成用户名密码认证并维持 Session; - 定义了统一的
Result响应体。
下一步 :目前用户数据存储在内存 Map 中,下节课将接入真实数据库(MySQL),使用 MyBatis 完成持久化存储。
系列课程:Java SpringBoot 多人协作平台实战 · 第八章 · SpringBoot登录状态维持与Cookie原理