最近遇到一个项目,需要跟某大厂的单点登录接口对接。然而该厂使用的并非行业通用的 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 登录。
Grails 的grails-shiro-oauth插件使用上还是比较简单的,插件已经实现了一个OauthController
,并且实现了callback
和authenticate
两个 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 流程了。
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章