文章目录
-
- 1. 项目概述
-
- 1.1 OAuth2 流程
- 2. OAuth 2.0 Storage接口解析
-
- 2.1 基础方法
- 2.2 客户端管理相关方法
- 2.3 授权码相关方法
- 2.4 访问令牌相关方法
- 2.5 刷新令牌相关方法
- 2.6 方法调用时序
- 2.7 关键注意点
- 3. MySQL存储实现原理
-
- 3.1 数据库设计
- 3.2 核心实现
- 4. OAuth 2.0授权码流程时序图
- 5. 使用示例
-
- 5.1 初始化存储
- 5.2 创建OAuth服务器
- 5.3 实现授权端点
- 5.4 实现客户端令牌端点
- 5.5 Callback回调断点(code换access_token)
- 完整流程
- 6. 总结
1. 项目概述
在上一篇文章中,我们详细介绍了OAuth 2.0的基本概念、授权流程以及各种授权模式的应用场景。本文将使用Go语言实现一个完整的OAuth 2.0认证服务器。
我们选择了github.com/openshift/osin这个成熟的OAuth 2.0框架作为基础,重点实现了其MySQL 来作为storage的驱动。osin提供了OAuth 2.0服务器的核心功能,但它的存储接口需要我们自己实现。通过实现MySQL存储,我们可以将OAuth 2.0的授权数据持久化到数据库中,使得服务更加可靠和可扩展。
本文的完整代码:oauth2
1.1 OAuth2 流程
让我们通过一个流程图来说明这些方法在 OAuth2 授权码模式中的位置:
2. OAuth 2.0 Storage接口解析
osin库中的Storage接口是实现OAuth 2.0服务器的核心,它定义了所有必要的存储操作。让我们详细解析每个方法在OAuth 2.0流程中的作用:
2.1 基础方法
Clone() Storage // 克隆存储实例,用于处理并发访问
Close() // 关闭存储连接,释放资源
2.2 客户端管理相关方法
GetClient(id string) (Client, error)
UpdateClient(c Client) error
CreateClient(c Client) error
RemoveClient(id string) error
这些方法负责OAuth客户端的CRUD操作:
- GetClient: 根据客户端ID获取客户端信息,用于验证客户端身份
- UpdateClient: 更新客户端信息
- CreateClient: 创建新的客户端
- RemoveClient: 删除指定客户端
2.3 授权码相关方法
SaveAuthorize(data *AuthorizeData) error
LoadAuthorize(code string) (*AuthorizeData, error)
RemoveAuthorize(code string) error
这些方法处理授权码授权流程:
- SaveAuthorize: 保存授权码信息
- LoadAuthorize: 验证授权码有效性
- RemoveAuthorize: 使用后删除授权码
这组方法用于处理授权码的生命周期:
2.4 访问令牌相关方法
SaveAccess(data *AccessData) error
LoadAccess(token string) (*AccessData, error)
RemoveAccess(token string) error
这些方法处理访问令牌的生命周期:
- SaveAccess: 保存访问令牌
- LoadAccess: 验证访问令牌
- RemoveAccess: 撤销访问令牌
访问令牌的生命周期管理:
2.5 刷新令牌相关方法
LoadRefresh(token string) (*AccessData, error)
RemoveRefresh(token string) error
这些方法处理刷新令牌:
- LoadRefresh: 加载刷新令牌对应的访问令牌数据
- RemoveRefresh: 删除刷新令牌
刷新令牌的处理流程:
2.6 方法调用时序
在完整的 OAuth2 流程中,这些方法的调用顺序如下:
2.7 关键注意点
原子性:
- SaveAuthorize 和 SaveAccess 操作需要保证原子性
- RemoveAuthorize 和 SaveAccess 通常需要在同一事务中执行
安全性:
- 所有存储的令牌数据应该加密
- 实现适当的过期机制
性能考虑:
- LoadAccess 方法会频繁调用,应考虑缓存
- Clone 方法对并发性能很重要
数据一致性:
- 确保授权码只能使用一次
- 正确处理令牌过期
- 维护刷新令牌与访问令牌的关联
3. MySQL存储实现原理
3.1 数据库设计
项目使用了两个主要的数据表:
CREATE TABLE client (
id varchar(255) NOT NULL PRIMARY KEY,
secret varchar(255) NOT NULL,
extra text,
redirect_uri varchar(255) NOT NULL,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
)
CREATE TABLE token (
id varchar(255) NOT NULL PRIMARY KEY,
client_id varchar(255) NOT NULL,
type varchar(20) NOT NULL,
access_token varchar(255),
refresh_token varchar(255),
code varchar(255),
expires_in int NOT NULL,
scope varchar(255),
redirect_uri varchar(255) NOT NULL,
state varchar(255),
extra text,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at timestamp NULL
)
3.2 核心实现
我们的MySQL存储实现主要包含以下特点:
4. OAuth 2.0授权码流程时序图
5. 使用示例
5.1 初始化存储
func initStorage(svcCtx *svc.ServiceContext) *service.Storage {
storage := service.NewStorage(svcCtx, "oauth2_")
err := storage.CreateSchemas()
if err != nil {
panic(err)
}
return storage
}
5.2 创建OAuth服务器
// newOAuthServer 创建一个新的OAuth服务器实例
func newOAuthServer(svc *svc.ServiceContext) *osin.Server {
config := osin.NewServerConfig()
config.AllowedAuthorizeTypes = osin.AllowedAuthorizeType{osin.CODE}
config.AllowedAccessTypes = osin.AllowedAccessType{
osin.AUTHORIZATION_CODE,
osin.REFRESH_TOKEN,
}
config.AuthorizationExpiration = 600 // 10分钟
config.AccessExpiration = 3600 // 1小时
config.AllowGetAccessRequest = true
config.ErrorStatusCode = 401
storage := service.NewStorage(svc, "osin_")
server := osin.NewServer(config, storage)
return server
}
5.3 实现授权端点
func AuthorizeHandler(svc *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
server := newOAuthServer(svc)
resp := server.NewResponse()
defer resp.Close()
if ar := server.HandleAuthorizeRequest(resp, r); ar != nil {
// 验证客户端
if ar.Client == nil {
resp.SetError("unauthorized_client", "客户端未授权")
osin.OutputJSON(resp, w, r)
return
}
// 验证重定向URI
if ar.RedirectUri == "" {
resp.SetError("invalid_request", "缺少重定向URI")
osin.OutputJSON(resp, w, r)
return
}
ar.Authorized = true
// 完成授权请求,这里只会返回授权码
server.FinishAuthorizeRequest(resp, r, ar)
// 如果没有错误,会重定向到客户端的redirect_uri,并带上授权码
if !resp.IsError {
resp.Type = osin.REDIRECT
}
}
// 输出响应(可能是重定向或错误信息)
osin.OutputJSON(resp, w, r)
}
}
5.4 实现客户端令牌端点
func TokenHandler(svc *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := logx.WithContext(r.Context())
server := newOAuthServer(svc)
resp := server.NewResponse()
defer resp.Close()
if ar := server.HandleAccessRequest(resp, r); ar != nil {
// 验证客户端
if ar.Client == nil {
resp.SetError("unauthorized_client", "客户端未授权")
osin.OutputJSON(resp, w, r)
return
}
// 授权请求
ar.Authorized = true
server.FinishAccessRequest(resp, r, ar)
}
if resp.IsError {
logger.Errorf("Token error: %v", resp.InternalError)
} else {
logger.Infof("Token granted: %s", resp.Output["access_token"])
}
osin.OutputJSON(resp, w, r)
}
}
5.5 Callback回调断点(code换access_token)
func CallbackHandler(svc *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 获取授权码
code := r.URL.Query().Get("code")
if code == "" {
// 检查是否有错误信息
if error := r.URL.Query().Get("error"); error != "" {
errorDesc := r.URL.Query().Get("error_description")
http.Error(w, fmt.Sprintf("授权失败: %s – %s", error, errorDesc), http.StatusBadRequest)
return
}
http.Error(w, "未收到授权码", http.StatusBadRequest)
return
}
// 初始化 OAuth 服务器
server := newOAuthServer(svc)
// 先加载授权数据
authData, err := server.Storage.LoadAuthorize(code)
if err != nil {
resp := server.NewResponse()
resp.SetError("invalid_grant", "授权码无效或已过期")
osin.OutputJSON(resp, w, r)
return
}
// 创建访问令牌请求
ar := &osin.AccessRequest{
Type: osin.AUTHORIZATION_CODE,
Code: code,
Client: authData.Client,
RedirectUri: authData.RedirectUri,
Scope: authData.Scope,
GenerateRefresh: true,
Authorized: true,
Expiration: server.Config.AccessExpiration,
}
// 处理访问令牌请求
resp := server.NewResponse()
defer resp.Close()
if err := server.Storage.RemoveAuthorize(code); err != nil {
resp.SetError("server_error", "无法删除授权码")
osin.OutputJSON(resp, w, r)
return
}
server.FinishAccessRequest(resp, r, ar)
if resp.IsError {
osin.OutputJSON(resp, w, r)
return
}
// API 请求则返回 JSON
osin.OutputJSON(resp, w, r)
}
}
完整流程
关键流程说明
- 客户端首先访问/oauth/authorize端点获取授权码
- 服务器生成授权码并保存到数据库
- 客户端带着授权码访问/oauth/callback端点
- CallbackHandler负责验证授权码并换取访问令牌
- 这一步通常在实际应用中是由前端页面完成的,但在我们的实现中直接由后端处理
- 验证授权码有效性
- 删除已使用的授权码(确保一次性使用)
- 生成访问令牌和刷新令牌
- 将令牌信息返回给客户端
6. 总结
本项目实现了一个完整的OAuth 2.0认证服务器,通过实现osin的Storage接口,提供了可靠的MySQL存储层。主要特点包括:
通过这个实现,我们可以快速搭建起一个生产级别的OAuth 2.0认证服务器,为各类应用提供标准的身份认证服务。
评论前必须登录!
注册