【Spring Security】认证(二)

Spring Security

认证(Authentication)

在代码中手动执行认证逻辑

通常,Spring Security 会自动通过 Filter 链来认证用户(例如表单登录、Basic Auth 等)。

但在一些场景下,我们希望:

  • 前端使用 JSON 登录
  • 或通过 外部接口/第三方认证
  • 或在登录逻辑中加入 自定义验证规则(验证码、短信、OAuth等)

这些情况下,我们需要 手动执行认证过程

在 Controller 中手动登录

假设我们有自定义的登录接口 /api/login

控制器代码

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public String login(@RequestBody LoginRequest loginRequest) {

        // 构造 Authentication 对象(未认证状态)
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                );

        // 执行认证(会调用 UserDetailsService.loadUserByUsername)
        Authentication authentication = authenticationManager.authenticate(authenticationToken);

        // 认证成功后,将结果存入 SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 返回成功响应(这里简单返回用户名)
        return "登录成功,欢迎 " + authentication.getName();
    }
}

登录请求体类

java 复制代码
public class LoginRequest {
    private String username;
    private String password;

    // Getter / Setter
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
}

配置类中暴露 AuthenticationManager Bean

在 Spring Security 5.7+,AuthenticationManager 不再自动暴露,需要我们手动注册:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;

@Configuration
public class SecurityConfig {

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
}

请求示例

请求:

复制代码
POST /api/login
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}

响应:

复制代码
登录成功,欢迎 admin

常见用途

场景 示例
前后端分离项目 前端提交 JSON,后端验证后返回 JWT
移动端接口登录 App 提交账号密码
管理员模拟登录 管理员手动切换身份
外部系统同步认证 外部系统调用认证接口获得凭证

登出与会话清理

默认登出机制

Spring Security 默认启用登出功能:

默认项 默认值
请求路径 /logout
请求方式 POST(从 Spring Security 6.1 起默认如此)
登出后操作 清除 SecurityContextHttpSession
默认跳转 /login?logout

即:当用户发送 POST /logout 请求时,Security 自动执行登出操作,销毁认证状态并跳转回登录页。

整个 Logout 流程主要由以下组件组成:

组件 作用
LogoutFilter 负责拦截登出请求并触发登出流程
LogoutHandler 执行登出具体逻辑(清理上下文、删除 Session、清理 Cookie 等)
LogoutSuccessHandler 登出成功后的响应处理(跳转 / JSON)

默认过滤器:LogoutFilter

Spring Security 内部有一个专门的过滤器:

mathematica 复制代码
LogoutFilter
    ↓
    SecurityContextLogoutHandler
    ↓
    LogoutSuccessHandler

当用户访问 /logout 时,LogoutFilter 会:

  1. 调用 SecurityContextLogoutHandler
    • 删除认证信息 (SecurityContextHolder.clearContext())
    • 使 Session 失效 (session.invalidate())
    • 删除 "remember-me" token(如果启用)
  2. 调用 LogoutSuccessHandler:默认重定向到 /login?logout

自定义登出配置

可以在 SecurityFilterChain 中定制登出行为:

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/doLogin")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/doLogout") // 自定义登出路径
                .logoutSuccessUrl("/login?logout") // 成功后跳转路径
                .invalidateHttpSession(true) // 销毁 session
                .clearAuthentication(true) // 清除认证信息
                .deleteCookies("JSESSIONID") // 删除指定 Cookie
                .permitAll()
            );

        return http.build();
    }
}

自定义 LogoutSuccessHandler(返回 JSON)

在前后端分离项目中,我们通常不希望重定向,而是返回一个 JSON 消息。

自定义实现类

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onLogoutSuccess(HttpServletRequest request,
                                HttpServletResponse response,
                                Authentication authentication) throws IOException {

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);

        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "登出成功");

        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

配置注册

java 复制代码
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout
            .logoutUrl("/logout")
            .logoutSuccessHandler(customLogoutSuccessHandler) // 使用自定义处理器
            .invalidateHttpSession(true)
            .clearAuthentication(true)
            .permitAll()
        );

    return http.build();
}

请求示例

请求

复制代码
POST /logout
Cookie: JSESSIONID=xxxxxx

响应

复制代码
{
  "success": true,
  "message": "登出成功"
}

手动触发登出(在控制器中)

有时我们需要在业务逻辑中手动登出:

java 复制代码
@GetMapping("/manualLogout")
public void manualLogout(HttpServletRequest request, HttpServletResponse response) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null) {
        new SecurityContextLogoutHandler().logout(request, response, auth);
    }
}

Spring Security 登出时主要执行以下操作:

  1. 清除上下文SecurityContextHolder.clearContext()
  2. 使会话失效session.invalidate()
  3. 删除 Cookie(如果配置)
  4. 清除 remember-me token
  5. 触发登出成功处理器

登出后,SecurityContextHolder.getContext().getAuthentication() 会返回 null,表示用户已不再认证。

Session 与 Remember-Me 机制

认证状态的"持久化"问题

认证通过 ≠ 永久登录。

在用户登录成功后,Spring Security 会将用户的 Authentication 对象保存到 Session 中:

复制代码
SecurityContextHolder.getContext().setAuthentication(authResult);

每次请求,过滤器都会从 Session 中取出 Authentication 放回 SecurityContext

因此:

动作 结果
登录成功 Authentication 写入 Session
访问受保护资源 从 Session 读取认证信息
Session 失效 认证信息消失 → 用户退出登录

Session 的存储与恢复流程

请求生命周期可以用下图表示:

mathematica 复制代码
① 登录成功
   ↓
SecurityContextPersistenceFilter 保存认证信息
   ↓
Session 保存 SecurityContext

② 再次访问
   ↓
SecurityContextPersistenceFilter 从 Session 中取出 SecurityContext
   ↓
SecurityContextHolder 填充 Authentication

这意味着:

  • SecurityContext 实际是保存在 Session 中;
  • Session 是认证状态的容器

核心类与过滤器

组件 作用
SecurityContextHolder 当前线程的安全上下文(ThreadLocal 存储)
SecurityContextPersistenceFilter 请求开始与结束时加载/清理 SecurityContext
HttpSessionSecurityContextRepository 从 Session 中读写 SecurityContext

Session 管理配置

基本配置

java 复制代码
http
    .sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 需要时创建 Session
    );
策略 含义
ALWAYS 总是创建 Session
IF_REQUIRED 需要时创建(默认)
NEVER 不创建,但可使用现有 Session
STATELESS 无状态,不使用 Session(适合 JWT)

防止并发登录控制

Spring Security 可以控制同一用户账号的同时登录数量:

java 复制代码
http
    .sessionManagement(session -> session
        .maximumSessions(1) // 同一账号最多只能登录 1 个 session
        .maxSessionsPreventsLogin(false) // 若为 true,则拒绝新登录;false 表示踢出旧 session
    );

推荐:企业后台管理系统通常设置为 1,防止账号被多处同时使用。

Remember-Me 概念理解

Remember-Me 是"持久化登录"的一种方案:即使 Session 过期、浏览器关闭,用户仍然能自动登录。

工作原理简图

mathematica 复制代码
① 用户勾选 "记住我" 登录成功
   ↓
服务端生成 Remember-Me Token
   ↓
浏览器保存 Cookie(token 值)
   ↓
② 下次访问时
   ↓
Cookie 被发送 → 后端验证 token → 自动登录

开启 Remember-Me

最基本配置如下:

java 复制代码
http
    .rememberMe(remember -> remember
        .key("my-remember-key")          // 签名密钥,防止伪造
        .tokenValiditySeconds(7 * 24 * 60 * 60) // 有效期 7 天
        .rememberMeParameter("rememberMe") // 表单字段名
        .userDetailsService(userDetailsService) // 用于重新加载用户信息
    );

登录表单需有一个字段:

复制代码
<input type="checkbox" name="rememberMe" value="true">

Remember-Me 的两种实现方式

方式 说明
基于 Cookie(默认) Token 存储在 Cookie 中(签名加密)
基于数据库(持久化) Token 存在数据库表中,支持多设备登录
  1. 基于 Cookie 的实现(默认)

    使用 TokenBasedRememberMeServices

    复制代码
    token = Base64(username + ":" + expiryTime + ":" + md5(username + expiryTime + password + key))

    验证时:

    • 取出 cookie;
    • 解码;
    • 验证签名是否有效;
    • 若有效 → 自动加载用户信息。

    缺点:Cookie 可被盗取(安全风险较高)。

  2. 基于数据库的实现

    启用 PersistentTokenBasedRememberMeServices

    • 创建数据库表

      Spring Security 需要一个固定结构的表:

      sql 复制代码
      CREATE TABLE persistent_logins (
          username VARCHAR(64) NOT NULL,
          series VARCHAR(64) PRIMARY KEY,
          token VARCHAR(64) NOT NULL,
          last_used TIMESTAMP NOT NULL
      );
    • 配置 JDBC TokenRepository

      java 复制代码
      @Bean
      public PersistentTokenRepository tokenRepository(DataSource dataSource) {
          JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
          repo.setDataSource(dataSource);
          // repo.setCreateTableOnStartup(true); // 启动时自动建表(首次可启用)
          return repo;
      }
    • 注册到 SecurityConfig

      java 复制代码
      @Autowired
      private PersistentTokenRepository tokenRepository;
      
      http
          .rememberMe(remember -> remember
              .tokenRepository(tokenRepository)
              .tokenValiditySeconds(14 * 24 * 60 * 60)
              .userDetailsService(userDetailsService)
          );

      优点:

      • 支持多端登录;
      • 可以在数据库中手动清除 Token;
      • 安全性高。

Remember-Me 过滤器流程

在过滤器链中,Remember-Me 对应:

复制代码
RememberMeAuthenticationFilter

其作用是:

  1. 检查请求中是否有 Remember-Me Cookie;
  2. 验证 token;
  3. 如果验证通过,自动登录用户;
  4. 创建新的 Authentication
  5. 写入 SecurityContextHolder

手动清除 Remember-Me Cookie

当登出时,应同时清除 cookie(否则仍可自动登录):

java 复制代码
http
    .logout(logout -> logout
        .deleteCookies("remember-me") // 删除 remember-me cookie
        .invalidateHttpSession(true)
        .clearAuthentication(true)
    );
相关推荐
33255_40857_280593 小时前
告别密码爆破!手把手教你用注解和拦截器实现登录限流
java
程序员爱钓鱼3 小时前
Python编程实战 · 基础入门篇 | Python的版本与安装
后端·python
舒克日记3 小时前
基于springboot针对老年人的景区订票系统
java·spring boot·后端
GoldenaArcher3 小时前
GraphQL 工程化篇 III:引入 Prisma 与数据库接入
数据库·后端·graphql
西西学代码3 小时前
Flutter---showCupertinoDialog
java·前端·flutter
多多*3 小时前
上传文件相关业务,采用策略模式+模版方法模式进行动态解耦
java·开发语言
晨非辰3 小时前
【面试高频数据结构(四)】--《从单链到双链的进阶,读懂“双向奔赴”的算法之美与效率权衡》
java·数据结构·c++·人工智能·算法·机器学习·面试
沐雨橙风ιε3 小时前
Spring Boot整合Apache Shiro权限认证框架(实战篇)
java·spring boot·后端·apache shiro