在将spring-session与spring-security集成时,我不确定当spring session标识会话时应该如何填充SecurityContextImpl#Authentication
。
上下文:spring-boot
应用程序实际上并不处理登录、注销或创建会话本身。会话是在一个外部的非spring微服务中创建的,并通过MongoDB共享。共享和映射会话信息是有效的,并且在春季安全之前没有出现任何问题。
什么有效:
- Spring会话正确解析会话id
- Spring会话从会话存储库(mongo)中检索会话(使用会话id),并填充属性
- 请求有一个填充的会话对象,其中包括所有属性
什么不起作用:
- 使用
http.authorizeRequests().antMatchers("admin/**").authenticated()
,然后使用请求和端点(使用会话cookie)决不能填充SecurityContext#Authenticated
可能的选项
a) 我知道,我可以实现一个自定义的CustomUserNameFromSessionFilter
来预填充Authenticated
在SecureContext中(但已验证=false),并将其放置在SecurityFilter链的早期。此外,我还实现了一个自定义的AuthenticationProviderCustomFromSessionAuthenticationProvider
,然后它会拾取Authenticated
,并在会话有效的情况下基本上设置authenticated=true
(在这一点上总是正确的)
b) 使用RememberMeAuthenticationFilter
,但我不确定该文档如何适合
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安全性,包括通过spring引导应用程序进行身份验证时,spring会话和spring安全性的集成将变得自然,而不需要任何其他功能。
当您第一次通过spring security的身份验证授权(登录)您的用户时,它将:
- 将
Authentication
对象存储在该请求的SecurityContext
中。-然后,SecurityContext
将被存储在HttpSession
中(如果存在的话),其中的spring会话是您将spring会话配置为(redis/mongo)的地方 - 使用公共会话数据中的密钥CCD_ 17权限的会话属性来存储CCD_
当您获取此身份验证后提供给您的会话id并发出额外请求时,会发生以下
- spring-session从存储中加载
HttpSession
(包括会话属性中的SecurityContext
,关键字为SPRING_SECURITY_CONTEXT
- spring-security将在
SecurityFilter
链中很早调用HttpSessionSecurityContextRepository
,并检查HttpSession
是否存在会话属性SPRING_SECURITY_CONTEXT
以及是否找到SecurityContext
。如果是,则使用该SecurityContext
并将其加载为current
请求SecurityContext
。由于此上下文包含先前身份验证中已通过身份验证的Authentication
对象,因此AuthenticationManager/Provider
将跳过身份验证,因为它已全部完成,并且您的请求被视为已通过身份认证
这是一种普通的方法,它有一个要求-身份验证过程(登录)需要在登录过程中将SecurityContext
写入HttpSession
对象。
我的案例-外部登录过程
在我的案例中,一个外部的、非spring引导的微服务正在处理整个登录过程。尽管如此,它还是将会话存储在(外部)会话存储中,在我的例子中就是MongoDB。
Spring会话配置正确,可以使用会话cookie/会话id读取此会话,并加载外部创建的会话。
最大的"但是"是,此外部登录不会在会话数据中存储任何SecurityContext
,因为它无法做到这一点。此时,如果您有一个创建会话的外部登录服务,并且它是一个春季启动服务,请确保正确编写SecurityContext
。因此,所有其他微服务都可以使用会话id和默认的spring会话/安全集成来正确地加载该会话(经过身份验证)。
由于这对我来说不是一个选项,如果对你来说也不是,以下解决方案似乎是春季启动/安全IMHO中的"设计"方式:
您可以实现自己的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