Goalng实现oauth2.0认证授权
oauth2.0介绍:
这里不再多说给出参考链接 :
总的来说,OAuth 不是一个API或者服务,而是一个验证授权(Authorization)的开放标准,所有人都有基于这个标准实现自己的OAuth。
在OAuth之前,HTTP Basic Authentication, 即用户输入用户名,密码的形式进行验证, 这种形式是不安全的。OAuth的出现就是为了解决访问资源的安全性以及灵活性。OAuth使得第三方应用对资源的访问更加安全。
oauth2.0 有四种模式分别如下:
- 授权码(authorization-code)
- 隐藏式(implicit)
- 密码式(password):
- 客户端凭证(client credentials)
本文主要写最难理解的第一种模式: 授权码模式。
oauth2.0 之授权码模式:
标准的Server授权模式,非常适合Server端的Web应用。一旦资源的拥有者授权访问他们的数据之后,他们将会被重定向到Web应用并在URL的查询参数中附带一个授权码(code)。
在客户端里,该code用于请求访问令牌(access_token)。并且该令牌交换的过程是两个服务端之前完成的,防止其他人甚至是资源拥有者本人得到该令牌。
另外,在该授权模式下可以通过refresh_token来刷新令牌以延长访问授权时间。
先介绍下四个基本名字:
- 资源拥有者(resource owner): 通常就是用户自己,能授权访问受保护资源的一个实体,如QQ用户注册了QQ拥有对应实际账号数据资源
- 资源服务器(resource server): 存储受保护资源,客户端通过access token请求资源,资源服务器响应受保护资源给客户端;其实就是QQ服务器,它存储着你的QQ账号数据。
- 授权服务器(authorization server): 验证资源拥有者并获取授权之后,授权服务器颁发授权令牌(Access Token)给客户端,一般来说授权认证服务器与资源服务器都是一个内部体系的,不然它凭什么给你一个access token,你就能访问对应资源了。
- 客户端(client):一些第三方应用,其本身不存储资源,而是资源拥有者授权通过后,使用它的授权(授权令牌)访问受保护资源,然后客户端把相应的数据展示出来。“客户端”术语不代表任何特定实现(如应用运行在一台服务器、桌面、手机或其他设备)。 比如你的网站想接入QQ或则微信直接登录你的平台。
demo 时序图如下:
- 用户浏览器,对应的用户其实自身就 resource owner,用于QQ账号
- 豆瓣网站,对应 client,它接入了QQ登录的方式
- QQ授权服务:对应 authorization server, 下图也包括内部的 resource server。
梳理下每一步必须要参数:
- 首先你得拥有属于自己应用对应的 appkey、appsecret 分别对应 OAuth2.0 中的 client_id 与client_secret,可能一些服务端会扩展一些其他的名称。并设置好你自己的 redirect_uri 跳转回调地址,用于通知给你code码,还可以设置好 state附带返回参数,scope授权范围.
- 当你拿到code后,就可以换取access_token了。一般都必须返回至少三个参数:refresh_token、access_token、expires_in
- 三方应用拿到access_token后就可以请求资源服务器获取用户数据了,如果超时则用refresh_token进行重刷access_token。
参数列表:
- response_type:表示授权类型,必选项,此处的值固定为”code”
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围如 user,order,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
- code:表示授权码,必选项。通常授权码只能使用该码一次,并且会设置有效时间。
- grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code”。
- client_secret:表示客户端密钥,必选项。
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
golang 实现 oauth2.0 代码实战:
这里直接使用开源三方 oauth2.0 库:
GitHub - openshift/osin: Golang OAuth2 server library
谈谈该库:
优点:
- 实现代码简单易读,实现方式可借鉴学习,典型的插件化调用。
- 代码test和demo较为完整
- storage 存储只要自己实现好interface 就能自动存入对应表数据
- 只有三张表数据结构基本满足,其中一张为临时code对应access_token表。
缺点:
- 检验机制较为单薄,如 Authorization header拼接参数的检验
- 没有refresh_token的超时设计
- 致命问题是返回的提示语没有code编码提示,很多错误提示都得自己看源码调试才知道原因,而且返回的错误提示大体长的都差不多。
- 另外一个问题,请求参数输入参数没有具体文档,得自己看源码研究,参数一旦错误没有明确提示得自己debug调试,这个把我坑惨了,不过还好源码简单。。。
具体show code:
除开一些获取配置和前端联动的接口外,这里只设计了三个主要接口:
- 根据client_id、redirect_uri、scope、state获取跳转地址和code码;
func (o *OauthController) GetCodeByAppID(ctx *gin.Context) {
// 检验参数、系统内部授权jwt验证等...
// 开始调用三方库方法:
serverCf := osin.NewServerConfig()
// NewOauthStorage 返回的strcut需要你自行实行对应的interface接口
server := osin.NewServer(serverCf, service.NewOauthStorage(ctx))
// 注入你自己的Log,这样服务端才能打印对应你的日志格式
server.Logger = log.G(ctx)
resp := server.NewResponse()
defer resp.Close()
// HandleAuthorizeRequest 会进行各种参数验证和类型逻辑判断
if ar := server.HandleAuthorizeRequest(resp, ctx.Request); ar != nil {
ar.Authorized = true
// 这里需要注入你的用户相关数据,便于框架存入表中
// UserData 是个 interface类型,需要你自己定义用户数据 strcut
ar.UserData = model.UserData{
ID: sess.UID,
Account: sess.Account,
AccountDB: sess.AccountDB,
}
// FinishAuthorizeRequest 执行数据生产如token产生和赋值
server.FinishAuthorizeRequest(resp, ctx.Request, ar)
}
if resp.IsError && resp.InternalError != nil {
response.RespError(ctx, resp.InternalError)
return
}
// 最终输出前端 或 302跳转
err = osin.OutputJSON(resp, ctx.Writer, ctx.Request)
if err != nil {
response.RespError(ctx, resp.InternalError)
return
}
}
- 根据 code、state 换取 access_token
这个接口入参如下:
Authorization header 认证授权:Basic空格{加密字段f} string 是
grant_type formData 授权类型 固定填写 "authorization_code" string 是
code formData code编码 string 是
state formData state附带参数 string no
加密字段f = {client}:{secret} ==> base64 encode
golang demo:
auth := "1234" + ":" + "abcd"
base64.StdEncoding.EncodeToString([]byte(auth))
func (o *OauthController) GetAccessToken(ctx *gin.Context) {
// 获取参数检验...
// 这里建议自己实现一次 参数验证,固定code返回格式便于错误定位。
// 这里只要 接口1调通并且拿到正确的code,这里框架直接应用,会自动就生成返回数据:
serverCf := osin.NewServerConfig()
var server = osin.NewServer(serverCf, service.NewOauthStorage(ctx))
resp := server.NewResponse()
defer resp.Close()
if ar := server.HandleAccessRequest(resp, ctx.Request); ar != nil {
ar.Authorized = true
server.FinishAccessRequest(resp, ctx.Request, ar)
}
err := osin.OutputJSON(resp, ctx.Writer, ctx.Request)
if err != nil {
response.RespError(ctx, resp.InternalError)
return
}
return
}
该接口返回格式如下:
{
"access_token": "SKb9y_ZkQrmGg9UXJHIldA",
"expires_in": 3600,
"refresh_token": "YfoYnjYmS3a-jpmKezSWbQ",
"token_type": "Bearer"
}
- 根据refresh_token 重新刷新access_token
入参:
Authorization header 认证授权:Basic空格{加密字段f} string 是
grant_type formData 授权类型 固定填写 "refresh_token" string 是
refresh_token formData refresh_token string 是
state formData state附带参数 string no
func (o *OauthController) GetRefreshAccessToken(ctx *gin.Context) {
// 参数检验
// 正式授权换token
serverCf := osin.NewServerConfig()
// 注意这里 AllowedAccessTypes : REFRESH_TOKEN 需要加载
serverCf.AllowedAccessTypes = osin.AllowedAccessType{osin.REFRESH_TOKEN}
server := osin.NewServer(serverCf, storageDao)
server.Logger = log.G(ctx)
resp := server.NewResponse()
defer resp.Close()
if ar := server.HandleAccessRequest(resp, ctx.Request); ar != nil {
ar.Authorized = true
server.FinishAccessRequest(resp, ctx.Request, ar)
}
err = osin.OutputJSON(resp, ctx.Writer, ctx.Request)
if err != nil {
response.RespError(ctx, resp.InternalError)
return
}
return
}
依旧返回:
{
"access_token": "SKb9y_ZkQrmGg9UXJHIldA",
"expires_in": 3600,
"refresh_token": "YfoYnjYmS3a-jpmKezSWbQ",
"token_type": "Bearer"
}
主题流程就走通了,其余就是自己根据业务系统进行调整即可。