1. 背景
4A是指:认证Authentication、授权Authorization、账号Account、审计Audit。云巧4A统一安全管控组件实现对自然人、资源、资源账号的集中管理,提升系统安全性和可管理能力,实现可视、可控、可信的安全管理体系。统一安全管控组件(以下简称4A组件)系统超级管理员角色包括用户管理、数据字典管理、系统公告管理、应用管理等,各个应用对接4A组件之后用户、角色、资源权限都会交给该平台进行管理,其他应用将无权直接对用户、角色、资源进行管理操作。
认证(Authentication)和授权(Authorization)是两个容易被弄混的概念,尤其是只看英文。
- 认证即确认该用户的身份是他所声明的那个人;
- 授权即根据用户身份授予他访问特定资源的权限。
2. 常见第三方登录认证协议
第三方登录常见的协议主要有 CAS、OAuth、OIDC(OpenID Connect)、SAML协议,在社交应用、互联网企业应用通常使用OAuth或OIDC协议比较多,在企业级应用中一使用SAML协议比较多。
2.1 OAuth2.0
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。接下来介绍一些概念。
客户端:本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器端)、微信客户端等。
资源拥有者:通常为用户,也可以是应用程序,即该资源的拥有者。
授权服务器(也称认证服务器):用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token),作为客户端访问资源服务器的凭据。l例如:微信的认证服务器、支付宝的认证服务器
资源服务器:存储资源的服务器,例如:微信、支付宝等包含用户信息服务器,可以通过认证服务器认证之后,通过access_token获取微信、支付宝保存的用户信息
OAuth 2.0的运行流程如下图
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
不难看出来,上面六个步骤之中,B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。
下面一一讲解客户端获取授权的四种模式。
2.1.1 授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。这种模式是四种模式中最安全的一种模式。一般用于Web服务器端应用或第三方的原生App调用资源服务的时候。 因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大限度的减小了令牌泄漏的风险。
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
2.1.2 简化模式
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。一般来说,简化模式用于第三方单页面应用,这种模式只需要前端就可以完成认证流程,无需和后端的交互,所以一般应用于vue、react等前后端分离的单页面前端应用,可以直接获取到access_token。
(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
2.1.3 密码模式
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是 我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用 或者是可信任的第三方系统。
(A)资源拥有者将用户名、密码发送给客户端(需提前配置好,发送给客户端或者第三方系统)。
(B)客户端将用户名和密码发给授权服务器,向后者请求令牌。
(C)授权服务器确认无误后,向客户端提供访问令牌。
2.1.4 客户端模式
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。客户端模式和密码模式比较类似,通过一个流程就可以获取到access_token,相对简单的两种认证方式,一般可以直接应用于客户端或者第三方系统。
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
2.2 CAS
Central Authentication Service简称CAS,用户仅需登陆一次,访问其他应用则无需再次登陆。
顾名思义,CAS是一种仅用于Authentication的服务,它和OAuth/OIDC协议不一样,并不能作为一种Authorization的协议。
当前CAS协议包括CAS 1.0、CAS2.0、CAS3.0版本,这三个版本的认证流程基本类似。
CAS的认证流程通过包括几部分参与者:
Client: 通常为使用浏览器的用户
CAS Client: 实现CAS协议的Web应用
CAS Server: 作为统一认证的CAS服务器
认证流程大致为:
(A)Client(终端用户)在浏览器里请求访问Web应用example;
(B)浏览器发起一个GET请求访问example应用的主页https://www.example.com;
(C)应用example发现当前用户处于未登陆状态,Redirect用户至CAS服务器进行认证;
(D)用户请求CAS服务器;
(E)CAS发现当前用户在CAS服务器中处于未登陆状态, 要求用户必须得先登陆;
(F)CAS服务器返回登陆页面至浏览器;
(G)用户在登陆界面中输入用户名和密码(或者其他认证方式);
(H)用户把用户名和密码通过POST,提交至CAS服务器;
(I)CAS对用户身份进行认证,若用户名和密码正确,则生成SSO会话, 且把会话ID通过Cookie的方式返回至用户的浏览器端(此时,用户在CAS服务端处于登陆状态);
(J)CAS服务器同时也会把用户重定向至CAS Client, 且同时发送一个Service Ticket;
(K)CAS Client的服务端收到这个Service Ticket以后,请求CAS Server对该ticket进行校验;
(L)CAS Server把校验结果返回给CAS Client, 校验结果包括该ticket是否合法,以及该ticket中包含对用户信息;
至此,CAS Client根据Service Ticket得知当前登陆用户的身份,CAS Client处于登陆态。
经过上述流程以后,CAS Server和CAS Client都处于登陆态,当用户如果访问另外一个CAS Client 2的时候,用户不需要再次认证,即会跳过5、6、7、8、9这几步,从而达到SSO的效果。
2.3 OpenID Connect
OIDC是OpenID Connect的简称,OIDC=(Identity, Authentication) + OAuth 2.0。它在OAuth2上构建了一个身份层,是一个基于OAuth2协议的身份认证标准协议。OAuth2是一个授权协议,它无法提供完善的身份认证功能,OIDC使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且可以适用于各种类型的客户端(比如服务端应用,移动APP,JS应用),且完全兼容OAuth2,也就是说你搭建了一个OIDC的服务后,也可以当作一个OAuth2的服务来用。
OIDC的核心在于在OAuth2的授权流程中,一并提供用户的身份认证信息(ID Token)给到第三方客户端,ID Token使用JWT格式来包装,得益于JWT(JSON Web Token)的自包含性,紧凑性以及防篡改机制,使得ID Token可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了UserInfo的接口,用于获取用户的更完整的信息。主要角色:
EU(End User):用户。
RP(Relying Party ): 用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方;
OP(OpenID Provider):有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息;
ID Token:JWT格式的数据,包含EU身份认证的信息。
UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用Access Token访问时,返回授权用户的信息,此接口必须使用HTTPS。
(A)RP发送一个认证请求给OP
(B)OP对EU进行身份认证,然后提供授权
(C)OP把ID Token和Access Token(需要的话)返回给RP
(D)RP使用Access Token发送一个请求UserInfo EndPoint
(E)UserInfo EndPoint返回EU的Claims
2.4 SAML 2.0
安全断言标记语言SAML协议全称为Security Assertion Markup Language,它是一个基于XML的标准协议。SAML标准定义了身份提供者(Identity Provider)和服务提供者(Service Provider)之间,如何通过SAML规范,采用加密和签名的方式来建立互信,从而交换用户身份信息。
安全断言标记语言:我们可以把他拆为俩个点来看,安全和断言。
安全:为了防止断言被假冒,篡改。saml中加入了安全错失,当然现今能抵御假冒,篡改,重放攻击的利器就是公钥-私钥系统了。 通过给断言加上签名和加密,再结合数字证书系统就确保了saml不受攻击。
断言:简单来说就是做出判断的语言。比如说,A在网上尝试登录某个服务,但这个服务知道你不是合法的用户,这时候,如果你能提供一个安全可靠的断言:“A有权限登录xx服务”,那么这个服务知道你合法了,就会提供服务给你,当然这个例子有些抽象,SAML大部分主要内容,都是用于证明你是谁,你有什么权限
SAML断言是 IDP 发送给服务提供商的 XML 文档。存在三种不同类型的 SAML 断言 — 身份验证、属性和授权决策。
SAML的优点在于:1.可以提升用户体验,如果系统使用SAML,那么可以在登录一次的情况下,访问多个不同的系统服务。2.可以提升系统的安全性,使用SAML,我们只需要向IdP提供用户名密码即可。3.用户的认证信息不需要保存在所有的资源服务器上面,只需要在在IdP中存储一份就够了。
SAML通信中主要有三个主体
End User:代表用户本身或最终用户
IDP(identity provider):身份提供者,提供在线资源以通过网络向最终用户提供身份认证
SP(service provider):服务提供商,必须相信IDP,由IDP认证受到服务提供商的信任,授权其访问指定资源信息
SAML由两种发起流程,分别是
1.SP Initiated: 服务提供者主动发起
2.IDP Initiated: 身份认证服务器主动发起
(A)End User从浏览器中请求访问某SP(服务提供商) ;
(B)SP发现用户未登陆,则发起SAML的AuthnRequest请求至IDP(身份提供者), 用户浏览器跳转至IDP页面;
(C)IDP发现用户处于未登陆状态,重定向用户至IDP的登陆界面,请求用户进行身份验证
(D)用户在登陆页面中进行身份认证, 通常情况下需要校验用户名和密码;
(E)IDP校验用户身份,若成功,则把包含着用户身份信息的校验结果,以SAML Reponse的形式,签名/加密发送给SP;
(F)SP拿到用户身份信息以后,进行签名验证/解密,拿到明文的用户身份信息,此时SP处于登陆状态,可以对用户提供服务。
可以看到,在整个流程中,IDP是负责颁发用户身份,SP负责信任IDP颁发的用户身份, SP和IDP之间的信任关系是需要提前建立的,即SP和IDP需要提前把双方的信息预先配置到对方,通过证书信任的方式来建立互信。
2.5 各协议简单对比
上面简单介绍了主流的几种协议,本质上它们大同小异,都是基于中心信任的机制,服务提供者和身份提供者之间通过互信来交换用户信息,只是每个协议信息交换的细节不同,或者概念上有些不同。
最后,通过一个简单对比表格来总结本文重点内容:
3. 云巧4A组件统一认证模块
4A SDK是一个拥抱云巧架构,使业务系统可以快速基于云巧完成用户登录,鉴权等能力的 SDK。4A组件的登录的主干流程由4A SDK提供,并提供了扩展点给开发者实现,以对接第三方的服务。因为 4A SDK 使用者是一个后端应用,我们将集成 4A SDK的应用称为 sso server, 我们通过时序图看一下如何和第三方做集成,并展示提供了哪些扩展点。
在了解了上述的时序图后,我们发现 4A SDK 在包括了主要的登录流程后,留下了扩展点用来和 client 对接和一个对接第三方服务的接口即 SsoTokenExtension 、 IClientForSession, 实现这两个接口即可成功对接第三方的登录。
4. 云巧4A组件第三方登录实现
接下来将介绍4A组件已经实现的钉钉第三方企业应用免登、钉钉登录第三方网站、专有钉钉登录第三方网站以及未来可能拓展的企业微信登录第三方网站,以下第三方登录方式均采用OAuth2.0协议。
4.1 钉钉第三方企业应用免登
“免登”是指用户进入应用后,无需输入钉钉用户名和密码,应用程序可自动获取当前用户身份,进而使用此用户身份登录系统的流程。在钉钉上开发的第三方企业应用,作为公开的云端SaaS服务可以让企业客户安装使用。管理员开通第三方企业应用后,企业员工在钉钉内使用该第三方企业应用时,无需输入账号密码便可直接登录该应用。
4.1.1 获取免登授权码
PC端暂不支持小程序开发,如果要开发PC端应用,需使用微应用开发方式。
4.1.1.1 小程序免登授权码
调用dd.getAuthCode接口获取小程序免登授权码。企业应用和个人应用的免登授权码均可通过该JSAPI获取。
dd.getAuthCode({ success:function(res){ /*{ authCode: 'hYLK98jkf0m' //string authCode }*/ }, fail:function(err){ } });
返回说明:
参数 |
说明 |
authCode |
授权码,有效期5分钟,且只能使用一次,使用后会失效。 |
4.1.1.2 微应用免登流程
使用以下代码获取免登授权码(调用此api不需要进行鉴权,即不需要进行dd.config)。获取的免登授权码有效期5分钟,且只能使用一次。
客户端 |
Android |
iOS |
PC |
钉钉版本 |
支持 |
支持 |
支持 |
dd.ready(function() { dd.runtime.permission.requestAuthCode({ corpId: "ding12345xxx", // 企业idonSuccess: function (info) { code=info.code// 通过该免登授权码可以获取用户身份 }}); });
参数说明
参数 |
类型 |
是否必填 |
说明 |
corpId |
String |
是 |
企业的corpid,由前端从URL中获取。 |
返回结果
参数 |
说明 |
code |
授权码,有效期5分钟,且只能使用一次,使用后会失效。 |
4.1.2 获取access_token
企业内部应用调用本接口获取access_token。调用服务端API获取应用资源时,需要通过access_token来鉴权调用者身份进行授权。
参数说明
名称 |
类型 |
是否必填 |
示例值 |
描述 |
appkey |
String |
是 |
dingeqqpkv3xxxx |
应用的唯一标识key。 |
appsecret |
String |
是 |
GT-lsu-taDAsTsxxxx |
应用的密钥。AppKey和AppSecret可在钉钉开发者后台的应用详情页面获取。 |
返回参数
名称 |
类型 |
示例值 |
描述 |
access_token |
String |
fw8ef8we8f76e6f7s8dxxxx |
生成的access_token。 |
expires_in |
Number |
7200 |
access_token的过期时间,单位秒。 |
errmsg |
String |
ok |
返回码描述。 |
errcode |
Number |
0 |
返回码。 |
请求示例
DingTalkClientclient=newDefaultDingTalkClient("https://oapi.dingtalk.com/gettoken"); OapiGettokenRequestrequest=newOapiGettokenRequest(); request.setAppkey("appkey"); request.setAppsecret("appsecret"); request.setHttpMethod("GET"); OapiGettokenResponseresponse=client.execute(request); System.out.println(response.getBody());
返回示例
{ "errcode": 0, "access_token": "96fc7a7axxx", "errmsg": "ok", "expires_in": 7200}
4.1.3 通过免登码获取用户信息
开发者需要使用本接口通过access_token和免登接口中获取的code来获取用户userid。
参数说明
名称 |
类型 |
是否必填 |
示例值 |
描述 |
access_token |
String |
是 |
6ed1bxxx |
调用服务端API的应用凭证。 ●企业内部应用,通过获取企业内部应用的access_token接口获取 ●第三方企业应用,通过获取第三方企业应用的ccess_token接口获取 |
code |
String |
是 |
bab02f63c1e030fbbxxxx |
免登授权码,获取方式包括: ●小程序免登授权码 ●微应用免登授权码 |
返回参数
名称 |
类型 |
示例值 |
描述 |
request_id |
String |
e8krly4vyiln |
请求ID。 |
errcode |
Number |
0 |
返回码。 |
errmsg |
String |
ok |
对返回码的文本描述内容。 |
result |
UserGetByCodeResponse |
|
返回结果。 |
请求示例
DingTalkClientclient=newDefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getuserinfo"); OapiV2UserGetuserinfoRequestreq=newOapiV2UserGetuserinfoRequest(); req.setCode("bab02f63c1e030fbbxxxx"); OapiV2UserGetuserinfoResponsersp=client.execute(req, access_token); System.out.println(rsp.getBody());
返回示例
{ "errcode": 0, "result": { "associated_unionid": "N2o5U3axxxx", "unionid": "gliiW0piiii02zBUjUxxxx", "device_id": "12drtfxxxxx", "sys_level": 1, "name": "张xx", "sys": true, "userid": "userid123" }, "errmsg": "ok"}
4.2 钉钉登录第三方网站
钉钉登录第三方网站通过扫码或账号密码方式实现用户登录第三方网站,已应用于红十字基金会项目。在本场景中,第三方网站可以获取用户授权的个人信息。之前4A组件已经实现了钉钉扫码登录,但是由于钉钉对使用扫码登录第三方网站产品功能进行升级,功能暂不会下线。本次开发的登录功能基于升级后的钉钉扫码登录第三方网站产品。
旧版钉钉扫码登录核心逻辑是通过authCode直接换取userId,新版钉钉扫码登录的核心逻辑是通过authCode换取个人token,使用个人token换取unionId,再请求企业内部token,使用unionId和企业内部token换取userId,更具安全性。
核心步骤分为四步:
●authCode换取个人token
●个人token换取unionId
●请求企业内部token
●unionId和企业内部token换取userId
4.2.1 引入依赖
<dependency><groupId>com.aliyun</groupId><artifactId>dingtalk</artifactId><version>1.1.86</version></dependency>
4.2.2 authCode换取个人token
根据authCode,调用服务端获取用户token接口,获取用户个人token
/*** 获取用户token* @param authCode* @return* @throws Exception*///接口地址:注意/auth与钉钉登录与分享的回调域名地址一致value="/auth", method=RequestMethod.GET) (publicStringgetAccessToken( (value="authCode")StringauthCode) throwsException { com.aliyun.dingtalkoauth2_1_0.Clientclient=authClient(); GetUserTokenRequestgetUserTokenRequest=newGetUserTokenRequest() //应用基础信息-应用信息的AppKey,请务必替换为开发的应用AppKey .setClientId("AppKey") //应用基础信息-应用信息的AppSecret,,请务必替换为开发的应用AppSecret .setClientSecret("AppSecret") .setCode(authCode) .setGrantType("authorization_code"); GetUserTokenResponsegetUserTokenResponse=client.getUserToken(getUserTokenRequest); //获取用户个人tokenStringaccessToken=getUserTokenResponse.getBody().getAccessToken(); returngetUserinfo(accessToken); }
4.2.3 个人token换取unionId
根据用户个人token,调用获取通讯录个人信息接口,实现获取用户个人信息。
/*** 获取用户个人信息* @param accessToken* @return* @throws Exception*/publicStringgetUserinfo(StringaccessToken) throwsException { com.aliyun.dingtalkcontact_1_0.Clientclient=contactClient(); GetUserHeadersgetUserHeaders=newGetUserHeaders(); getUserHeaders.xAcsDingtalkAccessToken=accessToken; //获取用户个人信息Stringme=JSON.toJSONString(client.getUserWithOptions("me", getUserHeaders, newRuntimeOptions()).getBody()); System.out.println(me); returnme; }
4.2.4 请求企业内部token
企业内部应用调用本接口获取access_token。调用服务端API获取应用资源时,需要通过access_token来鉴权调用者身份进行授权。
请求示例
DingTalkClientclient=newDefaultDingTalkClient("https://oapi.dingtalk.com/gettoken"); OapiGettokenRequestrequest=newOapiGettokenRequest(); request.setAppkey("appkey"); request.setAppsecret("appsecret"); request.setHttpMethod("GET"); OapiGettokenResponseresponse=client.execute(request); System.out.println(response.getBody());
返回示例
{ "errcode": 0, "access_token": "96fc7a7axxx", "errmsg": "ok", "expires_in": 7200}
4.2.5 unionId和企业内部token换取userId
请求示例
DingTalkClientclient=newDefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/getbyunionid"); OapiUserGetbyunionidRequestreq=newOapiUserGetbyunionidRequest(); req.setUnionid("z21HjQliSzpw0Yxxxxxx"); OapiUserGetbyunionidResponsersp=client.execute(req, access_token); System.out.println(rsp.getBody());
返回示例
{ "errcode":"0", "errmsg":"ok", "result":{ "contact_type":"0", "userid":"zhangsan" }, "request_id": "XXXXXXXXXX"}
4.3 专有钉钉扫码实现登录第三方网站
首先搞清楚钉钉,专有钉钉,浙政钉的关系:
钉钉是阿里巴巴集团专为中小企业打造的沟通和协同的多端平台。
专有钉钉原名政务钉钉,有更开放的设计能力、集成能力、更结构化协同产品、更定制化开发平台。支持专有化部署,数据落本地,更安全、客户可对该产品持牌运营、结构化编排,协助构建本地应用中心、提供移动应用全面安全保障体系。
浙政钉为规范浙政钉整体架构体系,按照统分结合原则,由省政府办公厅统一设计整体工作界面和系统框架,统筹指导全省统建应用建设,各单位根据自身业务特点分别建设自建应用,最终形成全省统一的政府系统掌上协同办公平台。
●浙政钉 1.0 钉钉上的一个特殊组织架构。
●浙政钉 2.0 根据2020年浙江省深化“最多跑一次”改革推进政府数字化转型工作要点,为实现基本建成“掌上办公之省”年度工作目标,省大数据局联合阿里巴巴启动了“浙政钉”的迭代升级,将浙政钉从钉钉组织迁移出来,成为独立的浙政钉客户端。
三者之间的关系:
●钉钉 和 专有钉钉 & 浙政钉 是两者独立的系统,两者没有什么关系。
●浙政钉 是 专有钉钉 的一个特殊租户,是 专有钉钉 SAAS化部署给政府使用的一个专有钉钉(改名为浙政钉,本质上还是专有钉钉)。
专有钉钉扫码实现登录第三方网站已经应用于绍兴交投项目。4A基线已经支持公版钉的扫码登录,但绍兴交投采购的是政务钉,属于专有钉钉的一种,因此在实现公版钉钉扫码登录功能之外再实现专有钉扫码登录功能。
核心步骤分为两步:
●appkey和appsecret获取access_token
●access_token和authCode换取userId
4.3.1 获取应用标识及 appkey/appSecret
首先到开放平台的开发者中心创建扫码登录应用
填入如下必填信息,完成应用创建
创建完应用,可以复制应用标识(作为构造登录页面client_id的参数值),点击详情进入应用配置页面
配置应用基础信息(应用凭证处查看应用的App Key和App Secret。基础信息可以进行编辑,如下图。)
点击应用配置,配置回调地址
4.3.2 构造扫码登录页面
步骤1:在页面中通过iframe嵌入页面
通过方式一构造的地址增加embedMode=true的参数
https://login.dg-work.cn/oauth2/auth.htm?response_type=code&client_id=应用标识&redirect_uri=回调地址&scope=get_user_info&a
步骤2:扫码成功后需要在页面中监听扫码结果
<scripttype="application/javascript">window.addEventListener('message', function(event) { // 这里的event.data 就是登录成功的信息// 数据格式:{ "code": "aaaa", "state": "bbbb" }alert(JSON.stringify(event.data)); }); </script>
注意:生成二维码大小固定为200*200px,不支持修改。
4.3.3 配置各环境域名/登录域名
完整接口地址:https://环境域名/+接口名 例如:https://openplatform.dg-work.cn/gettoken.json
环境 |
开放平台域名(调接口使用) |
登录域名(构造登录页面) |
Saas |
openplatform.dg-work.cn |
login.dg-work.cn |
浙政钉 |
openplatform-pro.ding.zj.gov.cn (域名对应政务外网IP:59.202.52.1) |
login-pro.ding.zj.gov.cn (域名对应政务外网IP:59.202.52.68) |
在用户中心代码的bootstrap-yunqiao.yml配置文件中新增开放平台域名配置
在大禹平台-用户中心的环境变量中新增开放平台域名配置
4.3.4 引入专有钉扫码登录依赖
引入依赖
<dependency><groupId>com.oracel</groupId><artifactId>zwdd-sdk-java</artifactId><version>1.2.0</version></dependency>
jar生成maven依赖命令
mvninstall:install-file-Dfile="下载地址"\zwdd-sdk-java-1.2.0.jar-DgroupId=com.oracel-DartifactId=zwdd-sdk-java-Dversion=1.2.0-Dpackaging=jar
4.3.5 获取应用access_token
【注意】正常情况下 access_token 有效期为7200秒,有效期内重复获取返回相同结果,并⾃动续期。
请求方式:GET(HTTPS)
接口名
/gettoken.json
请求参数
参数 |
参数类型 |
必须 |
说明 |
appkey |
String |
是 |
应用的唯一标识key |
appsecret |
String |
是 |
应用的密钥 |
请求示例
ExecutableClientexecutableClient=ExecutableClient.getInstance(); executableClient.setAccessKey("appkey"); executableClient.setSecretKey("appsecrt"); executableClient.setDomainName("不同环境对应不同域名"); executableClient.setProtocal("https"); executableClient.init(); //executableClient要单例,并且使用前要初始化,只需要初始化一次Stringapi="/gettoken.json"; GetClientgetClient=executableClient.newGetClient(api); //设置参数getClient.addParameter("appkey", "XXXXXXXXXX"); getClient.addParameter("appsecret", "XXXXXXXXXX"); //调用APIStringapiResult=getClient.get(); System.out.println(apiResult);
返回参数
参数 |
说明 |
accessToken |
应用access_token |
expiresIn |
过期时间,单位(秒) |
返回结果
{ "success":true, "content":{ "data":{ "accessToken":"XXXXXXXXXX", "expiresIn":"7200" }, "requestId":"XXXXXXXXXX", "responseMessage":"OK", "responseCode":"0", "success": true } }
4.3.6 获取授权用户的个人信息
服务端通过临时授权码获取授权用户的个人信息
请求方式:POST(HTTPS)
接口名
/rpc/oauth2/getuserinfo_bycode.json
注意:请使用接口域名调用接口,不能使用登录域名调接口。
请求参数
参数 |
参数类型 |
必须 |
说明 |
access_token |
String |
是 |
调用接口凭证,应用access_token |
code |
String |
是 |
用户授权的临时授权码code,只能使用一次;在前面步骤中跳转到redirect_uri时会追加code参数 |
代码示例
ExecutableClientexecutableClient=ExecutableClient.getInstance(); executableClient.setAccessKey(appKey); executableClient.setSecretKey(appSecret); executableClient.setDomainName(domainName); executableClient.setProtocal("https"); executableClient.init(); //executableClient要单例,并且使用前要初始化,只需要初始化一次Stringapi="/rpc/oauth2/getuserinfo_bycode.json"; GetClientgetClient=executableClient.newGetClient(api); //设置参数getClient.addParameter("access_token", "xxxxx"); getClient.addParameter("code", "xxxxx"); //调用APIStringapiResult=getClient.get();
返回结果
{ "success":true, "content":{ "data":{ "accountId":100135, "lastName":"name", "clientId":"mozi-buc-sso", "realmId":12371, "tenantName":"租户2", "realmName":"租户2", "namespace":"local", "tenantId":12371, "nickNameCn":"name", "tenantUserId":"12371$100135", "account":"admin2" }, "success":true, "responseMessage":"成功", "responseCode":"0" } }
返回参数
参数 |
说明 |
accountId |
账号id |
realmId |
租户id |
realmName |
租户名 |
lastName |
姓名 |
nickNameCn |
昵称 |
account |
登录账号 |
employeeCode |
人员code |
tenantUserId |
员工在当前企业内的唯一标识 |
namespace |
账号类型标识 |
clientId |
应用标识 |
tenantId |
租户id |
tenantName |
租户名 |
4.4 企业微信扫码登录实现登录第三方网站
企业微信提供了OAuth的授权登录方式,可以让从企业微信终端打开的网页获取成员的身份信息,从而免去登录的环节。企业应用中的URL链接(包括自定义菜单或者消息中的链接),均可通过OAuth2.0验证接口来获取成员的UserId身份信息。
企业微信OAuth2接入流程
4.4.1 生成企业微信二维码
登录方式有两种,但是仅有第一步不同,后面方式不变
url直接给前端,由前端处理重定向,用户扫码后得到参数code,携参调用后端用户验证接口
●网页授权登录:在企业微信中直接登录
●扫码授权登录:在网站上,扫码登录
网页授权登录
●API
https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&agentid=AGENTID&state=STATE#wechat_redirect
参数解析
参数 |
必须 |
说明 |
appid |
是 |
企业的CorpID |
redirect_uri |
是 |
授权后重定向的回调链接地址,请使用urlencode对链接进行处理 |
response_type |
是 |
返回类型,此时固定为:code |
scope |
是 |
应用授权作用域。 snsapi_base:静默授权,可获取成员的的基础信息(UserId与DeviceId);snsapi_privateinfo:手动授权,可获取成员的详细信息,包含头像、二维码等敏感信息 |
agentid |
是 |
企业应用的id。注意redirect_uri的域名必须与该应用的可信域名一致。 |
state |
否 |
重定向后会带上state参数,企业可以填写a-zA-Z0-9的参数值,长度不可超过128个字节 |
#wechat_redirect |
是 |
终端使用此参数判断是否需要带上身份信息 |
●返回值
●直接返回带code的链接
http://api.3dept.com/cgi-bin/query?action=get&code=AAAAAAgG333qs9EdaPbCAP1VaOrjuNkiAZHTWgaWsZQ&state= # 拿到code调用后端接口
4.4.2 扫码授权登录
●API
https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=CORPID&agentid=AGENTID&redirect_uri=REDIRECT_URI
参数解析
参数 |
必须 |
说明 |
appid |
是 |
企业微信的CorpID,在企业微信管理端查看 |
agentid |
是 |
授权方的网页应用ID,在具体的网页应用中查看 |
redirect_uri |
是 |
重定向地址,需要进行UrlEncode |
state |
否 |
用于保持请求和回调的状态,授权请求后原样带回给企业。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议企业带上该参数,可设置为简单的随机数加session进行校验 |
lang |
否 |
自定义语言,支持zh、en;lang为空则从Headers读取Accept-Language,默认值为zh |
●扫码后返回值
redirect_uri?code=CODE&state=STATE # 拿到code调用后端接口
4.4.3 获取AccessToken
AccessToken有效期是2小时(两小时内重复获取是不变的),频繁获取会报错,因此需要后端缓存起来(可以使用redis)
●API
GET(HTTPS) https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
●参数解析
●corpid:每个企业都拥有唯一的corpid
●corpsecret:serect是企业应用里面用于保障数据安全的“钥匙”,务必不能泄漏
●返回值
{ "errcode": 0, "errmsg": "ok", "access_token": "accesstoken000001", "expires_in": 7200}
java实现
●调用api
name="QYWechatApi", url="https://qyapi.weixin.qq.com") (publicinterfaceQYWechatApi { /*** 获取access_token* https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET*/"GET /cgi-bin/gettoken?corpid={corpId}&corpsecret={corpSecret}") (WxTokenVOgetAccessToken( ("corpId") StringcorpId, "corpSecret") StringcorpSecret); (}
获取AccessToken
privateWxTokengetAccessToken(){ // 从redis获取accessToken,没有则获取新的并存入redisWxTokenaccessToken= (WxToken)redisTemplate.opsForValue().get("key"); if(accessToken==null){ accessToken=qYWechatApi.getAccessToken(QYWxConfig.CORP_ID, QYWxConfig.CORP_SECRET); if (accessToken.getErrcode() !=null&&accessToken.getErrcode() !=0) { log.error("获取企业微信账户accessToken异常: {}", JSON.toJSONString(accessToken)); } redisTemplate.opsForValue().set("key", accessToken.getAccess_token(), 119, TimeUnit.MINUTES); } returnaccessToken; }
4.4.4 获取访问用户身份
扫码后获得用户企业内唯一身份UserId(作用:与数据库中进行比对,如比对成功则登录成功,否则登录失败)
●API
GET(HTTPS)https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
返回值
{ "errcode": 0, "errmsg": "ok", "UserId":"USERID"}
java实现
●调用api
name="QYWechatApi", url="https://qyapi.weixin.qq.com") (publicinterfaceQYWechatApi { /*** 获取用户身份信息* https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE*/"GET /cgi-bin/user/getuserinfo?access_token={accessToken}&code={code}") (WxUserInfogetUserInfo( ("accessToken") StringaccessToken, "code") Stringcode); (
●获取用户身份
publicWxUserInfogetUserInfo(Stringcode) throwsEmcsCustomException { // 获得accessTokenWxTokenVOaccessToken=getAccessToken(); // 根据accessToken和code获得企业微信的userIdWxUserInfouserInfo=qYWechatApi.getUserInfo(accessToken.getAccess_token(), code); if (userInfo.getErrcode() !=null&&userInfo.getErrcode() !=0) { log.error("根据token获取用户信息异常: {}", JSON.toJSONString(userInfo)); } returnuserInfo; }
4.4.5 获取成员详细信息(拓展步骤)
第三步获取的UserId是公司唯一,这步可以获得用户详细信息包括全局唯一open_userid
●API
GET(HTTPS)https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&userid=USERID
●返回值
{ "errcode": 0, "errmsg": "ok", "userid": "zhangsan", "name": "张三", "department": [1, 2], "order": [1, 2], "position": "后台工程师", "mobile": "13800000000", "gender": "1", "email": "zhangsan@gzdev.com", "is_leader_in_dept": [1, 0], "avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0", "thumb_avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/100", "telephone": "020-123456", "alias": "jackzhang", "address": "广州市海珠区新港中路", "open_userid": "xxxxxx", "main_department": 1, "extattr": { "attrs": [ { "type": 0, "name": "文本名称", "text": { "value": "文本" } }, { "type": 1, "name": "网页名称", "web": { "url": "http://www.test.com", "title": "标题" } } ] }, "status": 1, "qr_code": "https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx", "external_position": "产品经理", "external_profile": { "external_corp_name": "企业简称", "external_attr": [{ "type": 0,
java实现
●调用api
name="QYWechatApi", url="https://qyapi.weixin.qq.com") (publicinterfaceQYWechatApi { /*** 获取用户身份信息* https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE*/"GET /cgi-bin/user/get?access_token={accessToken}&userid={userId}") (WxUserInfogetUserDetailInfo( ("accessToken") StringaccessToken, "userId") StringuserId); (
●获取用户身份
publicWxUserInfoVOgetUserInfo(Stringcode) throwsEmcsCustomException { // 获得accessTokenWxTokenVOaccessToken=getAccessToken(); // 根据accessToken和code获得企业微信的userIdWxUserInfouserInfo=qYWechatApi.getUserInfo(accessToken.getAccess_token(), code); if (userInfo.getErrcode() !=null&&userInfo.getErrcode() !=0) { log.error("根据token获取用户信息异常: {}", JSON.toJSONString(userInfo)); } // 根据企业微信的userId获得用户详细信息userInfo=qYWechatApi.getUserDetailInfo(accessToken.getAccess_token(), code); if (userInfo.getErrcode() !=null&&userInfo.getErrcode() !=0) { log.error("根据userId获取用户详细信息异常: {}", JSON.toJSONString(userInfo)); } returnuserInfo; }
5. 总结
随着数字化转型的不断深入,通过组装式的方式开发的云巧越来越多地应用于政企客户高度复杂行业应用系统中,4A组件作为云巧的核心组件之一,支持了大量政企数字化转型项目。不同项目对于4A组件的第三方登录方式有越来越多的要求,我们在实际项目中积累了多种第三方登录方式。本文以此为契机主要介绍了常见的第三方登录认证协议以及它们之间的对比和云巧4A组件第三方登录模块的实现。随着互联网的快速发展,第三方登录的方式将会越来越丰富,后期4A组件可能还会拓展企业微信或其他方式,但这些第三方登录方式实现逻辑基本相似,希望本文能为大家在了解三方登录协议以及第三方登录具体实现带来一些参考意义。