Spring Security:如何实现“两步式”身份验证(oauth然后基于表单)?

时间:2018-12-10 06:21:00

标签: java spring security oauth

在我的应用中,我有两个不同的身份验证网关,分别可以很好地工作:

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

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

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

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

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

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

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

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

如何才能以Spring-ish方式正确实现此目的?谢谢。

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

1 个答案:

答案 0 :(得分:0)

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

  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将通过使用输入的凭据创建另一个Authentication对象来处理输入,默认行为只是丢失连接到先前OAuth Authentication对象中的现有信息。因此,我们需要通过扩展此类来覆盖此默认行为,例如,在标准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());
}

就是这样。该示例经过测试是可行的。不确定之后可能会发现一些副作用。另外,我敢肯定,可以通过更精细的方式来完成此操作,但现在我将继续使用此代码。