java - Spring Security手动登录最佳实践

标签 java spring spring-security

我正在使用 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 用于基于表单的登录相同),告诉他们发送标准 usernamepassword 形成参数,尽管它们不需要发送真实值 - 像 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/

相关文章:

java - 为什么Java配置后找不到这个Spring Security AuthenticationProvider?

java - 收集日志 ZIP 并上传到 Http 服务,无需临时文件

java - 在iText pdf中制作一个段落不分为两页

java - 播放 jar 中的 wav 文件 - java netbeans

java - 如何检测方法发布中同一时间的重复请求

Spring BeanCreationException : cannot resolve reference to bean exception

java - Spring OAuth2客户端,CSRF保护

Java/JAudiotagger : Mp3 wrong ID3 tag size

java - Spring - 响应文件流的正确方法是什么?

java - 如何在运行时修改Spring Security过滤器链?