集成Spring Security OAuth2和Spring Social

34
我正在使用Spring Boot + Spring Security OAuth2应用程序工作,我认为它是受到了Dave Syer的示例启发而创建的。该应用程序被配置为OAuth2授权服务器,具有使用Resource Owner Password Credentials流程的单个公共客户端。成功令牌被配置为JWT。
公共Angular客户端发送一个带有基本身份验证头包含客户端ID和密钥的POST请求到/oauth/token(即使密钥不是私有的,这也是让客户端进行身份验证最简单的方法)。请求的主体包含“密码”的用户名、密码和授权类型。
除了作为认证服务器外,该应用程序还是用户、团队和组织的RESTful资源服务器。
我试图添加一个使用Spring Social的额外SSO身份验证流程。我已经将Spring Social配置为通过/ auth / [provider]验证外部提供程序;但是,在后续请求中,SecurityContext未正确设置。可能是因为Spring Security OAuth服务器或客户端覆盖了SecurityContext?
如果我可以在Spring Social流程之后正确设置SecurityContext,我就有了一个新的TokenGranter,允许检查SecurityContextHolder中的预验证用户的新授权类型“social”。
我对解决我与SecurityContext的特定问题的解决方案感兴趣(我认为这是Spring OAuth + Social集成的问题),或者尝试使用外部提供程序进行身份验证并从我们自己的身份验证服务器获取有效的JWT的不同方法。
谢谢!

你是否有任何可以分享的代码?您所描述的设置有点具有挑战性,因此如果有一些可以直接运行的代码供我们开始使用,而不是自己构建这样一个示例,那么我们可能会更快地为您找到答案。 - Craig Walls
4个回答

12

我在由 JHipster 生成的Web应用程序中遇到了类似的问题。最终,我决定选择来自Spring Social的SocialAuthenticationFilter选项(通过SpringSocialConfigurer)。成功进行社交登录后,服务器会通过重定向向客户端应用程序生成并返回“自己”的访问令牌。

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter implements EnvironmentAware {

    //...

    @Inject
    private AuthorizationServerTokenServices authTokenServices;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        SpringSocialConfigurer socialCfg = new SpringSocialConfigurer();
        socialCfg
            .addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
                @SuppressWarnings("unchecked")
                public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
                    filter.setAuthenticationSuccessHandler(
                            new SocialAuthenticationSuccessHandler(
                                    authTokenServices,
                                    YOUR_APP_CLIENT_ID
                            )
                        );
                    return filter;
                }
            });

        http
            //... lots of other configuration ...
            .apply(socialCfg);
    }        
}

并且是 SocialAuthenticationSuccessHandler 类:

public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    public static final String REDIRECT_PATH_BASE = "/#/login";
    public static final String FIELD_TOKEN = "access_token";
    public static final String FIELD_EXPIRATION_SECS = "expires_in";

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final AuthorizationServerTokenServices authTokenServices;
    private final String localClientId;

    public SocialAuthenticationSuccessHandler(AuthorizationServerTokenServices authTokenServices, String localClientId){
        this.authTokenServices = authTokenServices;
        this.localClientId = localClientId;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
                    throws IOException, ServletException {
        log.debug("Social user authenticated: " + authentication.getPrincipal() + ", generating and sending local auth");
        OAuth2AccessToken oauth2Token = authTokenServices.createAccessToken(convertAuthentication(authentication)); //Automatically checks validity
        String redirectUrl = new StringBuilder(REDIRECT_PATH_BASE)
            .append("?").append(FIELD_TOKEN).append("=")
            .append(encode(oauth2Token.getValue()))
            .append("&").append(FIELD_EXPIRATION_SECS).append("=")
            .append(oauth2Token.getExpiresIn())
            .toString();
        log.debug("Sending redirection to " + redirectUrl);
        response.sendRedirect(redirectUrl);
    }

    private OAuth2Authentication convertAuthentication(Authentication authentication) {
        OAuth2Request request = new OAuth2Request(null, localClientId, null, true, null,
                null, null, null, null);
        return new OAuth2Authentication(request,
                //Other option: new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), "N/A", authorities)
                new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A")
                );
    }

    private String encode(String in){
        String res = in;
        try {
            res = UriUtils.encode(in, GeneralConstants.ENCODING_UTF8);
        } catch(UnsupportedEncodingException e){
            log.error("ERROR: unsupported encoding: " + GeneralConstants.ENCODING_UTF8, e);
        }
        return res;
    }
}

只要在SocialAuthenticationSuccessHandler中设置相应的REDIRECT_PATH_BASE,你的客户端应用程序就会通过重定向到/#/login?access_token=my_access_token&expires_in=seconds_to_expiration接收到你的Web应用程序的访问令牌。

希望能对你有所帮助。


YOUR_APP_CLIENT_ID 是什么?GeneralConstants.ENCODING_UTF8 是什么静态常量? - Wim Deblauwe
公共静态最终字符串 ENCODING_UTF8 = "UTF-8"。 - rbarriuso
在添加了 UsersConnectionRepositorySocialAuthenticationServiceLocatorSocialUserDetailsService 的 beans 后,我成功让我的应用程序启动了。我还不得不在 apply 方法中添加一个强制转换,像这样:.and().apply((SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity>) socialCfg)。但是现在如何从 HTML 使用它呢? - Wim Deblauwe
关于 YOUR_APP_CLIENT_ID,你可以猜到,它是你想分配给OAuth服务提供商的客户端ID。 - rbarriuso
我尝试添加了SocialAuthenticationSuccessHandler,但是应用程序从未进入此代码(使用调试器进行了检查)。您是否通过访问例如 /auth/facebook 来启动处理过程? - Wim Deblauwe
是的,访问 http://localhost:8080/auth/facebook 会重定向到 Facebook,然后返回到定义的回调地址。 - rbarriuso

5

首先,我强烈建议您不要在这种情况下使用密码授权

公共客户端(JavaScript、安装应用程序)无法保护其客户端秘密的机密性,因此不应分配客户端秘密:任何检查您的 JavaScript 代码的访问者都可以发现秘密,从而实现您拥有的相同身份验证页面,并在该过程中存储您的用户密码。

为此,隐式授权已经被创建出来了。

使用基于重定向的流程的优点是将认证机制留给授权服务器处理,而不是让每个应用程序都承担其中一部分:那基本上就是单点登录(SSO)的定义。

话说回来,您的问题与我刚回答的这个问题紧密相关:Own Spring OAuth2 server together with 3rdparty OAuth providers

总的来说,答案如下:

最终,这取决于您的授权服务器如何保护AuthorizationEndpoint:/oauth/authorize。由于您的授权服务器正在工作,因此您已经有一个扩展了WebSecurityConfigurerAdapter配置类来处理/oauth/authorize的安全性,并使用formLogin进行认证。这就是您需要集成社交内容的地方。

您无法使用密码授权来实现您要实现的功能,您必须让公共客户端重定向到授权服务器。授权服务器将重定向到社交登录作为其/oauth/authorize端点的安全机制。


1
我想参与这个对话。我迷失了。我也有一个独立的自定义认证服务器,但不明白如何在我的WebSecurityConfigurerAdapter中“集成社交平台”。 我正在考虑使用社交平台进行身份验证,并在后续身份验证阶段将此令牌交换为自己的JWT。谢谢! - maret
Michael,你是否建议避免使用密码授权,因为Web GUI托管在与服务器端相同的服务器上?如果是这样,你推荐不使用密码授权的特定原因是什么?例如,该特定流程类型在移动设备上非常有效,移动设备通过HTTPS发送凭据,检索JWT,然后将JWT用于未来的通信。你是否认为在这种特定情况下,在托管Web应用程序中使用密码授权会提供虚假的安全感? - Kent Bull
@KentJohnson,密码授权有有效的用例(自己安装的应用程序--桌面/移动设备),只是对于Web应用程序来说有更好的解决方案,即隐式授权。就像我所说的,根据定义,密码授权告诉您如何验证用户(用户名+密码),并将该逻辑强制应用于每个Web应用程序中。基于重定向的流程将身份验证(用户名+密码)与授权/身份验证的证明(令牌)分离开来,将第一部分留给授权服务器。这在应用程序中的代码量也可以说更少。 - Michael Técourt
从安全角度来看,让一个应用程序负责存储和收集用户凭据比让多个应用程序通过网络发送用户凭据更少风险。最后一个论点是,您可以使用隐式授权作为单点登录机制,这意味着即使用户在不同的Web应用程序之间导航,只要您的授权服务器具有某种会话,用户只需连接一次即可。 - Michael Técourt
当然,绝不要让第三方使用密码授权与您的用户凭据。 - Michael Técourt
显示剩余2条评论

1
我从上面的好答案开始(https://dev59.com/nVwY5IYBdhLWcg3wsplj#33963286),但是在我的 Spring Security 版本(4.2.8.RELEASE)中失败了。原因是在 org.springframework.security.access.intercept.AbstractSecurityInterceptor#authenticateIfRequired 中,答案中的 PreAuthenticatedAuthenticationToken 没有被认证。必须传递一些 GrantedAuthorities。 此外,在 URL 参数中共享令牌并不好,它应该始终隐藏在 HTTPs 负载或标头中。代替地,加载 HTML 模板并将令牌值插入到 ${token} 占位符字段中。 这里是修订版:
注意:使用的 UserDetails 实现了 org.springframework.security.core.userdetails.UserDetails
@Component
public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private OAuth2TokenStore tokenStore;

    @Qualifier("tokenServices")
    @Autowired
    private AuthorizationServerTokenServices authTokenServices;

    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        IClient user = ((SocialUserDetails) authentication.getPrincipal()).getUser();
        // registration is not finished, forward the user, a marker interface 
        // IRegistration is used here, remove this if there no two step approach to 
        // create a user from a social network
        if (user instanceof IRegistration) {
            response.sendRedirect(subscriberRegistrationUrl + "/" + user.getId());
        }
        OAuth2AccessToken token = loginUser(user);
        // load a HTML template from the class path and replace the token placeholder within, the HTML should contain a redirect to the actual page, but must store the token in a safe place, e.g. for preventing CSRF in the `sessionStorage` JavaScript storage.
        String html = IOUtils.toString(getClass().getResourceAsStream("/html/socialLoginRedirect.html"));
        html = html.replace("${token}", token.getValue());
        response.getOutputStream().write(html.getBytes(StandardCharsets.UTF_8));
    }

    private OAuth2Authentication convertAuthentication(Authentication authentication) {
        OAuth2Request request = new OAuth2Request(null, authentication.getName(),
                authentication.getAuthorities(), true, null,
                null, null, null, null);
        // note here the passing of the authentication.getAuthorities()
        return new OAuth2Authentication(request,
                new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A",  authentication.getAuthorities())
        );
    }

    /**
     * Logs in a user.
     */
    public OAuth2AccessToken loginUser(IClient user) {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        UserDetails userDetails = new UserDetails(user);
        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A", userDetails.getAuthorities());
        securityContext.setAuthentication(authentication);
        OAuth2Authentication oAuth2Authentication = convertAuthentication(authentication);
        // delete the token because the client id in the DB is calculated as hash of the username and client id (here also also identical to username), this would be identical to the
        // to an existing user. This existing one can come from a user registration or a previous user with the same name.
        // If a new entity with a different ID is used the stored token hash would differ and the the wrong token would be retrieved 
        tokenStore.deleteTokensForUserId(user.getUsername());
        OAuth2AccessToken oAuth2AccessToken = authTokenServices.createAccessToken(oAuth2Authentication);
        // the DB id of the created user is returned as additional data, can be 
        // removed if not needed
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(new HashMap<>());
        oAuth2AccessToken.getAdditionalInformation().put("userId", user.getId());
        return oAuth2AccessToken;
    }

}

示例 socialLoginRedirect.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Example App</title>
    <meta http-equiv="Refresh" content="0; url=/index.html#/home"/>
</head>
<script>
     window.sessionStorage.setItem('access_token', '${token}');
</script>
<body>
<p>Please follow <a href="/index.html#/home">this link</a>.</p>
</body>
</html>

WebSecurityConfigurerAdapter 中的配置连线:
@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(WebServiceConfig.class)
public class AuthenticationConfig extends WebSecurityConfigurerAdapter {

    @Value("${registrationUrl}")
    private String registrationUrl;

    @Autowired
    private SocialAuthenticationSuccessHandler socialAuthenticationSuccessHandler;

    @Value("${loginUrl}")
    private String loginUrl;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> permitAllUrls = new ArrayList<>();
        // permit social log in
        permitAllUrls.add("/auth/**");
        http.authorizeRequests().antMatchers(permitAllUrls.toArray(new String[0])).permitAll();

        SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
        springSocialConfigurer.signupUrl(registrationUrl);
        springSocialConfigurer.postFailureUrl(loginUrl);
        springSocialConfigurer
                .addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
                    @SuppressWarnings("unchecked")
                    public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
                        filter.setAuthenticationSuccessHandler(socialAuthenticationSuccessHandler);
                        return filter;
                    }
                });
        http.apply(springSocialConfigurer);

        http.logout().disable().csrf().disable();

        http.requiresChannel().anyRequest().requiresSecure();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

0
我使用Spring OAuth2来保护我的REST服务,并为首次登录添加了社交登录和隐式注册。对于普通用户,您可以仅使用用户名和密码生成令牌,但是对于社交用户,生成令牌存在问题。因此,您需要实现一个过滤器,在处理/oauth/token请求之前拦截它。如果您想为社交用户生成令牌,请传递用户名和Facebook令牌,其中您可以将Facebook令牌用作密码,并为Facebook用户生成令牌。如果Facebook令牌已更新,则还必须编写一个数据库触发器以在用户表中更新您的令牌...这可能会对您有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接