我正在使用 Spring Security 来实现编程式手动用户登录。我有一个场景,我已经确定了用户的身份,并希望登录他们。我不知道他们的密码,因此无法使用常规登录代码路径,在该路径中将表单提交到 url,该路径会在通过 servlet Filter
进行拦截,完成所有的 auth+session 魔法。
我搜索过,似乎大多数人都会创建自己的Authentication
对象,然后通过以下方式告诉spring:
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(user, "", user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
确实,这有效。 Spring甚至帮我将其放入 session 中,使后续的http请求保持其身份验证状态。
但是,我觉得这是一个肮脏的黑客行为。我将介绍一些细节,希望能够给出与在 Controller 内使用 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)
;
上述配置的要点:
- 我使用自定义成功和失败处理程序进行表单登录
- 我想配置每个用户最大并发 session 数的行为
- 我想维护 Spring 的默认 session 固定保护(在登录时更改 session ID)。
- 我想使用 session 注册表
- ...更多这些 session /登录功能,如果我选择配置的话。
我单步执行代码来了解 spring 如何处理表单登录。正如预期的那样,当我使用表单登录时,Spring 会执行我的 HttpSecurity 配置告诉它执行的所有 session /登录功能。但是,当我通过 SecurityContextHolder.getContext().setAuthentication() 进行自己的自定义/手动登录时,它不执行任何这些功能。这是因为 spring 在 servlet Filter
内部完成所有 session /登录功能,而我的编程代码无法真正调用 Filter。现在,我可以尝试自己添加缺少的功能,复制它们的代码:我看到 spring Filter 使用:ConcurrentSessionControlAuthenticationStrategy、ChangeSessionIdAuthenticationStrategy 和 RegisterSessionAuthenticationStrategy >。我可以自己创建这些对象,配置它们,并在自定义登录后调用它们。但是,复制所有 Spring 代码确实很蹩脚。此外,我还缺少其他行为 - 我注意到,当使用表单登录代码路径时,spring 会触发一些登录事件,而当我进行自定义登录时,这些事件不会被触发。可能还有其他我遗漏或不理解的东西。整个过程相当复杂,我觉得如果做得不对,很有可能引入错误,更不用说如果我开始复制 spring 代码,库更新将是一件痛苦的事情。
所以,我觉得我正在以错误的方式处理这个问题。 我应该使用不同的策略,这样我就不会绕过 spring 为我做的那么多事情吗?也许我应该尝试制作自己的AuthenticationProvider
完成这个自定义登录吗?
*澄清一下,我的代码或多或少有效。但是,我觉得我使用了一个糟糕的策略来完成它,因为我必须编写代码来重复 spring 为我做的很多事情。此外,我的代码并没有完美地复制 spring 的功能,这让我想知道可能会产生什么负面影响。必须有更好的方法来以编程方式实现登录。
最佳答案
我想详细说明我是如何实现 dur 的建议的。在我的场景中,我只使用了自定义的 AuthenticationProvider 。
我选择使用以下策略,而不是创建自定义 servlet Filter
(例如扩展 AbstractAuthenticationProcessingFilter
),这似乎需要大量工作:
- 在我的代码中,当我确信我已经识别出用户并希望他们“登录”时,我在用户 session 中添加了一个标志,标记他们应该在下一个请求时登录,以及我需要的任何其他身份/簿记信息,例如他们的用户名。
- 然后,我告诉浏览器客户端向
loginProcessingUrl
发送一个 http post(与我配置 spring security 用于基于表单的登录相同),告诉他们发送标准username
和password
形成参数,尽管它们不需要发送真实值 - 像foo
这样的虚拟值就可以了。 - 当用户发出该 post 请求时(例如发送到
/login
),spring 将调用我的自定义AuthenticationProvider
,它将在用户的 session 中查找该标志,并收集用户名。然后,它将创建并返回一个Authentication
对象,例如用于标识用户的PreAuthenticatedAuthenticationToken
。 - Spring 将处理剩下的事情。用户现已登录。
通过这样做,您可以保持“正常”的登录方式,因此 spring 仍然会自动:
- 调用您为表单登录配置的任何自定义成功和失败处理程序,如果您使用该位置在登录时执行某些操作(例如查询或更新数据库),这会很好。
- 它将尊重您可能使用的每个用户的最大并发 session 数设置。
- 您可以保留 Spring 的默认 session 固定攻击保护(登录时更改 session ID)。
- 如果您设置自定义 session 超时(例如通过属性文件中的
server.session.timeout
),spring 将使用它。此时可能还完成了其他 session 配置属性。 - 如果您启用了 spring 的“记住我”功能,它将起作用。
- 它将触发一个登录事件,该事件用于其他 Spring 组件,例如将用户的 session 存储在
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 {
...
}
}
关于java - Spring Security手动登录最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47233187/