DTeam 技术日志

Doer、Delivery、Dream

Grails Oauth2 插件适配非标准 SSO 接口

冯宇 Posted at — Nov 26, 2020 阅读

最近遇到一个项目,需要跟某大厂的单点登录接口对接。然而该厂使用的并非行业通用的 OAuth2 接口,经过一番魔改之后,成功搞定了单点登录的对接。

该厂单点登录的流程

该厂单点登录的流程大致如下:

虽然不是标准的单点登录流程(如 Oauth2 协议跳转回来的参数是code而不是token,需要用code换取access token,然后用access token登录 SSO 系统获取用户信息),但大体流程上还是大同小异的。因此可以通过稍加改造 Grails Oauth2 插件来对接到我们已经支持的 OAuth2 登录。

实现非标准 Oauth2 适配

Grails 的grails-shiro-oauth插件使用上还是比较简单的,插件已经实现了一个OauthController,并且实现了callbackauthenticate两个 action,默认注册了/oauth/callback/$provider这个 URL 帮助自动处理 OAuth2 的回调,我们只需要实现目标 SSO 服务端的 API 以及成功的响应onSuccess action 就可以了。

大致的主要部分逻辑如下:

Api 实现部分:

class MyApi extends DefaultApi20 {

    private static final ConfigObject configObject = Holders.grailsApplication.config
    private static final String ssoUrl = configObject.oauth.providers.myApi.ssoUrl
    private static final String resourceUrl = configObject.oauth.providers.myApi.resourceUrl

    @Override
    Verb getAccessTokenVerb() {
        Verb.POST
    }

    @Override
    AccessTokenExtractor getAccessTokenExtractor() {
        new JsonTokenExtractor()
    }

    @Override
    String getAccessTokenEndpoint() {
        resourceUrl
    }

    @Override
    String getAuthorizationUrl(OAuthConfig config) {
        Preconditions.checkValidUrl(config.callback, "Must provide a valid url as callback. RootCloud does not support OOB")
        String.format(ssoUrl, OAuthEncoder.encode(config.callback))
    }

    @Override
    OAuthService createService(OAuthConfig config) {
        new MyApiOAuthService(this, config);
    }

}

OAuthService 实现部分:

class MyApiOAuthService extends OAuth20ServiceImpl {

    private final DefaultApi20 api
    private final OAuthConfig config

    MyApiOAuthService(DefaultApi20 api, OAuthConfig config) {
        super(api, config)
        this.api = api
        this.config = config
    }

    @Override
    Token getAccessToken(Token requestToken, Verifier verifier) {
        String url = api.accessTokenEndpoint + "?token=${requestToken.token}"
        OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), url)
        request.addHeader('Authorization', "Basic ${config.apiSecret}")
        Response response = request.send()
        new Token(response.body, config.apiSecret)
    }

}

这里有一些 hack 的部分,getAccessToken按照 OAuth2 的流程应该是使用code换取access token的过程,但是被我们调整成了直接用token请求用户信息,将响应封装进入了Token类,交由onSuccess action 进行解析。

由于 grails shiro oauth 插件只认code参数,不认非标准的token参数,因此我们需要在 filter 中做一下参数转换,以便对接插件。

class OauthCallbackFilters {

    def filters = {
        fillInCodeParam(controller: 'oauth', action: 'callback') {
            before = {
                if (params.token && !params.code) {
                    params.put('code', params.token)
                }
            }
        }
    }

}

最后在 onSuccess action 的逻辑中变通一下,直接按照 JSON 格式解析Token就完事了:

class ShiroOAuthController {

    GrailsApplication grailsApplication
    OauthService oauthService

    def onSuccess() {
        if (!params.provider) {
            renderError 400, "The Shiro OAuth callback URL must include the 'provider' URL parameter."
            return
        }
        Map providerConfig = grailsApplication.config.oauth.providers[params.provider]
        String sessionKey = oauthService.findSessionKeyForAccessToken(params.provider)
        Token accessToken = session[sessionKey]
        if (!accessToken) {
            renderError 500, "No OAuth token in the session for provider '${params.provider}'!"
            return
        }

        // Create the relevant authentication token and attempt to log in.
        def oauthUserInfo

        if (params.provider == 'myapi') {
            oauthUserInfo = new JSONObject(accessToken.token)
        } else {
            Response response = oauthService."get${params.provider}Resource"(accessToken, providerConfig.resourceUrl)
            log.debug("OAuth resource response for ${params.provider}: ${response.body}")
            if (response.body =~ /\{.*}/) {
                oauthUserInfo = JSON.parse(response.body)
            } else if (response.body =~ /<.*>/) {
                oauthUserInfo = XML.parse(response.body)
            } else {
                oauthUserInfo = request.getParameterMap()
            }
        }
        //...
    }

}

这样通过小幅度变通就可以适配非标准的 OAuth2 流程了。


友情链接


相关文章