整合Spring Security OAuth2和Spring Social

时间:2015-08-31 14:36:47

标签: spring-security spring-boot single-sign-on spring-social spring-security-oauth2

我正在使用Spring Boot + Spring Security OAuth2应用程序,我认为该应用程序的灵感来自Dave Syer的示例。应用程序配置为OAuth2授权服务器,单个公共客户端使用资源所有者密码凭据流。成功的令牌配置为JWT。

公共Angular客户端向/ oauth / token发送一个POST请求,其中包含一个包含客户端ID和密码的基本auth头(这是让客户端进行身份验证的最简单方法,即使秘密不是私有的)。请求正文包含用户名,密码和授予类型“密码”。

除了作为身份验证服务器之外,该应用程序还是用户,团队和组织的RESTful资源服务器。

我正在尝试使用Spring Social添加其他SSO身份验证流程。我已经将Spring Social配置为通过/ auth / [provider]通过外部提供程序进行身份验证;但是,以下请求不再正确设置SecurityContext。可能是,Spring Security OAuth服务器或客户端正在覆盖SecurityContext?

如果我可以在Spring Social流程之后正确设置SecurityContext,我有一个新的TokenGranter,允许新的授权类型“social”,它将检查SecurityContextHolder以获得预先认证的用户。

我对SecurityContext的特定问题的解决方案感兴趣(我认为这是Spring OAuth +社交集成的问题),或者是与外部提供程序进行身份验证并从我们自己的身份验证中获取有效JWT的不同方法服务器

谢谢!

4 个答案:

答案 0 :(得分:12)

我在JHipster生成的网络应用程序上遇到了类似的问题。最后,我决定使用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;
    }
}

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

我希望它有所帮助。

答案 1 :(得分:5)

首先,我强烈建议您远离密码授予这样的用例。
公共客户端(JavaScript,已安装的应用程序)无法保密其客户机密,这就是为什么它们不能被分配一个:任何检查您的JavaScript代码的访问者都可以发现这个秘密,从而实现您拥有的相同身份验证页面,存储您的用户密码在此过程中。

隐式授权已完全针对您正在进行的操作而创建 使用基于重定向的流程具有将身份验证机制留给授权服务器的优势,而不是让每个应用程序都拥有它的一部分:主要是单点登录(SSO)的定义)

话虽如此,你的问题与我刚回答的问题紧密相关:Own Spring OAuth2 server together with 3rdparty OAuth providers

总结答案:

  

最后,它是关于授权服务器如何保护AuthorizationEndpoint的:/ oauth / authorize。由于您的授权服务器可以工作,因此您已经有一个扩展WebSecurityConfigurerAdapter的配置类,它使用formLogin处理/ oauth / authorize的安全性。那就是你需要整合社交内容的地方。

您根本无法使用密码授予您尝试实现的目标,您必须将公共客户端重定向到授权服务器。然后,授权服务器将重定向到社交登录,作为/oauth/authorize端点的安全机制。

答案 2 :(得分:1)

我从上面的好答案(https://stackoverflow.com/a/33963286/3351474)开始,但是在我的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);
    }

答案 3 :(得分:0)

我实现了spring oauth2来保护我的休息服务,并且还为首次登录添加了社交登录和隐式注册。对于用户用户,您只能使用用户名和密码生成令牌,并为社交用户生成令牌。为此,您必须实现在处理之前拦截/ oauth / token请求的Filter。这里如果你想为社交用户生成令牌传递用户名和facebook令牌,这里你可以使用facebook令牌作为密码并为facebook用户生成令牌。如果Facebook令牌更新,那么你必须编写一个数据库触发器来更新用户表中的令牌....可能它会帮助你