2017-11-11 129 views
0

我正在使用spring security来实现程序化的手动用户登录。我有一个场景,我积极建立了用户的身份,并希望登录他们。我不知道他们的密码,因此不能使用常规登录代码路径将表单提交到url,哪个弹簧通过servlet Filter拦截,完成它的所有auth +会话魔术。春季安全手册登录最佳实践

我已经搜查,似乎大多数人创建自己的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) 
; 

关键在上述各点:
- 我使用自定义成功和失败的处理程序表单登录
- 我想配置每用户
最大并发会话的行为 - 我想保持弹簧的默认会话固定保护(根据不断变化的会话ID登录)。
- 我想使用会话注册表
- ...更多,我选择配置它。

我翻遍了代码,看看spring如何处理表单登录。正如预期的那样,Spring使用我的HttpSecurity配置告诉它在我使用表单登录时执行的所有操作。但是,当我通过SecurityContextHolder.getContext().setAuthentication()进行我自己的自定义/手动登录时,它没有任何作用。这是因为Spring在servlet Filter中完成了auth + session的所有内容,而我的编程代码无法真正调用Filter。现在,我可以尝试自己添加缺少的行为,并复制其代码:我看到过滤器使用:ConcurrentSessionControlAuthenticationStrategy,ChangeSessionIdAuthenticationStrategyRegisterSessionAuthenticationStrategy。我可以自己创建这些对象,配置它们,并在我的自定义登录后调用它们。但是,以这种方式复制它有点蹩脚。此外,还有其他一些我缺少的行为 - 我注意到当使用表单登录代码路径时,该弹簧触发了一些登录事件,这些登录事件在我执行自定义登录时不会触发。而且可能还有其他我缺少或不理解的东西。整个过程非常复杂。

所以,我觉得我从错误的方式接近这一点。 我是否应该使用不同的策略,以避免跳过像Spring那样的东西?也许我应该尝试让自己的AuthenticationProvider完成此自定义登录?

*为了澄清,我的代码或多或少的作品。但是,我觉得我用一个糟糕的策略来实现它,因为我必须编写代码来复制春天为我做的很多东西。此外,我的代码并没有完美地复制春天做的事情,这让我不知道会产生什么负面影响。必须有更好的方式来以编程方式实现登录。

+1

这似乎过于宽泛。您是否可以减少内容以提供有关该问题的相关信息并发布[MCVE](https://stackoverflow.com/help/mcve)以进行复制。 –

+1

@dur我的自定义(手动)登录在其初始帐户创建后立即使用。我给他们发电子邮件并让他们点击一个链接来验证他们的帐户。点击链接后,我登录他们,而不再让他们再次输入凭据。由于在这种情况下我没有密码,因此我不能使用任何“正常”的春季认证机制,例如“ActiveDirectoryLdapAuthenticationProvider”。如果他们忘记密码,我也会做同样的事情,允许他们有限的帐户访问权限,而不必更改/重置密码,这对我的使用情况非常重要。 – goat

+0

@dur,更进一步,*即使我确实有密码*,但我真的没有办法让春天为我做所有这些事情,因为春天它在servlet'Filter'中很神奇,很好在执行我的应用程序代码之前(例如在控制器中)。我需要通过编程方式进行建立登录状态的调用,以及我列出的所有关联会话内容。 – goat

回答

1

我想阐述一下我是如何实现的dur意见。在我的场景中,我只使用了自定义的AuthenticationProvider。 而不是创建一个自定义的servlet Filter的,如延长AbstractAuthenticationProcessingFilter,这似乎是一个大量的工作,我选择,而不是使用以下策略:

  • 在点在我的代码,我相信我有确定用户,并希望他们“登录”,我在用户的会话中插入一个标志,标记他们应该登录下一个请求,以及我需要的任何其他身份/簿记信息,例如他们的用户名。
  • 然后,我告诉浏览器客户端发送一个http post到loginProcessingUrl(我配置的spring安全用于基于表单的登录),告诉他们发送标准的usernamepassword form params,虽然他们不喜欢不需要发送实际值 - 像foo这样的虚拟值很好。
  • 当用户发出这个帖子请求时(例如到/login),spring会调用我的自定义AuthenticationProvider,它会查看用户的会话以检查标志并收集用户名。然后它将创建并返回一个Authentication对象,如PreAuthenticatedAuthenticationToken,它标识用户。
  • 春天将处理其余的。现在的用户登录

通过做这种方式,你留做登录的“正常”的方式中,所以春天仍然会自动:

  • 呼叫任何自定义成功和失败为表单登录配置的处理程序,如果您使用该位置在登录时执行某些操作(如查询或更新数据库),这很好。
  • 它将尊重您可能正在使用的每个用户设置的最大并发会话数。
  • 您可以保留spring的默认会话固定攻击保护(登录时更改会话ID)。
  • 如果您设置自定义会话超时,例如通过属性文件中的server.session.timeout,spring将使用它。可能还有其他会话配置属性也在这个时候完成。
  • 如果您启用了spring的“记住我”功能,它将起作用。
  • 它会触发登录事件,该事件用于其他弹簧组件,例如将用户的会话存储在SessionRegistry中。我认为这些事件也被春季其他地区使用,如执行器和审计。

当我第一次尝试只是做了一般建议SecurityContextHolder.getContext().setAuthentication(authentication)登录我的用户,而不是自定义AuthenticationProvider,以上都不是子弹都做对我来说,它可以完全打破您的应用程序......或造成细微的安全性错误 - 既不好。

下面是一些代码,以帮助巩固我说:

自定义的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); 
    } 

} 

配置春季安全使用提供

// In your WebSecurityConfigurerAdapter 
@Configuration 
@EnableWebSecurity 
public class AppLoginConfig extends WebSecurityConfigurerAdapter { 
    @Autowired 
    private AccountVerificationAuthenticationProvider accountVerificationAuthenticationProvider; 

    @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 IO to check, 
     // so it's very fast. Only if that one rejects, do we then try the active directory one. 
     authenticationManagerBuilder 
      .authenticationProvider(accountVerificationAuthenticationProvider) 
      // I'm using ActiveDirectory, but whatever you want to use here should work. 
      .authenticationProvider(activeDirectoryLdapAuthenticationProvider); 
    } 

    @Override 
    protected void configure(HttpSecurity httpSecurity) throws Exception { 
     ... 
    } 
}