You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 09 | 实战利用OAuth 2.0实现一个OpenID Connect用户身份认证协议
你好,我是王新栋。
如果你是一个第三方软件开发者,在实现用户登录的逻辑时,除了可以让用户新注册一个账号再登录外,还可以接入微信、微博等平台,让用户使用自己的微信、微博账号去登录。同时,如果你的应用下面又有多个子应用,还可以让用户只登录一次就能访问所有的子应用,来提升用户体验。
这就是联合登录和单点登录了。再继续深究它们其实都是OpenID Connect简称OIDC的应用场景的实现。那OIDC又是什么呢
今天我们就来学习下OIDC和OAuth 2.0的关系以及如何用OAuth 2.0来实现一个OIDC用户身份认证协议。
## OIDC是什么
OIDC其实就是一种用户身份认证的开放标准。使用微信账号登录极客时间的场景就是这种开放标准的实践。
说到这里你可能要发问了“不对呀使用微信登录第三方App用的不是OAuth 2.0开放协议吗怎么又扯上OIDC了呢
没错用微信登录某第三方软件确实使用的是OAuth 2.0。但OAuth2.0是一种授权协议而不是身份认证协议。OIDC才是身份认证协议而且是基于OAuth 2.0来执行用户身份认证的互通协议。更概括地说OIDC就是直接基于OAuth 2.0 构建的身份认证框架协议。
换种表述方式,**OIDC=授权协议+身份认证**是OAuth 2.0的超集。为方便理解我们可以把OAuth 2.0理解为面粉把OIDC理解为面包。这下你是不是就理解它们的关系了因此我们说“第三方App使用微信登录用到了OAuth 2.0”没有错说“使用到了OIDC”更没有错。
考虑到单点登录、联合登录都遵循的是OIDC的标准流程因此今天我们就讲讲如何利用OAuth2.0来实现一个OIDC“高屋建瓴” 地去看问题。掌握了这一点,我们再去做单点登录、联合登录的场景,以及其他更多关于身份认证的场景,就都不再是问题了。
## OIDC 和 OAuth 2.0 的角色对应关系
说到“如何利用 OAuth 2.0 来构建 OIDC 这样的认证协议”我们可以想到一个切入点这个切入点就是OAuth 2.0 的四种角色。
OAuth 2.0的授权码许可流程的运转需要资源拥有者、第三方软件、授权服务、受保护资源这4个角色间的顺畅通信、配合才能够完成。如果我们要想在OAuth 2.0的授权码许可类型的基础上,来构建 OIDC 的话这4个角色仍然要继续发挥 “它们的价值”。那么这4个角色又是怎么对应到OIDC中的参与方的呢
那么,我们就先想想一个关于身份认证的协议框架,应该有什么角色。你可能已经想出来了,它需要一个登录第三方软件的最终用户、一个第三方软件,以及一个认证服务来为这个用户提供身份证明的验证判断。
没错这就是OIDC的三个主要角色了。在OIDC的官方标准框架中这三个角色的名字是
* EUEnd User代表最终用户。
* RPRelying Party代表认证服务的依赖方就是上面我提到的第三方软件。
* OPOpenID Provider代表提供身份认证服务方。
EU、RP和OP这三个角色对于OIDC非常重要我后面也会时常使用简称来描述希望你能先记住。
现在很多App都接入了微信登录那么微信登录就是一个大的身份认证服务OP。一旦我们有了微信账号就可以登录所有接入了微信登录体系的AppRP这就是我们常说的联合登录。
现在我们就借助极客时间的例子来看一下OAuth 2.0的4个角色和OIDC的3个角色之间的对应关系
![](https://static001.geekbang.org/resource/image/8f/e9/8f794280f949862af3ebdc61d69c5fe9.png "图1 OAuth 2.0和OIDC的角色对应关系")
## OIDC 和 OAuth 2.0 的关键区别
看到这张角色对应关系图,你是不是有点 “恍然大悟” 的感觉要实现一个OIDC协议不就是直接实现一个OAuth 2.0协议吗。没错我在这一讲的开始也说了OIDC就是基于OAuth 2.0来实现的一个身份认证协议框架。
我再继续给你画一张OIDC的通信流程图你就更清楚OIDC和OAuth 2.0的关系了:
![](https://static001.geekbang.org/resource/image/23/4b/23ce63497f6734dbc6dc9c5b6399c54b.png "图2 基于授权码流程的OIDC通信流程")
可以发现一个基于授权码流程的OIDC协议流程跟OAuth 2.0中的授权码许可的流程几乎完全一致,唯一的区别就是多返回了一个**ID\_TOKEN**,我们称之为**ID令牌**。这个令牌是身份认证的关键。所以接下来我就着重和你讲一下这个令牌而不再细讲OIDC的整个流程。
### OIDC 中的ID令牌生成和解析方法
在图2的OIDC通信流程的第6步我们可以看到ID令牌ID\_TOKEN和访问令牌ACCESS\_TOKEN是一起返回的。关于为什么要同时返回两个令牌我后面再和你分析。我们先把焦点放在ID令牌上。
我们知道访问令牌不需要被第三方软件解析因为它对第三方软件来说是不透明的。但ID令牌需要能够被第三方软件解析出来因为第三方软件需要获取ID令牌里面的内容来处理用户的登录态逻辑。
那**ID令牌的内容是什么呢**
首先ID令牌是一个JWT格式的令牌。你可以到[第4讲](https://time.geekbang.org/column/article/257747)中复习下JWT的相关内容。这里需要强调的是虽然JWT令牌是一种自包含信息体的令牌为将其作为ID令牌带来了方便性但是因为ID令牌需要能够标识出用户、失效时间等属性来达到身份认证的目的所以要将其作为OIDC的ID令牌时下面这5个JWT声明参数也是必须要有的。
* iss令牌的颁发者其值就是身份认证服务OP的URL。
* sub令牌的主题其值是一个能够代表最终用户EU的全局唯一标识符。
* aud令牌的目标受众其值是三方软件RP的app\_id。
* exp令牌的到期时间戳所有的ID令牌都会有一个过期时间。
* iat颁发令牌的时间戳。
生成ID令牌这部分的示例代码如下
```
//GENATE ID TOKEN
String id_token=genrateIdToken(appId,user);
private String genrateIdToken(String appId,String user){
String sharedTokenSecret="hellooauthhellooauthhellooauthhellooauth";//秘钥
Key key = new SecretKeySpec(sharedTokenSecret.getBytes(),
SignatureAlgorithm.HS256.getJcaName());//采用HS256算法
Map<String, Object> headerMap = new HashMap<>();//ID令牌的头部信息
headerMap.put("typ", "JWT");
headerMap.put("alg", "HS256");
Map<String, Object> payloadMap = new HashMap<>();//ID令牌的主体信息
payloadMap.put("iss", "http://localhost:8081/");
payloadMap.put("sub", user);
payloadMap.put("aud", appId);
payloadMap.put("exp", 1584105790703L);
payloadMap.put("iat", 1584105948372L);
return Jwts.builder().setHeaderParams(headerMap).setClaims(payloadMap).signWith(key,SignatureAlgorithm.HS256).compact();
}
```
接下来,我们再看看**处理用户登录状态的逻辑是如何处理的**。
你可以先试想一下,如果 “不跟OIDC扯上关系”也就是 “单纯” 构建一个用户身份认证登录系统我们是不是得保存用户登录的会话关系。一般的做法是要么放在远程服务器上要么写进浏览器的cookie中同时为会话ID设置一个过期时间。
但是当我们有了一个JWT这样的结构化信息体的时候尤其是包含了令牌的主题和过期时间后不就是有了一个“天然”的会话关系信息么。
所以依靠JWT格式的ID令牌就足以让我们解决身份认证后的登录态问题。这也就是为什么在OIDC协议里面要返回ID令牌的原因**ID令牌才是OIDC作为身份认证协议的关键所在**。
那么有了ID令牌后第三方软件应该如何解析它呢接下来我们看一段解析ID令牌的具体代码如下
```
private Map<String,String> parseJwt(String jwt){
String sharedTokenSecret="hellooauthhellooauthhellooauthhellooauth";//密钥
Key key = new SecretKeySpec(sharedTokenSecret.getBytes(),
SignatureAlgorithm.HS256.getJcaName());//HS256算法
Map<String,String> map = new HashMap<String, String>();
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt);
//解析ID令牌主体信息
Claims body = claimsJws.getBody();
map.put("sub",body.getSubject());
map.put("aud",body.getAudience());
map.put("iss",body.getIssuer());
map.put("exp",String.valueOf(body.getExpiration().getTime()));
map.put("iat",String.valueOf(body.getIssuedAt().getTime()));
return map;
}
```
需要特别指出的是第三方软件解析并验证ID令牌的合法性之后不需要将整个JWT信息保存下来只需保留JWT中的PAYLOAD数据体部分就可以了。因为正是这部分内容包含了身份认证所需要的用户唯一标识等信息。
另外在验证JWT合法性的时候因为ID令牌本身已经被身份认证服务OP的密钥签名过所以关键的一点是合法性校验时需要做签名校验。具体的加密方法和校验方法你可以回顾下[第4讲](https://time.geekbang.org/column/article/257747)。
这样当第三方软件RP拿到ID令牌之后就已经获得了处理身份认证标识动作的信息也就是拿到了那个能够唯一标识最终用户EU的ID值比如3521。
### 用访问令牌获取ID令牌之外的信息
但是,为了提升第三方软件对用户的友好性,在页面上显示 “您好3521” 肯定不如显示 “您好,小明同学”的体验好。这里的 “小明同学”,恰恰就是用户的昵称。
那如何来获取“小明同学”这个昵称呢。这也很简单,就是**通过返回的访问令牌access\_token来重新发送一次请求**。当然这个流程我们现在也已经很熟悉了它属于OAuth 2.0标准流程中的请求受保护资源服务的流程。
这也就是为什么在OIDC协议里面既给我们返回ID令牌又返回访问令牌的原因了。在保证用户身份认证功能的前提下如果想获取更多的用户信息就再通过访问令牌获取。在OIDC框架里这部分内容叫做创建UserInfo端点和获取UserInfo信息。
这样看下来细粒度地去看OIDC的流程就是**生成ID令牌->创建UserInfo端点->解析ID令牌->记录登录状态->获取UserInfo**。
好了利用OAuth 2.0实现一个OIDC框架的工作我们就做完了。你可以到[GitHub](https://github.com/xindongbook/oauth2-code/tree/master/src/com/oauth/ch09)上查看这些流程的完整代码。现在,我再来和你小结下。
用OAuth 2.0实现OIDC的最关键的方法是在原有OAuth 2.0流程的基础上增加ID令牌和UserInfo端点以保障OIDC中的第三方软件能够记录用户状态和获取用户详情的功能。
因为第三方软件可以通过解析ID令牌的关键用户标识信息来记录用户状态同时可以通过Userinfo端点来获取更详细的用户信息。有了用户态和用户信息也就理所当然地实现了一个身份认证。
接下来我们就具体看看如何实现单点登录Single Sign OnSSO
## 单点登录
一个用户G要登录第三方软件AA有三个子应用域名分别是a1.com、a2.com、a3.com。如果A想要为用户提供更流畅的登录体验让用户G登录了a1.com之后也能顺利登录其他两个域名就可以创建一个身份认证服务来支持a1.com、a2.com和a3.com的登录。
这就是我们说的单点登录,“一次登录,畅通所有”。
那么可以使用OIDC协议标准来实现这样的单点登录吗我只能说 “太可以了”。如下图所示只需要让第三方软件RP重复我们OIDC的通信流程就可以了。
![](https://static001.geekbang.org/resource/image/7b/48/7bf3cb13a5174f2068c916a4d1ef2748.png "图3 单点登录的通信流程")
你看单点登录就是OIDC的一种具体应用方式只要掌握了OIDC框架的原理实现单点登录就不在话下了。关于单点登录的具体实现在GitHub上搜索“通过OIDC来实现单点登录”你就可以看到很多相关的开源内容。
## 总结
在一些较大的、已经具备身份认证服务的平台上你可能并没有发现OIDC的描述但大可不必纠结。有时候我们可能会困惑于到底是先有OIDC这样的标准还是先有类似微信登录这样的身份认证实现方式呢
其实要理解这层先后关系我们可以拿设计模式来举例。当你想设计一个较为松耦合、可扩展的系统时即使没有接触过设计模式通过不断地尝试修改后也会得出一个逐渐符合了设计模式那样“味道”的代码架构思路。理解OIDC解决身份认证问题的思路也是同样的道理。
今天我们在OAuth2.0的基础上实现了一个OIDC的流程我希望你能记住以下两点。
1. **OAuth 2.0 不是一个身份认证协议**请一定要记住这点。身份认证强调的是“谁的问题”而OAuth2.0强调的是授权是“可不可以”的问题。但是我们可以在OAuth2.0的基础上通过增加ID令牌来获取用户的唯一标识从而就能够去实现一个身份认证协议。
2. 有些App不想非常麻烦地自己设计一套注册和登录认证流程就会寻求统一的解决方案然后势必会出现一个平台来收揽所有类似的认证登录场景。我们再反过来理解也是成立的。如果有个拥有海量用户的、大流量的访问平台来**提供一套统一的登录认证服务**让其他第三方应用来对接不就可以解决一个用户使用同一个账号来登录众多第三方App的问题了吗而OIDC就是这样的登录认证场景的开放解决方案。
说到这里你是不是对OIDC理解得更透彻了呢好了让我们看看今天我为了大家留了什么思考题吧。
## 思考题
如果你自己通过OAuth 2.0来实现一个类似OIDC的身份认证协议你觉得需要注意哪些事项呢
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。