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

news2025/4/19 13:45:49

文章目录

    • 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 关键注意点

  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 = 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)
	}
}

完整流程

在这里插入图片描述

关键流程说明

  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://www.coloradmin.cn/o/2336893.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【2025年认证杯数学中国数学建模网络挑战赛】C题 数据预处理与问题一二求解

目录 【2025年认证杯数学建模挑战赛】C题数据预处理与问题一求解三、数据预处理及分析3.1 数据可视化3.2 滑动窗口相关系数统计与动态置信区间耦合分析模型3.3 耦合关系分析结果 四、问题一代码数据预处理问题一 【2025年认证杯数学建模挑战赛】C题 数据预处理与问题一求解 三…

2025年最新Web安全(面试题)

活动发起人小虚竹 想对你说: 这是一个以写作博客为目的的创作活动,旨在鼓励大学生博主们挖掘自己的创作潜能,展现自己的写作才华。如果你是一位热爱写作的、想要展现自己创作才华的小伙伴,那么,快来参加吧&#xff01…

开源微调混合推理模型:cogito-v1-preview-qwen-32B

一、模型概述 1.1 模型特点 Cogito v1-preview-qwen-32B 是一款基于指令微调的生成式语言模型(LLM),具有以下特点: 支持直接回答(标准模式)和自我反思后再回答(推理模式)。使用 I…

Golang|Channel 相关用法理解

文章目录 用 channel 作为并发小容器channel 的遍历channel 导致的死锁问题用 channel 传递信号用 channel 并行处理文件用channel 限制接口的并发请求量用 channel 限制协程的总数量 用 channel 作为并发小容器 注意这里的 ok 如果为 false,表示此时不仅channel为空…

C++ - #命名空间 #输入、输出 #缺省参数 #函数重载 #引用 # const 引用 #inline #nullptr

文章目录 前言 一、实现C版本的hello world 二、命名空间 1、namespace 的价值 2、namespace 的定义 (1.域会影响一个编译器编译语法时的查找规则 (2、域会影响生命周期 (3、命名空间域只能定义在全局 (4、编译器会自动合并相同命名空间中的内容 (5、C标准库放在命名…

JSON处理工具/框架的常见类型及详解,以Java语言为例

以下是JSON处理工具/框架的常见类型及详解,以Java语言为例: 一、主流JSON处理工具对比 Jackson(推荐) 特点:高性能、功能丰富,支持注解(如JsonProperty)、树形模型(Json…

中间件--ClickHouse-1--基础介绍(列式存储,MPP架构,分布式计算,SQL支持,向量化执行,亿万级数据秒级查询)

1、概述 ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。它由俄罗斯的互联网巨头Yandex为解决其内部数据分析需求而开发,并于2016年开源。专为大规模数据分析,实时数据分析和复杂查询设计,具有高性能、实时数据和可扩展性等…

Java中的经典排序算法:插入排序、希尔排序、选择排序、堆排序与冒泡排序(如果想知道Java中有关插入排序、希尔排序、选择排序、堆排序与冒泡排序的知识点,那么只看这一篇就足够了!)

前言:排序算法是计算机科学中的基础问题之一,它在数据处理、搜索算法以及各种优化问题中占有重要地位,本文将详细介绍几种经典的排序算法:插入排序、选择排序、堆排序和冒泡排序。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解…

K8S+Prometheus+Consul+alertWebhook实现全链路服务自动发现与监控、告警配置实战

系列文章目录 k8s服务注册到consul prometheus监控标签 文章目录 系列文章目录前言一、环境二、Prometheus部署1.下载2.部署3.验证 三、kube-prometheus添加自定义监控项1.准备yaml文件2.创建新的secret并应用到prometheus3.将yaml文件应用到集群4.重启prometheus-k8s pod5.访…

uniapp-商城-25-顶部模块高度计算

计算高度: 使用computed进行顶部模块的计算。 总高度:bartotalHeight log 介绍--收款码这一条目 也就是上一章节的title的高度计算 bodybarheight。 在该组件中: js部分的代码: 包含了导出的名字: shop-head…

非关系型数据库(NoSQL)与 关系型数据库(RDBMS)的比较

非关系型数据库(NoSQL)与 关系型数据库(RDBMS)的比较 一、引言二、非关系型数据库(NoSQL)2.1 优势 三、关系型数据库(RDBMS)3.1 优势 四、结论 💖The Begin💖…

蓝桥杯2024国B数星星

小明正在一棵树上数星星,这棵树有 n 个结点 1,2,⋯,n。他定义树上的一个子图 G 是一颗星星,当且仅当 G 同时满足: G 是一棵树。G 中存在某个结点,其度数为 ∣VG​∣−1。其中 ∣VG​∣ 表示这个子图含有的结点数。 两颗星星不相…

中科院1区顶刊Expert Systems with Applications ESO:增强型蛇形算法,性能不错

Snake Optimizer(SO)是一种优化效果良好的新颖算法,但由于自然规律的限制,在探索和开发阶段参数更多是固定值,因此SO算法很快陷入局部优化并慢慢收敛。本文通过引入新颖的基于对立的学习策略和新的动态更新机制&#x…

Flask+Influxdb+grafna构建电脑性能实时监控系统

Influx下载地址,这里下载了以下版本influxdb-1.8.5_windows_amd64.zip 运行前需要先启动Influx数据库: 管理员方式运行cmd->F:->cd F:\influxdb\influxdb-1.8.5-1->influxd -config influxdb.conf,以influxdb.conf配置文件启动数…

若伊微服务版本教程(自参)

第一步 若伊官网下载源码 https://ruoyi.vip/ RuoYi-Cloud: 🎉 基于Spring Boot、Spring Cloud & Alibaba的分布式微服务架构权限管理系统,同时提供了 Vue3 的版本 git clone 到 本地 目录如下: 第二部 参考官网 运行部署说明 环境部署…

ESP32与STM32哪种更适合初学者?

目录 1、ESP32:物联网时代的“网红” 2、STM32:工业界的“常青树” 3、到底谁更容易? 无论是刚入坑的小白,还是想扩展技术栈的老鸟,在选择主力 MCU 时,学习曲线绝对是重要的考量因素。ESP32 以其强大的 …

秒杀秒抢系统开发:飞算 JavaAI 工具如何应对高并发难题?

秒杀、秒抢活动已成为电商促销与吸引流量的常用手段。然而,此类活动所带来的高并发访问,对系统性能构成了巨大挑战。如何确保系统在高并发场景下依然能够稳定、高效运行,成为开发者亟待解决的关键问题。飞算 JavaAI 工具作为一款功能强大的开…

C# 将Excel格式文件导入到界面中,用datagridview显示

界面按钮不做介绍。 主要代码: //用于获取从上一个页面传过来datagridview标题 public DataTable GetHeader { get; set; } private void UI_EXPINFO_Load(object sender, EventArgs e) { //页面加载显示listbox1中可…

Spring Boot整合难点?AI一键生成全流程解决方案

在当今的软件开发领域,Spring Boot 凭借其简化开发流程、快速搭建项目的优势,成为了众多开发者的首选框架。然而,Spring Boot 的整合过程并非一帆风顺,常常会遇到各种难点。而飞算 JavaAI 的出现,为解决这些问题提供了…

MySQL 锁机制全景图:分类、粒度与示例一图掌握

✅ 一、按粒度分类(锁的范围大小) 1. 表级锁(Table Lock) 锁住整张表粒度大,开销小,并发性差常见于:MyISAM 引擎 📌 示例: LOCK TABLES user WRITE; -- 会锁住整个 u…