【问题标题】:Spring security: how to implement "two-step" authentication (oauth then form-based)?Spring security:如何实现“两步”身份验证(oauth 然后基于表单)?
【发布时间】:2019-05-11 01:03:11
【问题描述】:

在我的应用程序中,我有两个不同的身份验证网关,它们分别工作得很好:

  1. OAuth 1.0 (service-to-service) 将 ROLE_OAUTH 授予 用户;在这里,我对用户一无所知,只有一些关于它在 Principal 对象中使用的服务的上下文信息;
  2. 标准的基于表单的身份验证,将 ROLE_USER 授予 用户;在这里,我有关于我的用户的完整信息,但没有关于它在 Principal 对象中使用的服务的上下文信息;

现在我想实现两步验证:1)OAuth 然后基于表单。

复杂性在于我不想在第 1 步 (OAuth) 之后丢失存储在 Principal 中的特定于上下文的信息;我只想在完成基于表单的身份验证后向安全上下文添加一些新的用户特定信息以及一个新角色 ROLE_USER,所有这些都在同一个身份验证会话中。

能否顺利实施?如何在第二步(基于表单的身份验证)中提取现有的 Principal 信息并将其添加到新的 Principal 中?

有没有不重新发明轮子的“模板解决方案”?

我目前的直接解决方案是:

  1. 我已通过角色 ROLE_OAUTH 验证用户并打开 身份验证会话;
  2. 为二维步骤创建一个单独的路径,例如 /oauth/login;
  3. 用户输入他的凭据后,我在外部处理它们 控制器中的安全链手动检查凭据;
  4. 如果成功,手动更新安全上下文不丢失 身份验证会话,然后将用户重定向到受保护的请求 ROLE_USER 资源;

但我不喜欢它,因为我必须手动处理第二个安全请求,这似乎很蹩脚..

如何以 Spring-ish 的方式正确实现这一点?谢谢。

P.S.由于遗留原因,我必须使用 Oauth 1.0,无法将其升级到 v.2 或任何其他解决方案。

【问题讨论】:

    标签: java spring security oauth


    【解决方案1】:

    好的,这就是我完成这项任务的方法。

    1. 我有一个经过身份验证的用户(实际上是服务),角色为 ROLE_OAUTH 并打开了身份验证会话和一些关于上下文的关键信息 保留在硬连接到 OAuth 请求中的会话中;
    2. 现在,当尝试访问需要另一个角色(例如 ROLE_USER)的受保护资源时,Spring 给了我AccessDeniedException 并发送 403 禁止响应(请参阅AccessDeniedHandlerImpl),如果需要,请在自定义 AccessDeniedHandler 中覆盖默认行为.这是代码示例:

       public class OAuthAwareAccessDeniedHandler implements AccessDeniedHandler {
       private static final Log LOG = LogFactory.getLog(OAuthAwareAccessDeniedHandler.class);
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response,
                AccessDeniedException accessDeniedException) throws IOException, ServletException {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (oauthSecurityUtils.isUserWithOnlyOAuthRole(auth)) {
                LOG.debug("Prohibited to authorize OAuth user trying to access protected resource.., redirected to /login");
                // Remember the request pathway
                RequestCache requestCache = new HttpSessionRequestCache();
                requestCache.saveRequest(request, response);
                response.sendRedirect(request.getContextPath() + "/login");
                return;
            }
            LOG.debug("Ordinary redirection to /accessDenied URL..");
            response.sendRedirect(request.getContextPath() + "/accessDenied");
        }
    }
    
    1. 现在我们需要将这个新的处理程序添加到配置中:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // all the config
            .and()
                .exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());
    }
    
    1. 在此步骤之后,默认UsernamePasswordAuthenticationFilter 将通过使用输入的凭据创建另一个身份验证对象来处理输入,默认行为只是丢失连接到先前 OAuth 身份验证对象的现有信息。所以我们需要通过扩展这个类来覆盖这个默认行为,就像这样,在标准的 UsernamePasswordAuthenticationFilter 之前添加这个过滤器。

    public class OAuthAwareUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
    private static final Log LOG = LogFactory.getLog(LTIAwareUsernamePasswordAuthenticationFilter.class);
    
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
        // Check for OAuth authentication in place
        if (oauthSecurityUtils.isUserWithOnlyOAuthRole(previousAuth)) {
            LOG.debug("OAuth authentication exists, try to authenticate with UsernamePasswordAuthenticationFilter in the usual way");
            SecurityContextHolder.clearContext();
            Authentication authentication = null;
            try {// Attempt to authenticate with standard UsernamePasswordAuthenticationFilter
                authentication = super.attemptAuthentication(request, response);
            } catch (AuthenticationException e) {
                // If fails by throwing an exception, catch it in unsuccessfulAuthentication() method
                LOG.debug("Failed to upgrade authentication with UsernamePasswordAuthenticationFilter");
                SecurityContextHolder.getContext().setAuthentication(previousAuth);
                throw e;
            }
            LOG.debug("Obtained a valid authentication with UsernamePasswordAuthenticationFilter");
            Principal newPrincipal = authentication.getPrincipal();
            // Here extract all needed information about roles and domain-specific info
            Principal rememberedPrincipal = previousAuth.getPrincipal();
           // Then enrich this remembered principal with the new information and return it
            LOG.debug("Created an updated authentication for user");
            return newAuth;
        }
        LOG.debug("No OAuth authentication exists, try to authenticate with UsernamePasswordAuthenticationFilter in the usual way");
        return super.attemptAuthentication(request, response);
    }
    
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
        Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
        if (oauthSecurityUtils.isUserWithOnlyOAuthRole(previousAuth)) {
            LOG.debug("unsuccessfulAuthentication upgrade for OAuth user, previous authentication :: "+ previousAuth);
            super.unsuccessfulAuthentication(request, response, failed);
            LOG.debug("fallback to previous authentication");
            SecurityContextHolder.getContext().setAuthentication(previousAuth);
        } else {
            LOG.debug("unsuccessfulAuthentication for a non-OAuth user with UsernamePasswordAuthenticationFilter");
            super.unsuccessfulAuthentication(request, response, failed);
        }
    }
    

    }

    唯一剩下的就是在 UsernamePasswordAuthenticationFilter 之前添加这个过滤器,并且只将它应用到给定的端点:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .addFilterBefore(oauthAwareUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            // here come ant rules
            .and()
            .formLogin()
            .and()
                .exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());
    }
    

    就是这样。这个例子被测试是可行的。以后可能会发现一些副作用,不确定。此外,我确信它可以以更精致的方式完成,但我现在将使用此代码。

    【讨论】:

      猜你喜欢
      • 2015-02-06
      • 2010-11-20
      • 2015-03-27
      • 2013-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-25
      • 2017-07-08
      • 2018-08-25
      相关资源
      最近更新 更多