SpringBoot 多人协作平台实战(8):Cookie 与登录状态维持

SpringBoot 多人协作平台实战(8):Cookie 与登录状态维持

一、背景:HTTP 是无状态协议

我们在使用淘宝时,登录一次之后就无需反复登录,即使刷新页面、重新打开浏览器,依然保持登录状态。这背后的实现原理是什么?

根本原因在于:HTTP 协议本身是无状态的 。每一次 HTTP 请求对服务器来说都是全新的,服务器并不会自动"记住"你是谁。因此,需要一种额外机制来维持用户的登录状态,这就是 Cookie


二、什么是 Cookie?

Cookie 是维持 HTTP 会话状态的一种常见方式,其工作流程如下:

  1. 用户登录成功后,服务器在 HTTP 响应头中写入 Set-Cookie 字段;
  2. 浏览器读取到 Set-Cookie 后,会将该信息保存在本地;
  3. 此后,浏览器对同一域名发起的请求,都会自动在请求头中携带该 Cookie;
  4. 服务器读取 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 需要同时承担两个职责:

  1. 实现 Spring Security 的 UserDetailsService 接口,供认证框架调用;
  2. 提供业务层面的用户查询方法(如 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原理

相关推荐
步行cgn6 小时前
Spring c 命名空间注入详解
java·后端·spring
明月_清风6 小时前
Maven 到底是什么?一篇文章搞懂 Java 项目构建与依赖管理
java·后端·maven
明月_清风6 小时前
AI 越来越强,程序员真正的价值到底是什么?
人工智能·后端
aramae6 小时前
MySQL复合查询(8)
java·c语言·开发语言·后端·算法
Rain的Java大神之路7 小时前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
码事漫谈8 小时前
别再跟AI说“请”了,它根本不 care——但有个东西它超在意
后端
郑州光合科技余经理10 小时前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程
IT_陈寒10 小时前
Vue的响应式让我熬到凌晨三点,原来漏了这个小细节
前端·人工智能·后端
颜进强11 小时前
01 · NestJS 是什么:用途、解决什么问题、与热门框架对比
前端·后端·ai编程
isfox11 小时前
MapReduce Shuffle 深度解析:数据从 Map 到 Reduce 的完整旅程
后端