9

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

 3 years ago
source link: https://blog.dteam.top/posts/2020-11/grails-oauth.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

该厂单点登录的流程

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

  • 请求登录页面:{ssoUrl}?returnUrl=http://localhost:8080/

  • 登录成功会给returnUrl带回一个token=xxYYzz的参数跳转回来

  • 使用这个token参数配合username:password(经过 base64 编码)认证请求用户信息接口获取用户信息:

    POST /{ssoUrl}/getUser?token=xxYYzz
    Authorization Basic {secret}
    

虽然不是标准的单点登录流程(如 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 流程了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK