本文主要目的为:指导开发者进行自有服务与Azure AD统一认证的集成,以及阐述云端用户数据同步的实现方案。本文除了会介绍必要的概念、原理、流程外,还会包含Azure门户设置说明,以及使用Fiddler进行全流程的实操验证,同时还会结合实际的业务需求提出具体的方案建议及关键实现方法的说明,以此达到开发指导的效果。
Azure Active Directory,简称 Azure AD,是微软提供的云身份验证服务,用于管理用户身份和访问权限。它是基于标准的开放式身份协议构建的,可以与多种应用程序和服务集成,包括云应用程序、本地应用程序和移动应用程序。

总之,Azure AD 是一个灵活且功能强大的身份验证和访问管理平台,可以帮助组织实现身份验证、授权和安全管理的统一化。从本质上来说,Azure AD 是基于 OAuth 2.0 和 OpenID Connect 规范的。OAuth 2.0 是一种用于授权的开放标准,用于安全地委托访问令牌,而 OpenID Connect 则是在 OAuth 2.0 的基础上添加了身份验证的协议。Azure AD 结合了这两种标准,以提供身份验证和授权功能,支持多种身份验证方法,并提供与其他服务和应用程序的集成。
需要补充说明的是Microsoft 将 Azure Active Directory (Azure AD) 更名为 Microsoft Entra ID,以介绍产品的多云多平台功能、缓解与 Windows Server Active Directory 的混淆,并统一 Microsoft Entra 产品系列。详见官方文档:https://learn.microsoft.com/zh-cn/entra/
Azure AD 和 Active Directory Federation Services(ADFS)都是由微软提供的身份认证解决方案,但它们在一些方面有所不同:
综上所述,Azure 统一认证适用于云环境和混合云环境,提供了更多的托管功能和集成选项,而 ADFS 则更适用于需要本地部署、自定义和控制的场景。关于ADFS及AD的部署配置可以参考:https://blog.csdn.net/camelials/article/details/134857159
Microsoft Graph 是微软提供的统一 API 平台,用于访问 Microsoft 365 中的各种数据和服务。它提供了一种统一的方式来访问多种 Microsoft 产品和服务的数据,包括 Office 365、Azure Active Directory、Exchange Online、SharePoint 等。

理解 Microsoft Graph 可以从以下几个方面来看:
总的来说,Microsoft Graph 提供了一种统一、灵活的方式来访问 Microsoft 365 中的数据和服务,为开发者提供了丰富的功能和资源,帮助他们构建创新的应用程序和解决方案。Microsoft Graph 中的主要服务和功能可以参考官方文档:https://learn.microsoft.com/zh-cn/graph/overview-major-services
微软Graph分为中国版(由上海世纪互联运营)和国际版,两者大致一样,但是也存在一些差异,例如:Portal门户设置UI不同,终结点不同,一些接口的参数和约定不同等,这里以全球版的v1.0进行介绍。
1、基本设置

应用程序主要基本设置如上图,其中租户ID(tenant_id)、应用程序ID(client_id)、客户端凭据(client_secret)、重定向URL(redirect_uri)等在后续的授权及微软Graph API调用的时候需要使用。例如:
### 登录授权终结点
https://login.microsoftonline.com/#{tenant}/oauth2/v2.0/authorize?client_id=#{client_id}&response_type=code&redirect_uri=#{redirect_uri}
### 授权码获取Token
POST https://login.microsoftonline.com/#{tenant}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id=#{client_id}
&scope=User.Read
&redirect_uri=#{redirect_uri}
&grant_type=authorization_code
&client_secret=#{client_secret}
&code=#{auth_code}
2、API权限设置

1、可以把Microsoft Graph理解为访问Microsoft 365 和 Azure 云端资源的网关,若要访问这些资源则需要用户或管理员向其授予所需的权限。如果权限设置不当,则在实际的访问中就会遇到各种权限相关错误,例如:Authorization_RequestDenied(Insufficient privileges to complete the operation.),因此权限控制是实操过程中经常被提及和困惑的问题。详细可以参考微软官方文档:https://learn.microsoft.com/zh-cn/graph/permissions-reference
2、从上图中的权限类型可以看到有2种:委托和应用程序,委托可以理解为用户认证成功之后的委托授权访问,为了好理解,我把这称之为:AppUser权限(需要用户授权);应用程序则不需要用户授权,而是由管理员授权,为了好理解和区分,可以称之为:AppOnly权限。举个例子:
在这里把开通Directory.Read.All和User.Read.All的AppOnly权限的原因是对于用户数据同步的方案,我们将会选择Delta Query方案。对于用户数据同步的方案微软其实提供了2种,后面会详细讲。

在了解了AppUser方式授权和AppOnly授权的差异之后,对于上面的流程图就能很容易的理解了,下面对于一些关键点的开发指导做进一步的描述:
1、AppServer需要向客户端提供一个后去登录认证地址的接口。认证终结点地址可以在Azure应用程序中进行查看,如下图:

同时,认证终结点还需要携带一些参数,示例如下(相关含义,前面的《应用程序注册/设置》章节已经进行过描述):
https://login.microsoftonline.com/#{tenant}/oauth2/v2.0/authorize?client_id=#{client_id}&response_type=code&redirect_uri=#{redirect_uri}
2、客户端拿到登录认证地址即可进行登录,如下图。一般来说,客户端会使用系统浏览器访问Azure授权终结点,那么会遇到一个问题:此时已不在客户端App的进程之内,那么后续该如何重新回到App,或者如何通知App登录认证完成呢?问题先放着,后面会讲。

3、登录完成之后Azure授权终结点会向事先配置好的回调终结发送一个GET请求,该请求会包含一个授权码,表示认证通过可以授权。下面演示通过使用Fiddler抓包去查看授权码:

通过上面的方法,我们可以获得授权码。由于这里只是测试,因此我把回调地址配置成了一个不存在的:https://www.baidu.com/login,因此404,但是这并不影响我们通过用Fiddler抓包去查看授权码。

4、拿到授权码后就可以请求Azure令牌终结点去获取Token了,请求示例如下:
### token[authorization_code]
POST https://login.microsoftonline.com/#{tenant}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id=#{client_id}
&scope=User.Read
&redirect_uri=#{redirect_uri}
&grant_type=authorization_code
&client_secret=#{client_secret}
&code=#{auth_code}
应答示例如下:
HTTP/1.1 200 OK
Cache-Control: no-store, no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
P3P: CP="DSP CUR OTPi IND OTRi ONL FIN"
x-ms-request-id: 803bf994-b432-4ee8-8380-b1e370535b00
x-ms-ests-server: 2.1.17846.6 - SEASLR1 ProdSlices
x-ms-srs: 1.P
X-XSS-Protection: 0
Set-Cookie: fpc=Al_NmhqaTrVPmpaVIt5Vo10V53uDAQAAAD0mtN0OAAAA; expires=Sun, 19-May-2024 08:45:50 GMT; path=/; secure; HttpOnly; SameSite=None
Set-Cookie: x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly
Date: Fri, 19 Apr 2024 08:45:50 GMT
Content-Length: 2340
{
"token_type": "Bearer",
"scope": "User.Read profile openid email",
"expires_in": 5229,
"ext_expires_in": 5229,
"access_token": "eyJ0eXAiOiJKV1QiLCJub25jZSI6Ik..."
}
需要补充说明的是access_token其实就是一个json的jwt序列化字符串,https://jwt.ms/ 可以进行在线解码:

其实使用com.auth0.jwt.JWT也可以进行解码,但是并不推荐通过这样的方式去获取用户信息,因为目前尚不明确微软对于算法是如何约定的,通过穷尽尝试几种常用的算法成功解码,假如有天微软把算法变了,程序不就出Bug了吗?这里,只是向告诉大家access_token的本质其实就是一个类似ADFS的授权Claims Json信息,常见的 JWT 签名算法有:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JWTDecoder {
public static void main(String[] args) {
String jwtToken = "your_jwt_token_here";
String secretKey = "your_secret_key_here";
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(jwtToken);
// 获取 JWT 中的用户 UPN
String upn = decodedJWT.getSubject();
System.out.println("User UPN: " + upn);
} catch (Exception e) {
// 处理异常
e.printStackTrace();
}
}
}
5、拿到AccessToken之后就可以请求微软Graph根据业务处理的需要去后去资源了,一般业务场景下都需要首先获得当前登录用户的Profile信息,请求示例如下:
### me
GET https://graph.microsoft.com/v1.0/me
Authorization: Bearer #{access_token}
微软Graph参考资源:
6、前面抛出了一个问题:客户端会使用系统浏览器访问Azure授权终结点那么后续该如何重新回到App,或者如何通知App登录认证完成了呢?要解决这个问题其实方案有很多,例如:
1)集成微软SDK;
2)通过服务端输出合适的JS将控制权交给重新交给客户端;
3)重定向到一个中间站点并通过url参数方式传值;
4)将处理结果放在header中;
5)服务端下发通知给客户端(双工App应用,服务端可以向客户端下发通知);
这里我推荐使用方案2,示例JS脚本如下:
<html>
<header>header>
<body>
body>
<script language='javascript'>
if (window.ad) {
window.ad.end(0, 'ok');
}
script>
html>
这段 JavaScript 脚本首先检查当前页面是否存在名为 “ad” 的全局对象或变量。如果存在,它调用 “ad” 对象的 “end” 方法,并传入两个参数:0 和 ‘ok’,其中,0 代表状态码,‘ok’ 为描述(code + msg 表达认证回调的服务端处理结果)。
通过服务端输出合适的JS将控制权交给重新交给客户端,这其实是目前的一种经典做法,因为JS是运行在客户端的Local上。上述JS脚本代码通常在网页中嵌入的广告脚本中使用,当广告成功加载并展示后,广告服务商通常会调用类似的方法来通知页面,以便页面做出相应的处理。

在了解了AppUser授权的相关内容后,AppOnly的授权就会十分简单了:AppServer直接请求Azure令牌终结点获取Token。当然,这里需要事先在Azure门户中对API权限进行应用程序授权(前面已经介绍过),请求示例如下:
### token[client_credentials]
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id=#{client_id}
&redirect_uri=#{redirect_uri}
&grant_type=client_credentials
&client_secret=#{client_secret}
&scope=https://graph.microsoft.com/.default
应答示例如下(这里贴个实测的图来证明确实可行)。

最后,大家可以用这种方式下获得的Token去调用微软Graph(当然,要访问的资源需要事先由Azure管理员授权),后面会涉及获取租户下所有的用户数据,用的token就是这种。大家可以比较下与AppUser授权方式获取Token的差异,那么就会发现grant_type和scope的不同。关于scope=https://graph.microsoft.com/.default的说明,大家可以去查阅微软官网。
Azure提供2种变更数据获取方案:订阅(webhook)方案、Delta Query方案,综合利弊建议大家选择Delta Query方案。
| 方案 | 利 | 弊 |
|---|---|---|
| 订阅方案 | 1、对于Azure第三方应用程序服务端实现简单,只需要接收数据变更通知并予以处理即可; | 1、webhook方式接收方无法自主控制频度和重试(对于订阅方式,Azure 平台会尽力发送数据通知,但并不保证数据的实时性或完整性,在设计应用程序时应考虑到这一点,并实施适当的容错机制) 2、Azure订阅会有费用成本(计费策略因子为:订阅的数量、通知的频率、数据传输量等),而且可以设置费用告警阈值。这些可能产生解释/沟通成本,同时可能会被客户挑战:为什么不使用无费用的Delta Query方案。 3、webhook订阅数据通知只限于增量变化数据的获取,首次全量数据同步仍然需要从微软Graph接口中获取。 |
| Delta Query方案 | 1、可以自主控制数据获取的频度和失败重试。 2、不涉及成本问题的客户挑战。 3、首次全量数据获取与增量数据获取的方案和处理统一(Delta Query接口有类似版本号的可选参数,不携带该参数则为首次全量获取) | 1、Azure第三方应用程序服务端实现相对复杂:需要自行考虑频度,重试等问题。同时还需要自行维护数据版本信息以实现增量数据获取。 2、需要一次性对应用程序API权限进行授权(AppOnly授权方式)。 |
1、Delta Query方案其实是一个轮询方案,那么就会涉及定时任务,当前可选的框架很多:Spring Framework 的任务执行器(TaskExecutor)、Quartz 、XX-Job等。至于说Delta Query的频度,这个大家根据业务需要自行指定(一天一次也好,多少小时分钟一次也好)。
2、Delta Query方案大家需要理解如何实现数据的增量获取,我给大家演示下首次全量数据获取和非首次增量数据获取大家就明白了。


每次Delta Query请求应答都会返回一个deltatoken,其实可以把这个deltatoken理解为资源的版本。那么就很容易的能够理解:首次全量数据获取就不携带deltatoken,要获取某个版本之后的增量数据就携带deltatoken去请求即可。
最后需要说明的是:对于订阅和Delta Query其实是Azure对于很多资源的通用方案,因此不只是users可以使用,其他的资源也可以使用。前面说了,可以把微软Graph理解为访问云端资源的网关,用户是资源,邮件是资源,outlook等都是资源。

1、要实现Azure AD统一认证与自己业务的系统的集成,一般来说自身业务系统需要实现2个接口即可:1)获取登录认证Url接口(getAuthUrl);2)认证回调处理接口(authCallback);
2、自身业务系统需要维护自身业务系统UID与Azure AD的用户唯一标识(一般为用户的UPN,这也是AD规范中所推荐使用的),可以把这个过程称之为绑定(大家可以参考下IDasS厂商的一些共同做法),对于已经绑定的用户数据就可以支持定时与云端的数据同步。
3、要实现Azure AD用户数据与自身业务系统用户数据的同步,虽然微软提供了2种方案(订阅方案和Delta Query方案),但是结合一般业务场景下的利弊分析后,建议选择Delta Query方案,但是这也不是绝对的,如何选择还得具体问题具体分析。
上述内容调研和折腾了几天的时间,整个过程也都进行了实操验证,而且我也尽量把一些概念、原理也都给予一定的介绍,最后个人认为做调研以下两点非常重要:
1)知其然,知其所以然。
2)一定要上手实操验证。