- OAuth 协议为用户资源的授权提供了一个安全又简易的标准。与以往的授权方式不同之处是 OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth是安全的。OAuth 是 Open Authorization 的简写
一、oAuth协议基本概念
1.从第三方登录说起
第三方登录是应用开发中的常用功能,通过第三方登录,我们可以更加容易的吸引用户来到我们的应用中。现在,很多网站都提供了第三方登录的功能,在他们的官网中,都提供了如何接入第三方登录的文档。但是,不同的网站文档差别极大,各种第三方文档也是千奇百怪,同时,很多网站提供的SDK用法也是各不相同。对于不了解第三方登录的新手来说,实现一个支持多网站第三方登录的功能可以说是极其痛苦。实际上,大多数网站提供的第三方登录都遵循OAuth协议,虽然大多数网站的细节处理都是不一致的,甚至会基于OAuth协议进行扩展,但大体上其流程是一定的。
- OAuth解决的问题:
任何身份认证,本质上都是基于对请求方的不信任所产生的。同时,请求方是信任被请求方的,例如用户请求服务时,会信任服务方。所以,身份认证就是为了解决身份的可信任问题。
简单的说就是:
(1)服务方不信任用户,所以需要用户提供密码或其他可信凭据
(2)服务方不信任第三方应用,所以需要第三方提供自已交给它的凭据(如微信授权的code,AppID等)
(3)用户部分信任第三方应用,所以用户愿意把自已在服务方里的某些服务交给第三方使用,但不愿意把自已在服务方的密码等交给第三方应用
2.oAuth基本概念及实现思路
- OAuth的作用就是让"客户端"安全可控地获取"用户"的授权,与"服务商提供商"进行互动。下面先熟悉几个概念:
(1)Third-party application:第三方应用程序,本文中又称"客户端"(client)
(2)HTTP service:HTTP服务提供商,本文中简称"服务提供商
(3)Resource Owner:资源所有者,本文中又称"用户"(user)。
(4)User Agent:用户代理,本文中就是指浏览器。
(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器
(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
- OAuth的实现思路:
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
3.OAuth基本流程
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。(关键步骤)
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
4.客户端的授权模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
(1)授权码模式(authorization code)
-
通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
- A步骤中,客户端申请认证的URI,包含以下参数:
例如:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
* response_type:表示授权类型,必选项,此处的值固定为"code"
* client_id:表示客户端的ID,必选项
* redirect_uri:表示重定向URI,可选项
* scope:表示申请的权限范围,可选项
* state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值
- C步骤中,服务器回应客户端的URI,包含以下参数:
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA &state=xyz
* code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次
,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
* state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
- D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
* grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"
* code:表示上一步获得的授权码,必选项。
* redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
* client_id:表示客户端ID,必选项。
- E步骤中,认证服务器发送的HTTP回复,包含以下参数:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
*access_token:表示访问令牌,必选项。
* token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
* expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
* refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项
* scope:表示权限范围,如果与客户端申请的范围一致,此项可省略
(2)密码模式
-
用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
- B步骤中,客户端发出的HTTP请求,包含以下参数:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=johndoe&password=A3ddj3w
* grant_type:表示授权类型,此处的值固定为"password",必选项。
* username:表示用户名,必选项。
* password:表示用户的密码,必选项。
* scope:表示权限范围,可选项。
- C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
(3)客户端模式
-
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
- A步骤中,客户端发出的HTTP请求,包含以下参数:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
* granttype:表示授权类型,此处的值固定为"clientcredentials",必选项。
* scope:表示权限范围,可选项。
- B步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"example_parameter":"example_value"
}
5.更新令牌
- 如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。客户端发出更新令牌的HTTP请求,包含以下参数:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
* granttype:表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
* refresh_token:表示早前收到的更新令牌,必选项。
* scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。
二、案例说明
- OAuth简单说就是一种授权的协议,只要授权方和被授权方遵守这个协议去写代码提供服务,那双方就是实现了OAuth模式。
1.场景案例
- 简单来说,下面例子中的豆瓣就是客户端,QQ就是认证服务器,OAuth2.0就是客户端和认证服务器之间由于相互不信任而产生的一个授权协议。
(1)在豆瓣官网点击用qq登录
这一步实际发生的情况是:当你点击用qq登录的小图标时,实际上是向豆瓣的服务器发起了一个 http://www.douban.com/leadToAuthorize 的请求,豆瓣服务器会响应一个重定向地址,指向qq授权登录。浏览器接到重定向地址 http://www.qq.com/authorize?callback=www.douban.com/callback ,再次访问。并注意到这次访问带了一个参数是callback,以便qq那边授权成功再次让浏览器发起这个callback请求。不然qq怎么知道你让我授权后要返回那个页面啊,每天让我授权的像豆瓣这样的网站这么多。
(2)跳转到qq登录页面输入用户名密码,然后点授权并登录
这一步实际过程如下如所示:qq的服务器接受到了豆瓣访问的authorize,在次例中所给出的回应是跳转到qq的登录页面,用户输入账号密码点击授权并登录按钮后,一定还会访问qq服务器中校验用户名密码的方法,若校验成功,该方法会响应浏览器一个重定向地址,并附上一个code(授权码)。由于豆瓣只关心像qq发起authorize请求后会返回一个code,并不关心qq是如何校验用户的,并且这个过程每个授权服务器可能会做些个性化的处理,只要最终的结果是返回给浏览器一个重定向并附上code即可,所以这个过程在图中并没有详细展开
(3)跳回到豆瓣页面,成功登录
这一步实际过程如下:首先接上一步,QQ服务器在判断登录成功后,使页面重定向到之前豆瓣发来的callback并附上code授权码,即callback= www.douban.com/callback 页面接到重定向,发起 http://www.douban.com/callback 请求豆瓣服务器收到请求后,做了两件再次与QQ沟通的事,即模拟浏览器发起了两次请求。一个是用拿到的code去换token,另一个就是用拿到的token换取用户信息。最后将用户信息储存起来,返回给浏览器其首页的视图。到此OAuth2.0授权结束。
2、场景案例代码实现
(1)豆瓣服务器代码(端口7001)
@Controller
public class DoubanOAuthController {
/**
* 0、用户点击豆瓣客户端上的qq登录时候,重定向到qq的登录页面
*/
@RequestMapping("leadToAuthorize")
public void leadToAuthorize(HttpServletResponse response) throws Exception {
response.sendRedirect("http://localhost:7000/authorize?" +
"response_type=code&" +
"client_id=10000032&" +
"redirect_uri=http://localhost:7001/index&" +
"scope=userinfo&" +
"state=hehe");
}
/**
*3、qq重定向到豆瓣,传递过来code等信息,
*客户端收到授权码,附上早先的"重定向URI"(QQ api),向认证服务器申请令牌。
*这一步是在客户端的后台的服务器上完成的,对用户不可见。
*/
@RequestMapping("index")
public String index(String code, HttpServletRequest request) throws Exception {
RestTemplate restTemplate = new RestTemplateBuilder().build();
//向qq获取token
String accessToken = restTemplate.getForObject("http://localhost:7000/getTokenByCode?" +
"grant_type=authorization_code&" +
"code=xxx&" +
"redirect_uri=http://localhost:7001/index", String.class);
//发起通过token换用户信息的请求
String username = restTemplate.getForObject("http://localhost:7000/getUserinfoByToken?" +
"access_token=yyy", String.class);
request.getSession().setAttribute("username",username);
return "index";
}
}
(2)qq服务器代码(端口7000)
@Controller
public class OAuthController {
/**
* 1、用户访问豆瓣,豆瓣重定向到qq服务器,qq然后重定向到qq登录页面
*/
@RequestMapping("authorize")
public String authorize(String response_type, String client_id, String redirect_uri, String scope, String state){
return "login";
}
/**
* 2、假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
*/
@RequestMapping("login")
public void login(String username, String password, HttpServletResponse response) throws IOException {
// 验证用户名密码是否正确
//http://localhost:7001/index是豆瓣事先指定的重定向uri
response.sendRedirect("http://localhost:7001/index?code=xxx&state=hehe");
}
/**
* 4、认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
*/
@RequestMapping("getTokenByCode")
@ResponseBody
public String getTokenByCode(String grant_type, String code, String client_id, String redirect_uri) throws IOException {
// 判断client_id、redirect_uri、code是否正确
// 返回token
return "{access_token:yyy," +
"token_type:bearer," +
"expires_in:600," +
"refresh_token:zzz}";
}
/**
* 5、资源服务器确认令牌无误,同意向客户端开放资源。。
*/
@RequestMapping("getUserinfoByToken")
@ResponseBody
public String getUserinfoByToken(String token) throws IOException {
// 判断token是否正确
return "{userGuid:j3jlk2jj32li43i," +
"username:Tom," +
"mobile:18811412324}";
}
}
参考文章:
阮一峰---理解OAuth 2.0
一张图搞定OAuth2.0