spring security 手动登录最佳实践
Posted
技术标签:
【中文标题】spring security 手动登录最佳实践【英文标题】:spring security manual login best practice 【发布时间】:2018-04-24 07:07:42 【问题描述】:我正在使用 Spring Security 来实现程序化的手动用户登录。我有一个场景,我已经确定了用户的身份,并希望让他们登录。我不知道他们的密码,所以不能使用常规的登录代码路径,你可以将表单提交到一个 url,哪个 spring通过 servlet Filter
进行拦截,完成所有的 auth+session 魔术。
我搜索过,似乎大多数人都创建了自己的Authentication
对象,然后通过以下方式告诉spring:
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(user, "", user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
确实,这行得通。 Spring 甚至为我将其放入会话中,使后续的 http 请求保持其身份验证状态。
但是,我觉得这是一个肮脏的黑客。我将介绍一些细节,希望能给出与在控制器中使用setAuthentication()
来实现手动登录相关的问题的具体示例:
给出一个想法,我的配置是:
httpSecurity
.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/admin/**", "/api/admin/**").hasRole("USER_SUPER_ADMIN")
.and()
.formLogin()
.loginPage("/sign-in?sp")
.loginProcessingUrl("/api/auth/sign-in")
.successHandler(createLoginSuccessHandler())
.failureHandler(createLoginFailureHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/api/auth/sign-out")
.logoutSuccessHandler(createLogoutSuccessHandler())
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry)
;
以上配置中的关键点:
我对表单登录使用自定义成功和失败处理程序 我想为每个用户的最大并发会话配置行为 我想保持 Spring 的默认会话固定保护(登录时更改会话 ID)。 我想使用会话注册表 ...更多这些会话/登录功能,如果我选择配置的话。我单步执行了代码以了解 Spring 如何处理表单登录。正如预期的那样,当我使用表单登录时,Spring 完成了我的HttpSecurity
配置告诉它执行的所有会话/登录功能。但是,当我通过SecurityContextHolder.getContext().setAuthentication()
进行自己的自定义/手动登录时,它没有这些功能。这是因为 spring 在 servlet Filter
中完成了所有会话/登录功能,而我的程序代码不能真正调用过滤器。现在,我可以尝试自己添加缺少的功能,复制他们的代码:我看到 spring 过滤器使用:ConcurrentSessionControlAuthenticationStrategy
、ChangeSessionIdAuthenticationStrategy
和 RegisterSessionAuthenticationStrategy
。我可以自己创建这些对象,配置它们,并在我的自定义登录后调用它们。但是,复制所有的 spring 代码真的很糟糕。此外,我还缺少其他行为 - 我注意到在使用表单登录代码路径时,spring 会触发一些登录事件,这些事件在我进行自定义登录时不会被触发。可能还有其他我遗漏或不理解的东西。整个过程相当复杂,我觉得如果做得不好很有可能引入错误,更不用说如果我开始复制spring代码,库更新会很痛苦。
所以,我觉得我从错误的方式来处理这个问题。 我应该使用不同的策略,这样我才不会绕过 spring 为我做的太多事情?也许我应该尝试创建自己的 AuthenticationProvider
来完成这个自定义登录?
*澄清一下,我的代码或多或少有效。但是,我觉得我使用了一个糟糕的策略来完成它,因为我必须编写代码来复制 Spring 为我所做的很多事情。此外,我的代码并没有完美地复制 spring 所做的事情,这让我想知道可能会产生什么负面影响。必须有更好的方式以编程方式实现登录。
【问题讨论】:
似乎太宽泛了。您能否减少内容以提供有关问题的相关信息并发布MCVE 以重现它。 @dur 我的自定义(手动)登录在他们初始帐户创建后立即使用。我给他们发电子邮件,让他们点击一个链接来验证他们的帐户。单击该链接后,我无需让他们再次输入凭据即可登录。由于在这种情况下我没有密码,因此我无法使用任何“正常”的 spring 身份验证机制,例如ActiveDirectoryLdapAuthenticationProvider
。如果他们忘记密码,我也会做同样的事情,允许他们限制帐户访问,而不必更改/重置密码,这对我的用例非常重要。
@dur,进一步说,即使我确实有密码,我真的没有办法让 spring 为我完成所有这些工作,因为spring 在我的应用程序代码(例如在控制器中)执行之前,它在 servlet Filter
中很神奇。我需要以编程方式进行调用以建立登录状态以及我列出的所有相关会话内容。
@RomanC 我认为我的问题一点也不宽泛。我正在向非常精通 Spring Security 的人寻求策略建议。我提供了详细信息,以具体演示人们使用 spring 进行手动登录的典型方式存在的问题。细节只是给出背景——我认为这不会使问题变得广泛。我认为在这里提供可重现的代码示例没有多大价值 - 我的代码有效,我只是不喜欢这种策略,并且觉得专家可能知道更好的方法。
【参考方案1】:
我想详细说明我是如何实施dur 的建议的。在我的场景中,我只使用了自定义AuthenticationProvider
。
我没有创建自定义 servlet Filter
,例如扩展 AbstractAuthenticationProcessingFilter
,这似乎需要做很多工作,而是选择使用以下策略:
loginProcessingUrl
(与我配置spring security用于基于表单登录的相同)发送一个http post,告诉他们发送标准username
和password
表单参数,尽管它们不需要发送实际值 - 像 foo
这样的虚拟值就可以了。
当用户发出该发布请求时(例如发送到/login
),spring 将调用我的自定义AuthenticationProvider
,它将在用户的会话中检查标志,并收集用户名。然后它将创建并返回一个Authentication
对象,例如PreAuthenticatedAuthenticationToken
,用于标识用户。
Spring 将处理剩下的事情。用户现在已登录。
通过这种方式,您可以保持“正常”的登录方式,因此 spring 仍然会自动:
调用您为表单登录配置的任何自定义成功和失败处理程序,如果您使用该位置在登录时执行某些操作(例如查询或更新数据库),这很好。 它将遵循您可能正在使用的每个用户设置的最大并发会话数。 您可以保留 Spring 的默认会话固定攻击保护(登录时更改会话 ID)。 如果您设置自定义会话超时,例如通过属性文件中的server.session.timeout
,spring 将使用它。此时可能还有其他会话配置属性。
如果您启用了 spring 的“记住我”功能,它将起作用。
它将触发登录事件,该事件用于其他spring组件,例如将用户的会话存储在SessionRegistry
中。我认为这些事件也被 spring 的其他部分(例如执行器)用于审计。
当我第一次尝试只使用通常推荐的 SecurityContextHolder.getContext().setAuthentication(authentication)
而不是自定义 AuthenticationProvider
来登录我的用户时,上述项目符号都没有为我完成,这可能会彻底破坏您的应用程序......或导致微妙的安全漏洞 - 两者都不好。
这里有一些代码可以帮助巩固我所说的:
自定义身份验证提供者
@Component
public class AccountVerificationAuthenticationProvider implements AuthenticationProvider
@Autowired
private AppAuthenticatedUserService appAuthenticatedUserService;
@Autowired
private AuthService authService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
// This will look in the user's session to get their username, and to make sure the flag is set to allow login without password on this request.
UserAccount userAccount = authService.getUserAccountFromRecentAccountVerificationProcess();
if (userAccount == null)
// Tell spring we can't process this AuthenticationProvider obj.
// Spring will continue, and try another AuthenticationProvider, if it can.
return null;
// A service to create a custom UserDetails object for this user.
UserDetails appAuthenticatedUser = appAuthenticatedUserService.create(userAccount.getEmail(), "", true);
PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken(appAuthenticatedUser, "", appAuthenticatedUser.getAuthorities());
authenticationToken.setAuthenticated(true);
return authenticationToken;
@Override
public boolean supports(Class<?> authentication)
return authentication.equals(UsernamePasswordAuthenticationToken.class);
配置 spring security 以使用提供者
// In your WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class AppLoginConfig extends WebSecurityConfigurerAdapter
@Autowired
private AccountVerificationAuthenticationProvider accountVerificationAuthenticationProvider;
@Autowired
private ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception
// Spring will try these auth providers in the order we register them.
// We do the accountVerificationAuthenticationProvider provider first, since it doesn't need to do any slow IO to check,
// so it's very fast. Only if this AuthenticationProvider rejects (which means this http request is not for programmatic login), will spring then try the next AuthenticationProvider in the list.
authenticationManagerBuilder
.authenticationProvider(accountVerificationAuthenticationProvider)
// I'm using ActiveDirectory / LDAP for when a user logs in via entering a user + password via the html form, but whatever you want to use here should work.
.authenticationProvider(activeDirectoryLdapAuthenticationProvider);
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
...
【讨论】:
对我来说,这听起来不像是一个通用的解决方案。如果没有一个特定的“代码中的位置”可以识别用户怎么办?例如,假设我们通过电子邮件发送 URL,URL 中包含一次性身份验证令牌。该 URL 可能会出现在任何地方……所以没有一个特定的地方可以检查他们的凭据。 假设我们通过实际添加一个过滤器来解决这个问题......并以某种方式将用户的浏览器重定向到登录表单......这意味着用户可以结束的唯一地方就是家页面,从登录表单重定向后?如果我们希望它们出现在许多网页中的任何一个中,运气不好?我远不是这方面的专家,我发现您的 cmets 和想法很有趣,但我看不出它如何作为通用解决方案发挥作用。 @xpusostomos 一般解决什么问题?我觉得您在描述与此问答无关的功能和要求。我可以想到几种方法来实现您提到的功能 - 数以百万计的网站使用这种策略:在页面加载时,检查用户是否登录?不?然后将当前页面存储在 url 或会话中,重定向到登录页面,登录后将用户重定向回存储的页面。你甚至可以通过狡猾的内部子请求和/或 servlet 转发来避免重定向,可能会从子请求中中继会话 id/cookie,或者使用不同的策略而不是我的。 您所描述的内容已经存在于 Spring 中。在您进入页面之前,我更多的是考虑身份验证......就像一次性使用密码恢复 url 登录......公平地说,可能没有多少人愿意这样做。但是我会说实现自己的过滤器比你想象的要简单得多。在 AbstractAuthenticationProcessingFilter 中执行的唯一有趣的额外步骤是调用 rememberMeServices... 如果您从该类继承(例如 UsernamePasswordAuthenticationFilter),您甚至不必显式调用它。 ChangeSessionIdAuthenticationStrategy 已过时,无论 ConcurrentSessionControlAuthenticationStrategy 做什么,它都独立于您编写的任何过滤器。所以真正更简单的解决方案是在会话中推一些东西,编写你自己的继承 AbstractAuthenticationProcessingFilter 的过滤器,覆盖尝试身份验证以返回成功,你应该完成。【参考方案2】:对于自定义 Web 身份验证,您应该实现自定义身份验证过滤器(例如 AbstractAuthenticationProcessingFilter
或只是 GenericFilterBean
)、自定义身份验证提供程序 (AuthenticationProvider
) 或/和自定义身份验证令牌 (AbstractAuthenticationToken
) 的组合。
例如,查看Spring Security Kerberos的来源。
另见:
The AuthenticationManager, ProviderManager and AuthenticationProvider【讨论】:
以上是关于spring security 手动登录最佳实践的主要内容,如果未能解决你的问题,请参考以下文章
Spring Security 玩出花!两种方式 DIY 登录
使用 spring security 和/或 shiro 的最佳实践
确保 AUTHENTICATED 用户被授权访问资源的最佳实践 - Spring mvc、Spring Security、Hibernate