spring-security-oauth2适配总结

spring-security-oauth2适配总结

前言

最近项目需要使用oauth2做第三方登录验证,原本以为在spring boot项目中,使用oauth2是个很简单的事情,毕竟spring security也支持oauth2 客户端模式嘛,谁知毕竟too naive,最后兜兜转转花了不少时间,才算搞定。
搜spring boot关于oauth2的使用,首先都是讲@EnableOAuth2Sso,一个注解加上配置文件就能搞定,我也建议都先用这种方式尝试一下,能搞定当然是最好的,搞不定的可以浏览下本篇文章,说不定能有帮到你的地方。
本文主要总结一下使用spring-security-oauth2适配三方登录验证的过程,authorization code模式。代码示例基于spring boot使用oauth2教程源码。

1、redirect_uri的设置

在服务端强校验redirect_uri的场景下,该值可能需要被设置以通过校验。而redirect_uri在spring-security-oauth2中并不是可配置的信息,只有通过源码方面的适配才能做到。
获取redirect_uri的功能是在 AbstractRedirectResourceDetails类中定义,AuthorizationCodeResourceDetails继承该类,验证码模式中使用该类来获取回调地址。

public String getRedirectUri(AccessTokenRequest request) {

    String redirectUri = request.getFirst("redirect_uri");

    if (redirectUri == null && request.getCurrentUri() != null && useCurrentUri) {
        redirectUri = request.getCurrentUri();
    }

    if (redirectUri == null && getPreEstablishedRedirectUri() != null) {
        // Override the redirect_uri if it is pre-registered
        redirectUri = getPreEstablishedRedirectUri();
    }如果

    return redirectUri;

}

从该函数的定义可以看出,回调地址会首先从AccessTokenRequest中获取,如果request中没有设置,则以当前访问地址为准,再者以上一次设置的redirect_uri为准。结合该段代码逻辑以及上文提到的filter,有三种方式可以设置redirect_uri:

1) 重新实现AuthorizationCodeResourceDetails,重写getRedirectUri函数,使之返回需要的redirect_uri。

2)重新实现AccessTokenRequest,使之能够设置redirect_uri。

3)设置OAuth2ClientAuthenticationProcessingFilter的捕获接口,使之与redirect_uri完全匹配, 因为该接口的定义间体现在上文代码中的request.getCurrentUri()。

这三种修改将在最后的代码实例中体现。

2 code的获取

验证码模式中,三方验证通过后,会通过回调地址传回验证码,一般包含code和state两个参数,通常,不太会出问题,但是奇葩就奇葩在参数命名上,不是每个服务器都使用code来命名参数,同时该参数也未提供配置支持,so,继续代码适配。
获取验证码是在DefaultAccessTokenRequest中定义:

/**
 * The authorization code for this context.
 * 
 * @return The authorization code, or null if none.
 */

public String getAuthorizationCode() {
    return getFirst("code");
}

(不知道你们看到这段代码是怎么想的,反正我当时是有点吃惊的,我以为最起码会定义个常量之类的),DefaultAccessTokenRequest通过http request获取到请求参数,然后其他类会调用getAuthorizationCode函数获取验证码,如果服务器返回的参数不是code而是auth_code或者其它东东,那这个流程肯定是不通的。
没有找到其它方法,只有重新实现AccessTokenRequest,使之能获取到正确的验证码。

3 access token的获取

同code的获取一样,这一步也可能会存在参数名不是默认名称的情况,但是我遇到的更奇葩,解析返回内容出错。。。
解析类的设置是在OAuth2AccessTokenSupport中定义,具体的解析功能是在FormOAuth2AccessTokenMessageConverter中定义:

public FormOAuth2AccessTokenMessageConverter() {
    super(new MediaType[]{MediaType.APPLICATION_FORM_URLENCODED,  MediaType.TEXT_PLAIN});
}

构造函数中指定了支持的MediaType,如果MediaType不同,是走不到解析的,逻辑判断是在AbstractHttpMessageConverter的canRead函数中定义:

MediaType supportedMediaType;
do {
    if (!var2.hasNext()) {
        return false;
    }

    supportedMediaType = (MediaType)var2.next();
} while(!supportedMediaType.includes(mediaType));

同样,倒霉的我碰到的MediaType偏偏不是上面定义的这两种,只有适配喽。

顺便说一下,access token的设置和获取是在DefaultOAuth2AccessToken中:

public static OAuth2AccessToken valueOf(Map     tokenParams) {
    DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken((String)tokenParams.get("access_token"));
    ...
}

所以,如果遇到access token的返回不是默认参数名称access_token的时候,在重写converter的时候,OAuth2AccessToken也需要重写。

4 用户信息的获取

默认的用户信息的获取功能是在UserInfoTokenServices中定义,同上面几种情况一样,如果在请求用户信息的时候需要额外的参数设置,或者返回内容无法使用默认函数解析的时候,只有重写实现ResourceServerTokenServices接口了。

5 代码实例

1) code的获取

重新实现AccessTokenRequest:

public class MyAccessTokenRequest extends   DefaultAccessTokenRequest {
        ...

        @Override
        public String getAuthorizationCode() {
            return getFirst("authorization_code");
        }

        @Override
        public void setAuthorizationCode(String code) {
            set("authorization_code", code);
        }
}

可以根据需要重写state的获取与设置函数。

2)redirect_uri的设置

redirect_uri的设置使用上文提到的重新实现AccessTokenRequest的方式,在AccessTokenRequest实例化的时候将值设置进AccessTokenRequest中:

//在这里将redirect_uri替换为你自己的回调地址
request.set("redirect_uri", "http://127.0.0.1:8080/login/oauth2");

该函数通常放在EnableOAuth2Client注解的类中。

3)access token的获取

我碰到的情况是服务器返回json,所以根据需要重写了converter:

public class MyOauth2AccessTokenMessageConverter extends AbstractHttpMessageConverter {

    private final StringHttpMessageConverter delegateMessageConverter = new StringHttpMessageConverter();

    public MyOauth2AccessTokenMessageConverter() {
        super(new MediaType[]{MediaType.APPLICATION_FORM_URLENCODED,
                MediaType.TEXT_PLAIN,
                MediaType.TEXT_HTML,
                MediaType.TEXT_XML});
    }

    protected boolean supports(Class clazz) {
        return OAuth2AccessToken.class.equals(clazz);
    }

    protected OAuth2AccessToken readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        String raw = this.delegateMessageConverter.read((Class)null, inputMessage);
        JSONObject o = JSON.parseObject(raw);
        Map m = new HashMap();
        for (Map.Entry  entry : o.entrySet()) {
            m.put(entry.getKey(), String.valueOf(entry.getValue()));
        }
        //如果重写OAuth2AccessToken,在这里进行实例化
        //吐槽一下:不太明白valueof为啥用,其实函数内部都做了强转,为毛不用?
        return DefaultOAuth2AccessToken.valueOf(m);
    }

    protected void writeInternal(OAuth2AccessToken accessToken, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        //根据需要实现
        //不需要的直接抛异常即可
    }

}

可以根据需要重写该converter。

4)用户信息的获取.

ResourceServerTokenServices比较简单,只有两个接口,不需要的接口直接抛异常就行。
public class MyUserInfoTokenService implements ResourceServerTokenServices {

    private String userInfoEndpointUrl;
    private String clientId;

    public MyUserInfoTokenService(String userInfoUrl, String appId) {
        this.userInfoEndpointUrl = userInfoUrl;
        this.clientId = appId;
    }

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken)
            throws AuthenticationException, InvalidTokenException {
        ... 
        //根据access token获取用户信息
    }
}

5) state的生成

根据项目需要,state可能需要按照自己的规则生成,很简单,重新实现StateKeyGenerator即可:

public class MyStateKeyGenerateor implements StateKeyGenerator {
    @Override
    public String generateKey(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
        //实现自己的state生成函数
        return "123456";
    }
}

6) 适配代码的组装

上面各种适配,总要在一个地方组装起来(示例代码只涉及改动的地方,缺失部分参考附录[3]的源码):

@SpringBootApplication
@EnableOAuth2Client
public class SocialApplication extends WebSecurityConfigurerAdapter {

    //自定义AccessTokenRequest的设置,以及redirect_uri的设置
    @Bean(name="accessTokenRequest")
    @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
    protected AccessTokenRequest accessTokenRequest(@Value("#{request.parameterMap}")
                                                            Map parameters, @Value("#{request.getAttribute('currentUri')}")
                                                            String currentUri) {
        MyAccessTokenRequest request = new MyAccessTokenRequest(parameters);
        request.setCurrentUri(currentUri);
        //该处可以设置你自己的回调地址
        request.set("redirect_uri", "http://127.0.0.1:8080/login/oauth2");
        return request;
    }

    private Filter ssoFilter() {
        //可以将/login/oauth2设置为你自己的回调接口,对应的是上文中提到的filter设置回调。
        //注意这个地方,使用filter方式,前端设置的访问接口也需要一致
        //(强校验场景下,localhost和127.0.0.1也是有区别的)
        OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(
                "/login/oauth2");
        OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
        AuthorizationCodeAccessTokenProvider tokenProvider = new AuthorizationCodeAccessTokenProvider();
        //设置state生成器
        tokenProvider.setStateKeyGenerator(new MyStateKeyGenerateor());
        List> messageConverters = new ArrayList<>();
        messageConverters.add(new MyOauth2AccessTokenMessageConverter());
        //设置access token解析器
        tokenProvider.setMessageConverters(messageConverters);

        facebookTemplate.setAccessTokenProvider(tokenProvider);
        facebookTemplate.setRetryBadAccessTokens(false);

        facebookFilter.setRestTemplate(facebookTemplate);
        //设置用户信息的获取方式
        facebookFilter.setTokenServices(new MyUserInfoTokenService(facebookResource().getUserInfoUri(), facebook().getClientId()));

        return facebookFilter;
    }

    @Bean
    @ConfigurationProperties("facebook.client")
    //如果使用上文提到的重新实现AuthorizationCodeResourceDetails方式实现获取redirect_uri,可以在这里进行实例化
    public AuthorizationCodeResourceDetails facebook() {
        return new AuthorizationCodeResourceDetails();
    }

    @Bean
    @ConfigurationProperties("facebook.resource")
    public ResourceServerProperties facebookResource() {
        return new ResourceServerProperties();
    }

}

总结

debug了几天,基本搞清楚了spring-security-oauth2的运转,有点不太明白有些参数名为啥被硬编码在代码中,这样做确实在协议兼容性上不太友好;另外,有时间需要看下原版oauth2协议,目前所知太片面。

参考:
[1] spring-security-oauth2源码
[2] spring boot使用oauth2教程
[3] spring boot使用oauth2教程源码

你可能感兴趣的:(spring,spring,boot,oauth2,redirect_uri,code)