如何解决Spring Session 集成到 Spring Security - 如何填充身份验证
将 spring 会话与 spring 安全性集成时,我不确定当 spring 会话标识会话时应该如何填充 SecurityContextImpl#Authentication
。
背景:
spring-boot
应用程序实际上并不处理登录、注销或创建会话本身。会话是在外部非 spring 微服务中创建的,并通过 MongoDB 共享。共享和映射会话信息有效,并且在 spring 安全性之前没有问题。
什么有效:
- Spring session 正确解析 session-id
- Spring session 从 session-repository(mongo) 中检索 session(使用 session-id)并填充属性
- 请求有一个包含所有属性的填充会话对象
什么不起作用:
- 使用
http.authorizeRequests().antMatchers("admin/**").authenticated()
然后使用请求和端点(使用会话 cookie)绝不会填充SecurityContext#Authenticated
可能的选择
a) 我明白,我可以实现一个自定义的 CustomUserNameFromSessionFilter
来预填充
Authenticated
在 SecureContext 中(但已验证=false)并将其放在 SecurityFilter chain 的早期。此外,我实现了一个自定义的 AuthenticationProvider CustomFromSessionAuthenticationProvider
,然后选择 Authenticated
并在会话有效时基本上设置 authenticated=true
(此时始终为真)>
b) 使用 RememberMeAuthenticationFilter
,但我不确定这 documentation 是否适合该目的
c) 以某种方式利用 AbstractPreAuthenticatedProcessingFilter
但它似乎用于外部身份验证请求
每个选项似乎都不对,而且对这种实现的需求似乎太普遍了,以至于没有现有的/更好的解决方案。正确的做法是什么?
代码片段
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable();
// logout is yet handled by PHP Only,we yet cannot delete/write sessions here
http.logout().disable();
http.formLogin().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.authorizeRequests()
.antMatchers("/admin").authenticated();
}
解决方法
感谢@Steve Riesenberg 为我提供足够的提示,让我找到正确的解决方案!
要了解我的解决方案并了解何时需要走这条额外的路线,如果首先解释默认集成:
spring-security 中的经典 spring-session 集成
当您通过 spring-boot 应用程序使用包括身份验证在内的 spring-security 时,spring-session 和 spring-security 的集成将变得自然而然,无需任何其他操作。
当您首次通过 spring-security 身份验证授权(登录)您的用户时,它将:
- 将
Authentication
对象存储在该请求的SecurityContext
中。 - 然后SecurityContext
将存储在HttpSession
(如果存在)中,使用 spring-session 将 spring-session 配置为 (redis/mongo)。 -
SecurityContext
使用密钥SPRING_SECURITY_CONTEXT
的会话属性存储在公共会话数据(序列化)中。
当您在此身份验证后获取给您的 session-id 并发出额外请求时,会发生以下情况
- spring session 从您的存储中加载
HttpSession
(包括带有键SecurityContext
的 session 属性中的SPRING_SECURITY_CONTEXT
- spring security 将在
HttpSessionSecurityContextRepository
链的早期调用SecurityFilter
并检查HttpSession
是否存在会话属性SPRING_SECURITY_CONTEXT
以及SecurityContext
是否为成立。如果是,则使用此SecurityContext
并将其加载为current
请求SecurityContext
。由于此上下文包含先前身份验证中已通过身份验证的Authentication
对象,因此AuthenticationManager/Provider
将跳过身份验证,因为它已全部完成并且您的请求被视为已通过身份验证。
这是普通方式,它有一个要求 - 身份验证过程(登录)需要在登录过程中将 SecurityContext
写入 HttpSession
对象。
我的案例 - 外部登录过程
就我而言,一个外部的非 spring-boot 微服务正在处理整个登录过程。尽管如此,它将会话存储在(外部)会话存储中,在我的例子中是 MongoDB。
Spring-session 配置正确,可以使用 session cookie/session id 读取这个 session,并加载外部创建的 session。
最大的“但是”是,这个外部登录不会在会话数据中存储任何 SecurityContext
,因为它不能这样做。
此时 - 如果您有一个创建会话的外部登录服务,并且它是一个 spring-boot 服务,请确保正确编写 SecurityContext
。因此,所有其他微服务只需使用会话 ID 和默认的 spring-session/security 集成即可正确加载此会话(经过身份验证)。
因为这对我来说不是一个选择,如果你也没有,下面的解决方案似乎是 spring-boot/security 恕我直言的“设计”方式:
您实现自己的 CustomHttpSessionSecurityContextRepository
并通过
public class ApplicationSecurity extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.securityContext()
.securityContextRepository(new FromExternalSessionSecurityContextRepository());
}
}
这确保我们用自己的实现替换库存 HttpSessionSecurityContextRepository
。
现在我们的自定义实现
public class CustomExternalSessionSecurityContextRepository implements SecurityContextRepository
{
@Override
public SecurityContext loadContext(final HttpRequestResponseHolder requestResponseHolder)
{
HttpServletRequest request = requestResponseHolder.getRequest();
HttpSession httpSession = request.getSession(false);
// No session yet,thus we cannot load an authentication-context from the session. Create a new,blanc
// Authentication context and let others AuthenticationProviders deal with it.
if (httpSession == null) {
return generateNewSecurityContext();
}
Optional<Long> userId = Optional.ofNullable(httpSession.getAttribute(Attribute.SUBJECT_ID.attributeName))
SecurityContext sc = generateNewSecurityContext();
if (userId.isEmpty()) {
// Return a emtpy context if the session has neither no subjectId
return sc;
}
// This is an session of an authenticated user. Create the security context with the principal we know from
// the session and mark the user authenticated
// OurAuthentication uses userId.get() as principal and implements Authentication
var authentication = new OurAuthentication(userId.get());
authentication.setAuthenticated(true);
sc.setAuthentication(authentication);
httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY,sc);
return sc;
}
@Override
public void saveContext(
final SecurityContext context,final HttpServletRequest request,final HttpServletResponse response
)
{
// do implement storage back into HttpSession if you want spring-boot to be
// able to write it.
}
@Override
public boolean containsContext(final HttpServletRequest request)
{
HttpSession session = request.getSession(false);
if (session == null) {
return false;
}
return session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null;
}
private SecurityContext generateNewSecurityContext()
{
return SecurityContextHolder.createEmptyContext();
}
}
所以现在改变了 spring-security 如何从会话加载 SecurityContext
的行为。我们不期望 SecurityContext
已经存在,而是检查会话是否正确并从给定数据创建 SecurityContext
并存储返回它。这将使整个以下链使用并尊重此SecurityContext
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。