自定义认证(Authentication)的存储位置
默认情况下, Spring Security 在 HTTP 会话中为你存储 security context。然而,这里有几个原因,你可能想自定义:
首先,你需要创建一个 SecurityContextRepository
的实现,或者使用一个现有的实现,如 HttpSessionSecurityContextRepository
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| package com.example.eochadmin.config;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.security.core.context.DeferredSecurityContext; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.SecurityContextRepository;
public class MySecurityContextRepository implements SecurityContextRepository { private static final String SESSION_ATTR_NAME = "SPRING_SECURITY_CONTEXT";
@Override public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest(); HttpSession session = request.getSession(false); if (session != null) { return (SecurityContext) session.getAttribute(SESSION_ATTR_NAME); } else { return null; } }
@Override public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) { return SecurityContextRepository.super.loadDeferredContext(request); }
@Override public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession(true); session.setAttribute(SESSION_ATTR_NAME, context); }
@Override public boolean containsContext(HttpServletRequest request) {
HttpSession session = request.getSession(false); return session != null && session.getAttribute(SESSION_ATTR_NAME) != null; } }
|
然后你可以在 HttpSecurity
中设置它。
1 2 3 4 5 6 7 8 9 10
| @Configuration public class SecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { MySecurityContextRepository repo = new MySecurityContextRepository(); http.securityContext((context) -> context.securityContextRepository(repo)); return http.build(); } }
|
在上面的示例中,确实是通过 HttpSession
存储了安全上下文(即 SecurityContext
对象),而不是直接存储认证用户的详细信息。让我来进一步解释:
1. 存储的内容:
HttpSession
中存储的是 SecurityContext
对象,而 SecurityContext
包含了 Authentication
对象,后者表示认证用户的详细信息(例如用户名、权限等)。
2. SecurityContext 中的 Authentication 对象:
SecurityContext
是 Spring Security 中用来持有当前用户的信息的容器。
Authentication
对象包含了认证用户的详细信息,它是 Principal
(主体)和 GrantedAuthority
(授权信息)的封装。
3. 存储过程:
- 在上述示例中,`saveContext` 方法将整个 SecurityContext
对象存储在 HttpSession
中的 SPRING_SECURITY_CONTEXT
属性中。
- 这意味着在会话中,我们可以通过 SecurityContext
对象来获取 Authentication
对象,进而获取认证用户的详细信息。
4. 认证用户信息的存储:
- 认证用户的具体信息(例如用户名、权限)通常包含在 Authentication
对象中。
- Spring Security 在认证成功后,会将有效的 Authentication
对象设置到 SecurityContext
中,然后由 saveContext
方法负责将整个 SecurityContext
存储在 HttpSession
中
因此,虽然代码示例中直接操作的是 SecurityContext
,实际上 SecurityContext
中包含了 Authentication
对象,从而间接地存储了认证用户的信息。这种设计符合了 Spring Security 的认证和授权机制,确保了安全上下文和认证信息的正确管理和使用。
手动存储 Authentication
例如,在某些情况下,你可能要手动验证用户,而不是依靠 Spring Security filter。你可以使用自定义 filter 或 Spring MVC controller 端点来做到这一点。如果你想在请求之间保存 认证,例如在 HttpSession 中,你就必须这样做
1 2 3 4 5 6 7 8 9 10 11 12 13
| private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
@PostMapping("/login") public void login(@RequestParam("username") String username,@RequestParam("password") String password, HttpServletRequest request, HttpServletResponse response) { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated( username, password); MyAuthenticationManager authenticationManager = new MyAuthenticationManager(); Authentication authentication = authenticationManager.authenticate(token); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); securityContextRepository.saveContext(context, request, response); }
|
将 SecurityContextRepository
添加到 controller 中
注入 HttpServletRequest
和 HttpServletResponse
,以便能够保存 SecurityContext
使用提供的凭证创建一个未经 认证的UsernamePasswordAuthenticationToken
调用 AuthenticationManager#authenticate
来验证用户
创建一个 SecurityContext
,并在其中设置 Authentication
在 SecurityContextRepository
中保存 SecurityContext
配置无状态认证(Authentication)的持久化
有时不需要创建和维护一个 HttpSession,例如,在不同的请求中坚持认证。一些认证机制,如 HTTP Basic 是无状态的,因此,在每次请求时都会重新认证用户。
如果你不希望创建会话,你可以使用 SessionCreationPolicy.STATELESS,像这样
1 2
| http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
上述配置是将 SecurityContextRepository 配置 为使用 NullSecurityContextRepository,同时也是为了 防止请求被保存在会话中
配置并发会话控制
如果你希望对单个用户登录你的应用程序的能力进行限制, Spring Security 支持开箱即用,只需添加以下简单内容。首先,你需要在你的配置中添加以下 listener,以保持 Spring Security 对会话生命周期事件的更新
1 2 3 4
| @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); }
|
1 2 3 4 5 6 7 8
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http .sessionManagement(session -> session .maximumSessions(1) ); return http.build(); }
|
会话会自行过期,不需要做任何事情来确保 security context 被删除。也就是说, Spring Security 可以检测到会话过期的情况,并采取你指定的具体行动。例如,当用户用已经过期的会话发出请求时,你可能想重定向到一个特定的端点。这可以通过 HttpSecurity 中的 invalidSessionUrl 实现
1 2 3 4 5 6 7 8
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http .sessionManagement(session -> session .invalidSessionUrl("/invalidSession") ); return http.build(); }
|
请注意,如果你使用这种机制来检测会话超时,如果用户注销后没有关闭浏览器又重新登录,它可能会错误地报告一个错误。这是因为当你使会话失效时,session cookie 没有被清除,即使用户已经注销,也会重新提交。如果你的情况是这样,你可能想 配置注销来清除 session cookie。
定制失效会话的策略
invalidSessionUrl
是使用 SimpleRedirectInvalidSessionStrategy
实现 来设置 InvalidSessionStrategy
的方便方法。如果你想自定义行为,你可以实现 InvalidSessionStrategy
接口并使用 invalidSessionStrategy
方法进行配置
注销时清除 Session Cookies
你可以在注销时明确地删除 JESSIONID cookie,例如通过使用 logout handler 中的 Clear-Site-Data
header:
1 2 3 4 5 6 7 8
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http .logout((logout) -> logout .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES))) ); return http.build(); }
|
Copied!
这样做的好处是与容器无关,可以与任何支持 Clear-Site-Data
header 的容器一起工作。
作为一种替代方法,你也可以在 logout handler 中使用以下语法:
1 2 3 4 5 6 7 8
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http .logout(logout -> logout .deleteCookies("JSESSIONID") ); return http.build(); }
|
Copied