Spring Security
- 认证(Authentication)
-
- 在代码中手动执行认证逻辑
- 登出与会话清理
- [Session 与 Remember-Me 机制](#Session 与 Remember-Me 机制)
认证(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 起默认如此) |
登出后操作 | 清除 SecurityContext 与 HttpSession |
默认跳转 | /login?logout |
即:当用户发送 POST /logout
请求时,Security 自动执行登出操作,销毁认证状态并跳转回登录页。
整个 Logout 流程主要由以下组件组成:
组件 | 作用 |
---|---|
LogoutFilter |
负责拦截登出请求并触发登出流程 |
LogoutHandler |
执行登出具体逻辑(清理上下文、删除 Session、清理 Cookie 等) |
LogoutSuccessHandler |
登出成功后的响应处理(跳转 / JSON) |
默认过滤器:LogoutFilter
Spring Security 内部有一个专门的过滤器:
mathematica
LogoutFilter
↓
SecurityContextLogoutHandler
↓
LogoutSuccessHandler
当用户访问 /logout
时,LogoutFilter
会:
- 调用
SecurityContextLogoutHandler
:- 删除认证信息 (
SecurityContextHolder.clearContext()
) - 使 Session 失效 (
session.invalidate()
) - 删除 "remember-me" token(如果启用)
- 删除认证信息 (
- 调用
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 登出时主要执行以下操作:
- 清除上下文 :
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 的存储与恢复流程
请求生命周期可以用下图表示:
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 存在数据库表中,支持多设备登录 |
-
基于 Cookie 的实现(默认)
使用
TokenBasedRememberMeServices
:token = Base64(username + ":" + expiryTime + ":" + md5(username + expiryTime + password + key))
验证时:
- 取出 cookie;
- 解码;
- 验证签名是否有效;
- 若有效 → 自动加载用户信息。
缺点:Cookie 可被盗取(安全风险较高)。
-
基于数据库的实现
启用
PersistentTokenBasedRememberMeServices
:-
创建数据库表
Spring Security 需要一个固定结构的表:
sqlCREATE 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
其作用是:
- 检查请求中是否有 Remember-Me Cookie;
- 验证 token;
- 如果验证通过,自动登录用户;
- 创建新的
Authentication
; - 写入
SecurityContextHolder
。
手动清除 Remember-Me Cookie
当登出时,应同时清除 cookie(否则仍可自动登录):
java
http
.logout(logout -> logout
.deleteCookies("remember-me") // 删除 remember-me cookie
.invalidateHttpSession(true)
.clearAuthentication(true)
);