整合Spring Security OAuth2和Spring Social

问题描述 投票:32回答:4

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

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

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

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

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

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

谢谢!

spring-security spring-boot single-sign-on spring-social spring-security-oauth2
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;
    }
}

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

我希望它有所帮助。


5
投票

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

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

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

总结答案:

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

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


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);
    }

0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.