本文介绍如何使用OIDC协议授权码模式,将自研应用接入IDaaS的单点登录(SSO)服务。帮助开发者轻松实现应用的SSO功能,提升用户体验和管理效率。
背景介绍
IDaaS 采用标准的 OIDC 协议授权码模式来支持常规企业自研应用接入。钉钉、微信等社交身份均采用 OAuth 协议实现扫码登录。IDaaS 采用的 OIDC 协议是 OAuth 协议的升级版。
兼容 OAuth:OIDC(OpenID Connect) 1.0 协议在 OAuth2.0 协议之上建立了用户身份层,OIDC 协议也因此兼容 OAuth2.0 协议。OIDC 授权码模式和 OAuth2.0 授权码模式流程一致,区别是 OIDC 对用户信息端点进行了标准化,并在 Token 端点会返回用户的 ID Token。
授权码流程介绍
自研应用 SSO 对接采用 OIDC 授权码模式。您的应用只需要完成与 IDaaS 之间的两个接口交互(授权端点、令牌端点),即可完成 SSO 的主体流程。登录将由 IDaaS 完全托管,您的应用只需解析登录结果即可。
对接 SSO
创建自研(或 OIDC 协议)应用
您需要在 IDaaS 中创建一个自研应用或标准协议(OIDC)应用,并获取应用密钥。若您已经获取到,可以跳过这个步骤。请参考自研应用完成应用创建。
在该应用的通用配置标签页中,即可获取到 client_id 和 client_secret。这对密钥将用于后续接口请求。
若希望对密钥进行管理或替换,请参考应用的 基本配置。
请求授权端点 Authorization Endpoint
下面步骤需要应用开发者处理。
在用户尝试访问您的应用时,应用需要判断当前是否有可用的已登录身份。
若用户需要登录,您需要向 IDaaS 发起授权登录的请求。您可以在
标签页,下方的应用配置信息中获取应用的授权端点。请参照如下示例,在授权端点地址的基础上,拼装出完整的授权请求访问地址,并在浏览器中发起 302 跳转。
{{授权端点 Authorization Endpoint}}? client_id=app_***& redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F***& response_type=code& scope=openid& state=525f49cc-***
字段
必填
示例
说明
client_id
是
app_michs7r****6pye
上一步骤获取到的 client_id。
scope
是
openid email profile
自研应用默认配置为 openid email profile,意味着应用可获取到已登录账户的 ID、用户名、邮箱信息。若您使用 OIDC 标准模板应用,管理员还可在配置页选择 phone。scope 对应的字段范围,可见文档末尾说明。
response_type
是
code
此值固定为 code,代表采用授权码模式。
redirect_uri
是
http://localhost:3000/user/oauth2/aliyunidaas/callback
用户登录完成后,IDaaS 向应用返回登录结果的重定向地址。该地址应该为应用接收 IDaaS 参数的中继地址,并能接收授权码
code
。state
否
525f49cc-87c4-4655-b79c-4c4f971b1ad1
state 是由应用生成的随机字符串,建议长度 32 位以上。该 state 值会在后续步骤返回给应用,应用届时应验证 state 值是否与发起传入的一致,以确保同一会话,以规避 XSRF 安全漏洞。非必填,但强烈建议填写。
用户自助登录
若请求顺利,向应用授权端点的请求,会跳转到 IDaaS 登录页。
用户可以通过任意已配置的登录方式,完成身份验证。IDaaS 提供了多样的、不同安全级别的登录能力,包括钉钉扫码登录、短信登录等。详情参考:登录方式。
登录成功后,浏览器会 302 跳转回应用指定的 redirect_uri,并在 URL 参数中携带 code 和 state 参数。
参考示例:
{{redirect_uri}}? code=CO***& state=525f49cc-***
字段
示例
说明
code
COE59pkCTm4A9nmowJUsfsfarGEaiShj3TuDc7NCzLCYu9
即授权码。在获取令牌的请求中使用。
state
525f49cc-87c4-4655-b79c-4c4f971b1ad1
应用接收到后,应确保与调用授权端点时传入的
state
一致。请求令牌端点 Token Endpoint
上一步接收到 code 授权码后,应用应使用获得的 code 向令牌端点(Token Endpoint)发起 POST 请求。
与上述授权端点一样,令牌端点也可以在
标签页下方的应用配置信息中获取到。请求示例如下:
POST /v2/<instance_id>/<app_id>/oauth2/token HTTP/1.0 Host: eiam-api-cn-hangzhou.aliyuncs.com Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=n0esc3N*****5acc3f0ogp4 &client_id=s6BhdR*****kqt3 &client_secret=7Fjfp0ZBr1*****KtDRbnfVdmIw &redirect_uri=http%3A%2F%2Fwww.example.com%2Fsso%2Fcallback
响应结果如下:
{ "token_type": "Bearer", "access_token": "ATM4SoVDqWgUq***********wk3ZS5mtn6fcSp8NH8", "expires_in": 1200, "expires_at": 1644843164, "id_token": "eyJraWQiOiJLRVkyV************gRIadj-frOIRFChA" }
至此,您的用户已经成功完成登录。您可以选择以下两种方式,进一步获取当前登录身份信息,并完成应用侧登录态的创建:
使用响应结果中的 id_token,经过验证后,直接拿到用户标识。
使用响应结果中的 access_token,调用 IDaaS 用户端点,获取当前已登录用户信息。
具体获取方式请参考下面章节。
重要可以获取到的用户数据范围,由第一步授权端点请求中的 scope 参数指定。
通过程序解析id_token
id_token
是一个包含身份信息的签名令牌信息(JWT 格式)。IDaaS 签发的id_token中,包含着解码即明文可见的用户数据,以及签名Signature。为了方便理解,您可以将
id_token
完整内容,粘贴到JWT 解码网站中,查看其包含的内容。内容示例如下:
{ "kid": "KEY2Ty1qL6u21NGKmccv3jwfd2ndmgtQPnag", "alg": "RS256" }.{ "sub": "user_uyvefotjn7kpbejfmxoos3rtmm", "jti": "jwt_aaaac7xyhclac6aqkgtjaxsthw5yotn5d77pmki", "iss": "https://pre-eiam-api-cn-hangzhou.aliyun-inc.com/v2/********/oidc", "iat": 1644841965, "nbf": 1644841965, "exp": 1644842265, "aud": "app_mhylgo3iairjqjdx5eop6uaf34", "at_hash": "XHEaGpMooM9zvQXaMzCNEA", "name": "testuser", "preferred_username": "testuser", }.[Signature]
在使用其中内容进行应用登录前,您需要对签名 [Signature] 进行验证,以确保令牌是 IDaaS 签发,而非任何其他三方,以保障登录的安全性。这一安全性是必须的。
获取验签公钥
在进行签名验证之前,需要首先获得 IDaaS 公开的验签公钥端点。
仍然在
标签页下方的应用配置信息中获取,我们可以看到应用的验签公钥端点。应用可通过访问这一端点,获取到当前的公钥信息。您也可以直接将此地址在浏览器中打开,即可展示公钥信息。
示例如下:
{ "keys": [ { "kty": "RSA", "e": "AQAB", "use": "sig", "kid": "KEYkYnc55G********CTvT7So44RGDYdbfs", "n": "pXmYkIpy1vaNjTMclU86BQjfmDhjlqMAX8ySVvh9gO-nae4ayvG_*********-v4gP27T7u6bUy0GXTlh3eKE0v1LYB81nfqjF2uazlPwPR5yYOhhWcK-gMNByLfE3CnkDc1YGwA3dZmIz-ZjOCKy8xLaBuqjrvwn5tpMpAoYEEaH4jIm7unTdhbKEKspNR-UXKD8q9RppMh5Tn2sB6oPHlQANudJDgqSwEOevIrdmHU0Zqxrb9cscGH9hH0QjmYEu72yI8BVeliPo3jK6JIoqCIcj5K_t8BJlFQ9QLJ8_o9tmd3BFv5_LVsh4BKGw" } ] }
接下来您即可以通过代码完成验签并获取
id_token
中的内容了。验签并登录
您可以通过JWT官方库列表找到对应语言的工具,并在代码中使用工具对
id_token
进行验签与解析。下面以 Java 库:org.bitbucket.b_c:jose4j 为例。
首先加入对应的 Maven 依赖:
<dependency> <groupId>org.bitbucket.b_c</groupId> <artifactId>jose4j</artifactId> <version>0.7.12</version> </dependency>
代码示例如下:
import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKeySet; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.consumer.JwtConsumer; import org.jose4j.jwt.consumer.JwtConsumerBuilder; public class IdTokenTest { public static void main(String[] args) throws Exception { // EIAM的Issuer标识(OIDC签发者URL) String issuer = "https://eiam-api-cn-hangzhou.aliyuncs.com/v2/idaas_padyrlux3mphrlsex4uonyqhxu/**********/oidc"; // 当前应用的唯一标识(从EIAM获取) String appId = "app_mkif4*****pxpzbasqmu"; // 请参照如下方式,设定解析用的应用公钥(JSON格式,用于验证JWT签名) String jwkJson = "{\n" + " \"keys\": [\n" + " {\n" + " \"kty\": \"RSA\",\n" + " \"e\": \"AQAB\",\n" + " \"use\": \"sig\",\n" + " \"kid\": \"KEY2H82C2at57itnW4onT3p1ySjwH4nirjCk\",\n" + " \"n\": \"w7Jl3fAUJp_9GuxV*****QsOA4lnXR5OD4kF4QbIeBiDiH8_MThrFi9k2MB6YMkSzf5JfIkpAS3JCqZ7k6Wooydp4pzaZNZAk3SGzdsa022RmAT" + "-Iayi4Yj6J9tSdTQCjwh2XkzzsIxA_Hla8rWiQ8Vhw1" + "-7QArgObfe67nSR7LxD55MFLxk9FU0*****RlGhrQGE_0LUuGWtCJG1r1e6aKquyswfxxAr3Rvj8QGIeJrG0R1Pv8m8d1_5OdULhB7149VqjM6D98WFjab0U2SNv0UlREZXTcS4p-2QNm_1egYRRpJEY_00FZqNSYsmErMGepYhO_61KoGqd8cphWQ\"\n" + " }\n" + " ]\n" + "}"; String jwt = "eyJraWQiOiJLRVkySDgyQzJhdD*****uaXJqQ2siLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2Vy*****lmNjRjZjR3amFrbnBieGpjd3V1IiwianRpIjoiand0X2FhYWFkYWllYTc2eWg1cW0zcm11bnoyeGg0eHd5aTJzZHBoNjR6aSIsImlzcyI6Imh0dHBzOi8vZWlhbS1hcGktY24taGFuZ3pob3UuYWxpeXVuY3MuY29tL3YyL2lkYWFzX3BhZHlybHV4M21waHJsc2V4NHVvbnlxaHh1L2FwcF9ta2lmNGR3bHBlaDZkbnM0cHhwemJhc3FtdS9vaWRjIiwiaWF0IjoxNjUzNjMwMDQxLCJuYmYiOjE2NTM2MzAwNDEsImV4cCI6MTY1MzYzMDM0MSwiYXVkIjoiYXBwX21raWY0ZHdscGVoNmRuczRweHB6YmFzcW11IiwibmFtZSI6InRlc3QiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0IiwidXBkYXRlZF9hdCI6MTY1MzYyODU5MH0.pAsUNB8OkdpIxJMZRfLJ7Pa31tsJyl44a1jVIlvdQxwOtPULAwrFxnB0X3eQx89hUGCdvYWl9FO9o-5kT7L-RER0wJYz9YNKqrVNBnaRwINRZyeYLRVurWMMzODQz-V0ULd9raM1M_i2f_SoWFs1gPFtYh_ijUARHISi7Q3q93ZfAuY8Lq2Nq07QunmDbosvioUd5wJG7WCxW5XXZYDUQe9p5IEYd1MSvnWuTOLbg7rKn0Vm4dNYGWjz1WuoAyCsc_QxOCqpmQ_2czoqPeN-SvPJAQ2CykLk7DSnGpABw1aNrjDidLS9Beqsga9VDCth86sk_0lyTZOaORtUrfVTtQ"; //解析JWK公钥集合 JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jwkJson); //创建JWT验证器 JwtConsumer jwtConsumer = createJwtConsumer(jsonWebKeySet, issuer, appId); //执行验签并解析claims JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt); // 已经验签完成,打印输出 id_token 中包含的用户信息 System.out.println(jwtClaims); } // 验签工具方法 public static JwtConsumer createJwtConsumer(JsonWebKeySet jsonWebKeySet, String issuer, String appId) { // 使用建造者模式配置JWT验证器 final JwtConsumerBuilder jwtConsumerBuilder = new JwtConsumerBuilder(); // 必须验证的字段配置 jwtConsumerBuilder.setExpectedIssuer(issuer); jwtConsumerBuilder.setRequireIssuedAt(); jwtConsumerBuilder.setRequireExpirationTime(); jwtConsumerBuilder.setAllowedClockSkewInSeconds(60); jwtConsumerBuilder.setExpectedAudience(appId); // 设置公钥解析器(通过kid匹配JWK) jwtConsumerBuilder.setVerificationKeyResolver((jws, nestingContext) - > { // 从JWT头部获取kid final String signKeyId = jws.getKeyIdHeaderValue(); // 遍历JWK集合查找匹配的密钥 for (JsonWebKey jsonWebKey: jsonWebKeySet.getJsonWebKeys()) { if (signKeyId.equals(jsonWebKey.getKeyId())) { return jsonWebKey.getKey(); } } throw new RuntimeException("Cannot find verification key: " + signKeyId); }); // 构建验证器实例 return jwtConsumerBuilder.build(); } }
重要请将代码中的issuer、appId、jwkJson和jwt部分替换为您从EIAM获取的实际信息。
输出示例如下:
JWT Claims Set:{sub=user_dt6kj6yf64cf4wjaknpbxjcwuu, jti=jwt_aaaadaiea76yh5qm3rmunz2xh4xwyi2sdph64zi, iss=https://eiam-api-cn-hangzhou.aliyuncs.com/v2/idaas_padyrlux3mphrlsex4uonyqhxu/app_**********/oidc, iat=1653630041, nbf=1653630041, exp=1653630341, aud=app_**********, name=test, preferred_username=test, updated_at=1653628590 }
由此获取到 IDaaS 中已登录身份信息,应用可用其顺利登录。
通过UserInfo端点获取用户信息
除了可以通过解析 id_token 获取用户信息外,还可以通过 UserInfo 端点(用户信息端点)获取用户信息。
仍然在
标签页下方的应用配置信息中获取,我们可以看到应用的验签公钥端点。UserInfo请求遵循标准 RFC6750,请求示例如下:
GET /v2/<instance_id>/<app_id>/oauth2/userinfo HTTP/1.0 Host: eiam-api-cn-hangzhou.aliyuncs.com Authorization: Bearer <AccessToken> 返回参数示例: { "sub": "user_dt6kj6yf64cf4wjaknpbxjcwuu", "name": "test", "preferred_username": "test", "updated_at": 1653899948 }
说明UserInfo 端点返回的业务字符与 id_token 中的字段是保持一致的,即在 “扩展 id_token” 中配置的字段也会在 UserInfo 端点中返回。
其他高级设置
若您对 OIDC 协议有深入了解,您可能会用到下列概念或能力,供参考。
OIDC Discovery 应用发现端点说明
OIDC 应用的
issuer
是对令牌发行方(即 IDaaS)的唯一标识,格式如下:https://<idaas-api-domain>/v2/<instance_id>/<application_id>/oidc
尖括号中的参数如下:
字段
说明
示例
idaas-api-domain
用户门户地址
https://******.aliyunidaas.com
instance_id
实例 ID
idaas_m********r2ed22e6m
application_id
应用 ID
app_m********jy6rbau
IDaaS 支持 OpenID Connect Discovery 1.0 标准,在
issuer
后再加上/.well-known/openid-configuration
就是该应用的 OIDC 发现端点地址。通过请求发现端点,您可以自动发现如下端点信息。所有的请求端点都可以直接从应用配置信息中获取。
端点
说明
authorization_endpoint
授权端点
device_authorization_endpoint
设备模式
需要标准OIDC应用支持该功能,自研应用暂时不支持设备码流登录token_endpoint
令牌端点
revocation_endpoint
令牌吊销端点
userinfo_endpoint
用户信息端点
jwks_uri
JWK公钥端点
Scope 与字段权限的对应关系
OIDC 中 id_token 中包含的用户信息与 scope 的对应关系如下:
字段
scope
说明
sub
openid
用户的
userId
jti
openid
JWT 令牌ID,辅助字段
iss
openid
JWT 签发的
issuer
,辅助字段iat
openid
JWT 签发时间,辅助字段
nbf
openid
JWT 令牌有效开始时间,辅助字段
exp
openid
JWT 令牌过期时间,辅助字段
aud
openid
即应用的 ClientID,辅助字段
at_hash
openid
AccessToken 哈希值,辅助字段
phone_number
phone
电话号码,比如
+86 130****5678
phone_number_verified
phone
电话号码是否被验证过,目前默认电话号码是已验证
email
email
电子邮箱,比如
al***@example.com
email_verified
email
电子邮箱是否被验证过,目前默认电子邮箱是已验证
name
profile
用户显示名
preferred_username
profile
用户的
username
updated_at
profile
用户资料最后更新时间
令牌端点支持的认证方式
根据 OIDC 协议指明,IDaaS 提供灵活性,允许以下 4 种不同方式进行身份验证。
在发现端点中返回的字段
token_endpoint_auth_methods_supported
指定了支持的认证方法。取值
说明
none
用于 Public 客户端,通过
none
认证方式认证时grant_type
不能是client_credentials
client_secret_basic
按规范 RFC 6749 - The OAuth 2.0 Authorization Framework 实现
client_secret_post
按规范 RFC 6749 - The OAuth 2.0 Authorization Framework 实现
client_secret_jwt
按规范 OpenID Connect Core 1.0 实现
上一步接收到 code 授权码,并验证请求合法(验证 state 与发起请求传入的一致)后,应用的后端服务,应使用获得的 code 向令牌端点(Token Endpoint)发起 POST 请求,请求示例如下:
以
client_secret_basic
为例,令牌端点请求样例为:POST /token HTTP/1.0 Host: api.aliyunidaas.com Authorization: Basic YXBwX21pY2hzN3I0*******cHllOkNTKioqKioq grant_type=authorization_code& code=COE59pkCTm4J*******arGEaiShj7NCzLCYu9
更多说明参看 OIDC Core 1.0 规范。
应用 Client Secret 轮转
请参考 基本配置 中密钥轮转章节说明。