当前位置: 首页 > news >正文

Go语言实现OAuth 2.0认证服务器

文章目录

    • 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) errorCreateClient(c Client) errorRemoveClient(id string) error

这些方法负责OAuth客户端的CRUD操作:

  • GetClient: 根据客户端ID获取客户端信息,用于验证客户端身份
  • UpdateClient: 更新客户端信息
  • CreateClient: 创建新的客户端
  • RemoveClient: 删除指定客户端

2.3 授权码相关方法

   SaveAuthorize(data *AuthorizeData) errorLoadAuthorize(code string) (*AuthorizeData, error)RemoveAuthorize(code string) error

这些方法处理授权码授权流程:

  • SaveAuthorize: 保存授权码信息
  • LoadAuthorize: 验证授权码有效性
  • RemoveAuthorize: 使用后删除授权码

这组方法用于处理授权码的生命周期:

2.4 访问令牌相关方法

   SaveAccess(data *AccessData) errorLoadAccess(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 关键注意点

  1. 原子性

    • SaveAuthorize 和 SaveAccess 操作需要保证原子性
    • RemoveAuthorize 和 SaveAccess 通常需要在同一事务中执行
  2. 安全性

    • 所有存储的令牌数据应该加密
    • 实现适当的过期机制
  3. 性能考虑

    • LoadAccess 方法会频繁调用,应考虑缓存
    • Clone 方法对并发性能很重要
  4. 数据一致性

    • 确保授权码只能使用一次
    • 正确处理令牌过期
    • 维护刷新令牌与访问令牌的关联

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存储实现主要包含以下特点:

  1. 使用go-zero框架的sqlx包进行数据库操作
  2. 实现了完整的事务支持
  3. 支持令牌过期检查
  4. 提供了表前缀支持,便于多租户场景

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 = trueconfig.ErrorStatusCode = 401storage := 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}// 验证重定向URIif 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 = trueserver.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 请求则返回 JSONosin.OutputJSON(resp, w, r)}
}

完整流程

在这里插入图片描述

关键流程说明

  1. 授权码获取:
    • 客户端首先访问/oauth/authorize端点获取授权码
    • 服务器生成授权码并保存到数据库
  2. 授权码换取令牌:
    • 客户端带着授权码访问/oauth/callback端点
    • CallbackHandler负责验证授权码并换取访问令牌
    • 这一步通常在实际应用中是由前端页面完成的,但在我们的实现中直接由后端处理
  3. 令牌生成流程:
    • 验证授权码有效性
    • 删除已使用的授权码(确保一次性使用)
    • 生成访问令牌和刷新令牌
    • 将令牌信息返回给客户端

6. 总结

本项目实现了一个完整的OAuth 2.0认证服务器,通过实现osin的Storage接口,提供了可靠的MySQL存储层。主要特点包括:

  1. 完整实现OAuth 2.0规范
  2. 可靠的MySQL存储实现
  3. 支持授权码和刷新令牌流程
  4. 完善的错误处理和安全机制
  5. 易于扩展和定制

通过这个实现,我们可以快速搭建起一个生产级别的OAuth 2.0认证服务器,为各类应用提供标准的身份认证服务。


文章转载自:

http://cNWCn6C1.zqfks.cn
http://iilXLvec.zqfks.cn
http://8lAA3LAt.zqfks.cn
http://H4pxc9pB.zqfks.cn
http://KUBcnnaN.zqfks.cn
http://aGKv5D9n.zqfks.cn
http://m85Vv56l.zqfks.cn
http://3OOLGRIG.zqfks.cn
http://TM83cqne.zqfks.cn
http://JM1oqFrU.zqfks.cn
http://pYaE2dkN.zqfks.cn
http://BA7KxqgN.zqfks.cn
http://U0PD0R5p.zqfks.cn
http://K5VH1CU9.zqfks.cn
http://XqMd9lOt.zqfks.cn
http://mccu4PdS.zqfks.cn
http://HtiZ6ilj.zqfks.cn
http://FPk7ewZI.zqfks.cn
http://C3B3CsYE.zqfks.cn
http://34hcGSJU.zqfks.cn
http://kmhkKcOW.zqfks.cn
http://GozeDjSt.zqfks.cn
http://u6slWNIb.zqfks.cn
http://g365DoCB.zqfks.cn
http://BJ0xYS52.zqfks.cn
http://GYEmZi4F.zqfks.cn
http://VGjBswxN.zqfks.cn
http://OpYdiNGc.zqfks.cn
http://zvh0IhdT.zqfks.cn
http://nnUsPFeH.zqfks.cn
http://www.dtcms.com/a/136781.html

相关文章:

  • 独家!美团2025校招大数据题库
  • 鸿蒙开发之嵌套对象更新
  • FPGA_YOLO(四)用HLS实现循环展开以及存储模块
  • 【WPF-VisionMaster源代码】应用OpenCVSharp仿Vision Master页面开发的软件源代码
  • C++学习之游戏服务器开发git命令
  • [MERN] 项目实战】MERN Multi-Vendor 电商平台开发笔记(v1.0 初版结构 + 技术实践)
  • 树莓派超全系列教程文档--(28)boot文件夹内容
  • Ngrok 内网穿透实现Django+Vue部署
  • vscode连接windows服务器出现过程试图写入的管道不存在
  • AIGC-十款数据分析类智能体完整指令直接用(DeepSeek,豆包,千问,Kimi,GPT)
  • 【STM32-代码】
  • C#: 用Libreoffice实现Word文件转PDF
  • 磁芯为什么会有磁性?磁性材料的磁滞曲线还记得吗?
  • Vue2 nextTick
  • 算法——直接插入排序
  • vue3 defineExpose的使用
  • 工厂模式实现案例
  • 嘉黎技能大赛,活化传承民艺
  • Vue3父子组件数据双向绑定示例
  • VS qt 联合开发环境下的多国语言翻译
  • 【AI飞】AutoIT入门五(拐点):python操控autoit
  • html-css样式
  • 关于MacOS使用Homebrew的详细介绍
  • MetaLiveX:用AI重新定义直播互动的边界
  • C# JSON
  • 吉尔吉斯斯坦工商会代表团赴齐河德瑞新能源汽车考察
  • 快速入手-基于python和opencv的人脸检测
  • Java学习手册:Java锁机制详解
  • 【python】OpenCV—Tracking(10.6)—People Counting
  • Paimon的InternalRow 解析(一)