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

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

  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 的方式正确实现这一点?谢谢你。

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


慕婉清6462132
浏览 302回答 1
1回答

千巷猫影

好的,这就是我设法完成这项任务的方法。我有一个经过身份验证的用户(实际上是服务),角色为 ROLE_OAUTH 并打开了身份验证会话,以及有关会话中保留的上下文的一些关键信息,这些信息硬连接到 OAuth 请求中;现在,当尝试访问需要另一个角色(例如 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");    }}现在我们需要将这个新的处理程序添加到配置中:@Overrideprotected void configure(HttpSecurity http) throws Exception {    http        // all the config        .and()            .exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());}在此步骤之后,默认UsernamePasswordAuthenticationFilter将通过使用输入的凭据创建另一个 Authentication 对象来处理输入,默认行为只是丢失连接到先前 OAuth Authentication 对象的现有信息。所以我们需要通过扩展这个类来覆盖这个默认行为,就像这样,在标准的 UsernamePasswordAuthenticationFilter 之前添加这个过滤器。public class OAuthAwareUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {private static final Log LOG = LogFactory.getLog(LTIAwareUsernamePasswordAuthenticationFilter.class);@Overridepublic 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);}@Overrideprotected 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 之前添加这个过滤器,并将其仅应用于给定的端点:@Overrideprotected void configure(HttpSecurity http) throws Exception {    http        .csrf().disable()        .addFilterBefore(oauthAwareUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)        // here come ant rules        .and()        .formLogin()        .and()            .exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());}而已。这个例子被测试是可行的。以后可能会发现一些副作用,不确定。此外,我确信它可以以更精致的方式完成,但我现在将使用此代码。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Java