1、OAuth2解决什么问题
1.1 bilibili故事
https://www.bilibili.com/
以上,打开尚未登录的哔哩哔哩,选择登录,会看到其他登录方式有微信、微博以及QQ。
举例微博
来说,这里,就是bilibili
想要使用作为用户的“你”
在微博的账号信息,简单地,你可以把微博的账户名、密码告诉B站,B站登录微博,就可以获取到它想要的信息。
但是这种方式存在一些问题:
- 信任问题;通过账户名+密码,B站可以拿到你微博账号的所有权限,万一给你发了条不可描述的微博咋办
- 取消授权;如果想要不让B站继续访问你的微博账号信息,唯一可以做的就是修改密码了
OAuth2就是解决以上的问题,它能保证B站能够得到“账号信息”的权限,而又不能发微博,在你想要取消授权的时候又不用大动干戈修改密码。
1.2 下个定义
OAuth(Open Authorization,开放授权)是一种发布和与受保护数据交互的简单方式。它是互联网中基于令牌的身份验证和授权的开放标准,它允许第三方服务(例如bilibili)使用用户的账户信息,而不用暴露用户密码。OAuth规范描述了用于获取访问令牌的5种授权方式:
- 授权码授权
- 隐式授权
- 资源所有者凭据授权;用户的用户名+密码,就是一种资源所有者凭据
- 客户端凭据授权
- 刷新令牌授权
简单来说,OAuth规范中,首先需要获取访问令牌,后续对用户资源的访问,都是需要将访问令牌带上的。
2、授权码授权模式
OAuth2.0规范提供的5中获取访问令牌的方式中,授权码授权是最为安全、且广泛使用的。在正式介绍授权码授权之前,首先声明几个概念,如上Bilibili的故事中:
- Resource Owner 资源所有者;“你”,想要登录bilibili的你,拥有微博账号信息所有权的你
- Client Application 客户端应用;bilibili,它想要使用resource owner的某些资源
- Resource Server 资源服务器;微博,承载resource owner资源的网络服务器
- Authorization Server 授权服务器;微博,负责验证用户身份,然后向客户端应用颁发访问令牌的网络服务器
2.1 bilibili故事续
回到bilibili的登录故事,选择“微博登录”,这时我们可以看到以下页面
扫码登录微博,同意授权
同意授权后自动跳转
因为尚未登录微博,所以需要先登录微博,整个流程是
- 微博进行用户身份验证,确认资源所有者的身份
- 微博进行授权,让资源所有者选择授予的权限,这里的权限有个人信息、分享、联系邮箱
- 通过微博授权服务器的重定向,bilibili得到了授权码
- bilibili通过授权码请求微博,得到访问令牌(acess token),这一步是在后台进行的,前端页面不可见
这是一个标准的OAuth2.0 授权码授权流程:
有必要对Client Application做一下单独说明;授权之前,client application需要到resource server关联的 authorization server注册一次,authorization server会为client application分配一个client id以及client secret(密码)。
2.2 授权码授权流程
通过授权码方式获取访问令牌,总体来说分为两步,一是获取授权码,而是授权码换取访问令牌。结合bilibili请求微博授权的实例说明。更多详细参考规范 https://www.tech-invite.com/y65/tinv-ietf-rfc-6749-2.html#e-4-1
2.2.1 授权请求
对应以上流程第2步。授权请求是客户端应用发送给授权服务器,获取授权码的过程。
// bilibili发给微博的授权请求是这样的
https://api.weibo.com/oauth2/authorize?
client_id=2841902482&
redirect_uri=https%3A%2F%2Fpassport.bilibili.com%2Flogin%2Fsnsback%3Fsns%3Dweibo%26state%3Ddaee0470162b11edbd071613e9886b23%26source%3Dnew_main_mini&
scope=email###
// OAuth2 rfc 规范中的授权请求
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
这些参数的意义是,
- host https://api.weibo.com/oauth2/authorize; 是微博Authorizaiton server地址
- 参数 client_id = 2841902482; 客户端应用bilibili事先在微博注册后得到的,用于标识bilibili,另外还有一个client_secret
- 参数 redirect_uri = https://passport.bilibili.com/login/snsback?sns=weibo&state=d484e480160511edbbf7ca276e45c3c0&source=new_main_mini; 这里经过了url decode,微博的授权服务器会使用这个地址,回调bilibili以传递授权码
- 参数 scope=email; 客户端应用申请的权限范围
- response_type; [规范中],值固定为code,标示授权码模式
可以看到微博的授权方式,与标准的OAuth2.0有一定有差异,但是大体上流程是按照流程来的。
2.2.2 授权响应(重定向)
对应以上流程第5步。怎么就直接从第2步到了第5步,步骤3、4呢?其实3、4步骤产生的结果是资源拥有者同意授权,这个过程不涉及客户端应用,完全是授权服务器内部的逻辑,所以在OAuth2.0规范中不涉及。
在bilibili的故事中,如果用户事先登录了微博,那么bilibili请求授权的时候就不会触发扫码登录,而是直接展示bilibili要求授权的页面,让用户同意;同样,身份认证方式也可以不是扫码,而是用户名密码登录。总之,这都是授权服务器的内部事宜,产出结果是资源拥有者同意/不同意授权。
规范中,如果资源拥有者同意了授权,授权服务器的响应是
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
注意到这是一个重定向的响应(http status 302)。user-agent(通常是浏览器)获得该响应后,会重定向访问地址,规范中在重定向地址中附加了参数 code
,这就是授权码,后续客户端应用可以通过code换取访问令牌。
规范中,若资源拥有者不同意授权,会进行一些说明,如下
HTTP/1.1 302 Found
Location: https://client.example.com/cb?error=access_denied&state=xyz
具体参考rfc规范,实现上也不是必须的。
微博的授权响应是,可以看到与规范差异比较大,关注snsAcessToken
,嗯?好像是用隐式授权,访问令牌直接暴露了,不怀好意的第三方可以通过这个访问令牌,访问用户在微博的一些资源了,哔哩哔哩的故事到此结束。
https://passport.bilibili.com/register/snsback.html?
sns=weibo&
snsUid=7780546299&
snsAccessToken=2.00PW4YUIoe11GD940dc91c8apEMysD&
snsAccessExpires=2644318&
csrf=21e50500160811eda02a22f04cb09b9e&
source=new_main_mini&
gourl=https%3A%2F%2Fwww.bilibili.com%2F#/
2.2.3 访问令牌请求
客户端应用使用授权码,请求授权服务器获取访问令牌。
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
可以看到,最重要的参数就是授权码code。成功的响应试是这样的:
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"
}
其中refresh token
可以用于在acess token
过期后,直接向授权服务器获取新的访问令牌,而不用资源拥有者参与。
3、sa-token实践OAuth2.0授权码
sa-token
是一个国产的轻量级Java权限认证框架,官方文档地址是: https://sa-token.dev33.cn/doc/index.html#/
根据官网的指引,搭建一个OAuth2.0授权服务及其简单,直接参考官方指引即可: https://sa-token.dev33.cn/doc/index.html#/oauth2/oauth2-server
最后可以下载官方的完整示例
但是目前的版本(v1.30.0
)中并没有直接介绍资源服务器侧的相关信息,即资源服务器如何根据访问令牌来做对应的访问权限控制。
权限控制这一块sa-token已经提供了基础的信息以及API:
- acess-token对应的scope,即权限,可以用一些逗号分隔的字符串来抽象表示一些权限,比如示例中的
userinfo
,可以抽象为用户信息访问权限 - 权限验证的API, 如
cn.dev33.satoken.oauth2.logic.SaOAuth2Util#checkScope(accessToken, scpes)
,接受acess-token和需要校验的权限列表,检查acess-token是否具备这些权限 - 构建
url<->scope
权限模型,访问哪个url,必须具备什么scope;可以通过servlet filter技术校验这个模型。
3.1 实践资源服务器
访问受保护资源时,规定通过请求头X-ACCESS-TOKEN
传递授权获得访问令牌,通过以上分析,定义一个Filter,在filter中校验请求携带的访问令牌,是否具备要求的scope权限。
部分代码如下:
private void accessTokenPermissionVerify(SaRequest request) {
if (SaRouter.isMatchCurrURI("/info/**")) {
// 获取请求头中的 access-token
String accessToken = request.getHeader(AC_TOKEN_HEADER);
if (StringUtils.isEmpty(accessToken)) {
throw new NotPermissionException("未通过授权");
}
// 验证 access-token 是否有效
AccessTokenModel acTokenEntity = SaOAuth2Util.getAccessToken(accessToken);
if (null == acTokenEntity) {
throw new NotPermissionException("未通过授权");
}
SaOAuth2Util.checkScope(accessToken, "userinfo");
}
}
这里构建的权限模型是,访问url/info/**
代表的资源,需要访问令牌,且具备scopeuserinfo
权限。可以看到这里将url
抽象为资源,匹配scope,并不能做到数据级别的权限控制,如用户A不能访问用户B的userinfo
数据。数据权限是更加复杂的课题,已经不在OAuth2的范围内了。
资源服务器的搭建完整示例: https://gitee.com/Z-A/sa-token-all
重点关注类com.example.authserver.config.SaTokenConfigure
filter对于权限模型的定义以及校验方法。
效果展示
胡乱填写的acess-token or 无权限的acess-token
GET http://localhost:8001/info/student?id=1
Accept: */*
X-ACCESS-TOKEN: ABG2ImmKfCKQXiIYlgtY9rotc2WDLojvfhzVY860aQsJk77j9GM9BKl6CtJm
###响应
{
"code": 500,
"msg": "无此权限:未通过授权",
"data": null
}
有效&有权限的acess-token
GET http://localhost:8001/info/student?id=1
Accept: */*
X-ACCESS-TOKEN: ABG2ImmKfCKQXiIYlgtY9rotc2WDLojvfhzVY860aQsJk77j9GM9BKl6CtJm
###响应
{
"id": "1",
"name": "张三",
"gender": "man",
"age": 18,
"mail": "zhangsan@qq.com"
}