OAuth 介绍

介绍

OAuth 是一个关于授权(authorization)的开放网络标准。

引例

豪车一般配备两种钥匙:主钥匙和泊车钥匙。当你到酒店后,只需要将泊车钥匙交给服务生,停车的事情就由服务生去处理。与主钥匙相比,这种泊车钥匙的使用功能是受限制的:它只能启动发动机并让车行驶一段有限的距离,可以锁车,但无法打开后备箱,无法使用车内其他设备。这里就体现了一种简单的“开放授权”思想:通过一把泊车钥匙,车主便能将汽车的部分使用功能(如启动发动机、行驶一段有限的距离)授权给服务生。从而避免自己找停车位而耽误很多时间。

示例2,来自参考资料第一篇

有一个”云冲印”的网站,可以将用户储存在Google的照片,冲印出来。用户为了使用该服务,必须让”云冲印”读取自己储存在Google上的照片。问题是只有得到用户的授权,Google才会同意”云冲印”读取这些照片。那么,”云冲印”怎样获得用户的授权呢?传统方法是,用户将自己的Google用户名和密码,告诉”云冲印”,后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。

  • “云冲印”为了后续的服务,会保存用户的密码,这样很不安全。
  • Google不得不部署密码登录,而我们知道,单纯的密码登录并不安全。
  • “云冲印”拥有了获取用户储存在Google所有资料的权力,用户没法限制”云冲印”获得授权的范围和有效期。
  • 用户只有修改密码,才能收回赋予”云冲印”的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。
  • 要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。

OAuth就是为了解决上面这些问题而诞生的。

把密码换成令牌(access_token),避免了问题1。凭令牌来访问用户的照片,也可以给令牌不同的权限,解决了问题3的范围问题。令牌可以设定有效时间,解决了问题3的有效期。令牌可以主动使之无效,解决了问题4,不用改用户密码。因为第三方应用只是存的令牌,没有密码,问题5也是不存的。

当然这都是个简单的模型,实际情况比这复杂。

RFC就定义了这个规范。

OAuth 2.0 协议

协议的参与者

  • RO (resource owner): 资源所有者,对资源具有授权能力的人。
  • RS (resource server): 资源服务器,它存储资源,并处理对资源的访问请求。
  • Client: 第三方应用,它获得RO的授权后便可以去访问RO的资源。
  • AS (authorization server): 授权服务器,它认证RO的身份,为RO提供授权审批流程,并最终颁发授权令牌(Access Token)。为了便于协议的描述,这里只是在逻辑上把AS与RS区分开来;在物理上,AS与RS的功能可以由同一个服务器来提供服务。

授权类型

OAuth为了支持这些不同类型的第三方应用,提出了多种授权类型

  • 如授权码 (Authorization Code Grant)
  • 隐式授权 (Implicit Grant)
  • RO凭证授权 (Resource Owner Password Credentials Grant)
  • Client凭证授权 (Client Credentials Grant)。

流程图

  • (A)用户打开客户端以后,客户端要求用户给予授权。
  • (B)用户同意给予客户端授权。
  • (C)客户端使用上一步获得的授权,向认证服务器申请令牌。
  • (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
  • (E)客户端使用令牌,向资源服务器申请获取资源。
  • (F)资源服务器确认令牌无误,同意向客户端开放资源。

使用Google认证登录OAuth的示例

Client是自己的网站
RO 就是google账号
AS与RS就是google的登录服务器

操作步骤

0x01 开始之前

去创建一个 client ID,在https://developers.google.com/identity/sign-in/web/devconsole-project

0x02 引入google平台库

1
<script src="https://apis.google.com/js/platform.js" async defer></script>

0x03 指定应用的ClientID

1
<meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com">

0x04 增加一个Google登录的按钮

1
<div class="g-signin2" data-onsuccess="onSignIn"></div>

0x05 获取授权用户概要信息

1
2
3
4
5
6
7
function onSignIn(googleUser) {
var profile = googleUser.getBasicProfile();
console.log('ID: ' + profile.getId()); // Do not send to your backend! Use an ID token instead.
console.log('Name: ' + profile.getName());
console.log('Image URL: ' + profile.getImageUrl());
console.log('Email: ' + profile.getEmail()); // This is null if the 'email' scope is not present.
}

登出

1
2
3
4
5
6
7
8
9
<a href="#" onclick="signOut();">Sign out</a>
<script>
function signOut() {
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
console.log('User signed out.');
});
}
</script>

效果

在页面上会有一个登录按钮

点击之后,会弹出一个登录页面。
提示在第三方应用上登录(图上是fortest)。
登录成功之后,就通过js,把授权用户概要信息打印出来。
当然,这个oauth的过程都是封装到js代码中。

后端程序获取

需要前台把这个token传到后端
关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) throws Exception {
final Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 1080));
final HttpTransport transport= new NetHttpTransport.Builder().setProxy(proxy).build();
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport,JacksonFactory.getDefaultInstance())
.setAudience(Collections.singletonList(CLIENT_ID))
// Or, if multiple clients access the backend:
//.setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3))
.build();
GoogleIdToken idToken = verifier.verify(idTokenString);
if (idToken != null) {
Payload payload = idToken.getPayload();
System.out.println(payload);
// Print user identifier
String userId = payload.getSubject();
System.out.println("User ID: " + userId);

// Get profile information from payload
String email = payload.getEmail();
boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
String locale = (String) payload.get("locale");
String familyName = (String) payload.get("family_name");
String givenName = (String) payload.get("given_name");
// todo
} else {
System.out.println("Invalid ID token.");
}
}

当然要引用google oauth库

1
2
3
4
5
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-oauth2</artifactId>
<version>v2-rev131-1.23.0</version>
</dependency>

参考

相关扩展及走的弯路

这里记录的是在查看google oauth各功能时走的岔道。

0x01 Identity Toolkit for Websites

先是使用的这个组件,Quick-start App for Java
各种配置,生成了一个oauth的clienid,还生了个服务账号的密钥。
样例代码很简单也有效,但没有报错,也没有继续,也不输出期望的信息。可能哪里没有配置对。

这个组件集成了yahoo,G+,facebook,paypal,mircrosof的第三方登录。ios,andorid,web的库完善。

0x02 google 官方示例

https://github.com/google/google-oauth-java-client.git
java 官方示例,先不能跑起来,因为LocalServerReceiver的实现端口总是变。

0x03 spring-boot 写的OAuth样例

Google OAuth 2.0 Sample Application

部署到服务器上,可以验证成功。但没有输出用户信息,也不能退出。也算是失败。容错上也不好。
在本地运行会超时错误。因为是spring-boot内部也是不好设置代理。

0x04 oauth2-cmdline-sample

https://github.com/google/google-api-java-client-samples.git
官方示例其中的oauth2-cmdline-sample,配置好密钥后,不是跑不起来。
查到LocalServerReceiver的实现端口总是变,而凭据的授权要指定url不能这样变,不然就出现redirect_uri_mismatch错误。自己写的一个端口的实现类之后呢,又一直在authorize方法止住。发现卡在TokenResponse response = flow.newTokenRequest(code).setRedirectUri(redirectUri).execute();这个方法,无法访问服务google资源服务器的原因。然后修改了HttpTransport的默认对像HttpTransport newhttpTransport = new NetHttpTransport.Builder().setProxy(proxy).build();。又卡在了tokenInfo的Tokeninfo tokeninfo = oauth2.tokeninfo().setAccessToken(accessToken).execute();猜测也是网络问题,也没法重设httptransport。要本地电脑挂vpn,就没继续下去。
这个示例代码算是完整了用代码方式讲述google oauth的一般流程。