【Spring Security】认证(二)
Spring Security
- 认证(Authentication)
- 在代码中手动执行认证逻辑
- 登出与会话清理
- Session 与 Remember-Me 机制
认证(Authentication)
在代码中手动执行认证逻辑
通常,Spring Security 会自动通过 Filter 链来认证用户(例如表单登录、Basic Auth 等)。
但在一些场景下,我们希望:
- 前端使用 JSON 登录;
- 或通过 外部接口/第三方认证;
- 或在登录逻辑中加入 自定义验证规则(验证码、短信、OAuth等)。
这些情况下,我们需要 手动执行认证过程。
在 Controller 中手动登录
假设我们有自定义的登录接口 /api/login
。
控制器代码
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 {@Autowiredprivate 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);// 认证成功后,将结果存入 SecurityContextSecurityContextHolder.getContext().setAuthentication(authentication);// 返回成功响应(这里简单返回用户名)return "登录成功,欢迎 " + authentication.getName();}
}
登录请求体类
public class LoginRequest {private String username;private String password;// Getter / Setterpublic 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
不再自动暴露,需要我们手动注册:
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 {@Beanpublic 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 起默认如此) |
登出后操作 | 清除 SecurityContext 与 HttpSession |
默认跳转 | /login?logout |
即:当用户发送 POST /logout
请求时,Security 自动执行登出操作,销毁认证状态并跳转回登录页。
整个 Logout 流程主要由以下组件组成:
组件 | 作用 |
---|---|
LogoutFilter | 负责拦截登出请求并触发登出流程 |
LogoutHandler | 执行登出具体逻辑(清理上下文、删除 Session、清理 Cookie 等) |
LogoutSuccessHandler | 登出成功后的响应处理(跳转 / JSON) |
默认过滤器:LogoutFilter
Spring Security 内部有一个专门的过滤器:
LogoutFilter↓SecurityContextLogoutHandler↓LogoutSuccessHandler
当用户访问 /logout
时,LogoutFilter
会:
- 调用
SecurityContextLogoutHandler
:- 删除认证信息 (
SecurityContextHolder.clearContext()
) - 使 Session 失效 (
session.invalidate()
) - 删除 “remember-me” token(如果启用)
- 删除认证信息 (
- 调用
LogoutSuccessHandler
:默认重定向到/login?logout
自定义登出配置
可以在 SecurityFilterChain
中定制登出行为:
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic 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 消息。
自定义实现类
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();@Overridepublic 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));}
}
配置注册
@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": "登出成功"
}
手动触发登出(在控制器中)
有时我们需要在业务逻辑中手动登出:
@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 登出时主要执行以下操作:
- 清除上下文:
SecurityContextHolder.clearContext()
- 使会话失效:
session.invalidate()
- 删除 Cookie(如果配置)
- 清除 remember-me token
- 触发登出成功处理器
登出后,SecurityContextHolder.getContext().getAuthentication()
会返回 null
,表示用户已不再认证。
Session 与 Remember-Me 机制
认证状态的“持久化”问题
认证通过 ≠ 永久登录。
在用户登录成功后,Spring Security 会将用户的 Authentication
对象保存到 Session 中:
SecurityContextHolder.getContext().setAuthentication(authResult);
每次请求,过滤器都会从 Session 中取出 Authentication
放回 SecurityContext
。
因此:
动作 | 结果 |
---|---|
登录成功 | Authentication 写入 Session |
访问受保护资源 | 从 Session 读取认证信息 |
Session 失效 | 认证信息消失 → 用户退出登录 |
Session 的存储与恢复流程
请求生命周期可以用下图表示:
① 登录成功↓
SecurityContextPersistenceFilter 保存认证信息↓
Session 保存 SecurityContext② 再次访问↓
SecurityContextPersistenceFilter 从 Session 中取出 SecurityContext↓
SecurityContextHolder 填充 Authentication
这意味着:
- SecurityContext 实际是保存在 Session 中;
- Session 是认证状态的容器。
核心类与过滤器
组件 | 作用 |
---|---|
SecurityContextHolder | 当前线程的安全上下文(ThreadLocal 存储) |
SecurityContextPersistenceFilter | 请求开始与结束时加载/清理 SecurityContext |
HttpSessionSecurityContextRepository | 从 Session 中读写 SecurityContext |
Session 管理配置
基本配置
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 需要时创建 Session);
策略 | 含义 |
---|---|
ALWAYS | 总是创建 Session |
IF_REQUIRED | 需要时创建(默认) |
NEVER | 不创建,但可使用现有 Session |
STATELESS | 无状态,不使用 Session(适合 JWT) |
防止并发登录控制
Spring Security 可以控制同一用户账号的同时登录数量:
http.sessionManagement(session -> session.maximumSessions(1) // 同一账号最多只能登录 1 个 session.maxSessionsPreventsLogin(false) // 若为 true,则拒绝新登录;false 表示踢出旧 session);
推荐:企业后台管理系统通常设置为
1
,防止账号被多处同时使用。
Remember-Me 概念理解
Remember-Me
是“持久化登录”的一种方案:即使 Session 过期、浏览器关闭,用户仍然能自动登录。
工作原理简图
① 用户勾选 “记住我” 登录成功↓
服务端生成 Remember-Me Token↓
浏览器保存 Cookie(token 值)↓
② 下次访问时↓
Cookie 被发送 → 后端验证 token → 自动登录
开启 Remember-Me
最基本配置如下:
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 存在数据库表中,支持多设备登录 |
-
基于 Cookie 的实现(默认)
使用
TokenBasedRememberMeServices
:token = Base64(username + ":" + expiryTime + ":" + md5(username + expiryTime + password + key))
验证时:
- 取出 cookie;
- 解码;
- 验证签名是否有效;
- 若有效 → 自动加载用户信息。
缺点:Cookie 可被盗取(安全风险较高)。
-
基于数据库的实现
启用
PersistentTokenBasedRememberMeServices
:-
创建数据库表
Spring Security 需要一个固定结构的表:
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
@Bean public PersistentTokenRepository tokenRepository(DataSource dataSource) {JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();repo.setDataSource(dataSource);// repo.setCreateTableOnStartup(true); // 启动时自动建表(首次可启用)return repo; }
-
注册到 SecurityConfig
@Autowired private PersistentTokenRepository tokenRepository;http.rememberMe(remember -> remember.tokenRepository(tokenRepository).tokenValiditySeconds(14 * 24 * 60 * 60).userDetailsService(userDetailsService));
优点:
- 支持多端登录;
- 可以在数据库中手动清除 Token;
- 安全性高。
-
Remember-Me 过滤器流程
在过滤器链中,Remember-Me 对应:
RememberMeAuthenticationFilter
其作用是:
- 检查请求中是否有 Remember-Me Cookie;
- 验证 token;
- 如果验证通过,自动登录用户;
- 创建新的
Authentication
; - 写入
SecurityContextHolder
。
手动清除 Remember-Me Cookie
当登出时,应同时清除 cookie(否则仍可自动登录):
http.logout(logout -> logout.deleteCookies("remember-me") // 删除 remember-me cookie.invalidateHttpSession(true).clearAuthentication(true));