Go 项目中 SSO 实现技术方案

发布于:2025-09-05 ⋅ 阅读:(16) ⋅ 点赞:(0)

Go 项目中 SSO 技术方案实现

一、方案概述

本方案基于OIDC(OpenID Connect)协议实现单点登录功能,采用"集中式IdP(身份提供者)+ 分布式SP(服务提供者)"架构,适用于中大型Go语言项目集群。通过该方案,用户只需一次登录即可访问所有信任的业务系统,实现"一处登录,多处使用"的效果。

二、架构设计

2.1 整体架构图

┌───────────┐      ┌──────────────┐      ┌──────────────┐
│           │      │              │      │              │
│  用户浏览器  │◄────►│  IdP服务     │◄────►│  数据库       │
│           │      │  (Go/Gin)    │      │  (用户信息)   │
└───────────┘      └──────────────┘      └──────────────┘
       ▲                   ▲
       │                   │
       ▼                   ▼
┌───────────┐      ┌──────────────┐
│           │      │              │
│  SP1服务   │◄────►│  Redis集群    │
│  (Go/Gin)  │      │  (会话/令牌)  │
└───────────┘      └──────────────┘
       ▲
       │
       ▼
┌───────────┐
│           │
│  SP2服务   │
│  (Go/Gin)  │
│           │
└───────────┘

2.2 核心组件

  1. IdP(身份提供者)

    • 统一认证中心,负责用户身份验证
    • 颁发和管理令牌(ID Token、Access Token)
    • 维护全局会话
  2. SP(服务提供者)

    • 各业务系统,如订单系统、用户系统等
    • 依赖IdP进行身份认证
    • 维护本地会话和权限控制
  3. 存储层

    • PostgreSQL:存储用户信息、SP配置
    • Redis:存储全局会话、令牌黑名单、缓存

三、技术栈选型

组件 选型 用途
Web框架 Gin 构建IdP和SP服务
OIDC协议 coreos/go-oidc + golang/oauth2 实现OIDC认证流程
JWT处理 golang-jwt/jwt 生成和验证JWT令牌
会话管理 gin-contrib/sessions 管理SP本地会话
数据库 PostgreSQL 存储用户和配置信息
缓存 Redis 存储会话和令牌
密码加密 golang.org/x/crypto/bcrypt 用户密码加密存储
日志 zap 系统日志记录
配置管理 viper 配置文件管理

四、核心流程实现

4.1 首次登录流程

  1. 用户访问SP,SP检测到未登录,重定向到IdP
  2. IdP展示登录页面,用户输入凭据
  3. IdP验证凭据,创建全局会话
  4. IdP生成授权码,重定向回SP的回调地址
  5. SP使用授权码向IdP请求令牌
  6. IdP返回ID Token和Access Token
  7. SP验证ID Token,创建本地会话
  8. SP允许用户访问资源

4.2 跨系统登录流程

  1. 用户已登录IdP,访问另一个SP
  2. SP检测到未登录,重定向到IdP
  3. IdP检测到已有全局会话,无需重新登录
  4. IdP生成授权码,重定向回该SP的回调地址
  5. SP使用授权码获取并验证令牌,创建本地会话
  6. SP允许用户访问资源

4.3 统一登出流程

  1. 用户在任一SP发起登出请求
  2. SP清除本地会话,并重定向到IdP的登出端点
  3. IdP清除全局会话,生成登出令牌
  4. IdP向用户已登录的所有SP发送登出通知
  5. 各SP接收通知,清除本地会话
  6. 重定向到登出成功页面

五、代码实现示例

5.1 IdP核心实现

package main

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
	"github.com/spf13/viper"
	"go.uber.org/zap"
)

// 全局变量
var (
	logger     *zap.Logger
	privateKey *rsa.PrivateKey
	publicKey  *rsa.PublicKey
)

// 配置结构体
type Config struct {
   
   
	ServerPort     int
	RedisAddr      string
	RedisPassword  string
	RedisDB        int
	JWTExpiryHours int
}

// 自定义JWT声明
type CustomClaims struct {
   
   
	UserID   string   `json:"user_id"`
	Username string   `json:"username"`
	Roles    []string `json:"roles"`
	jwt.RegisteredClaims
}

// 初始化配置
func initConfig() Config {
   
   
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")
	
	if err := viper.ReadInConfig(); err != nil {
   
   
		log.Fatalf("读取配置文件失败: %v", err)
	}
	
	var config Config
	if err := viper.Unmarshal(&config); err != nil {
   
   
		log.Fatalf("解析配置失败: %v", err)
	}
	
	return config
}

// 生成RSA密钥对
func generateRSAKeys() error {
   
   
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
   
   
		return err
	}
	
	privateKey = key
	publicKey = &key.PublicKey
	
	// 保存公钥到文件,供SP使用
	publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
	if err != nil {
   
   
		return err
	}
	
	publicKeyPEM := pem.EncodeToMemory(&pem.Block{
   
   
		Type:  "PUBLIC KEY",
		Bytes: publicKeyBytes,
	})
	
	return os.WriteFile("public_key.pem", publicKeyPEM, 0644)
}

// 生成JWT令牌
func generateJWT(userID, username string, roles []string, expiryHours int) (string, error) {
   
   
	expirationTime := time.Now().Add(time.Duration(expiryHours) * time.Hour)
	
	claims := &CustomClaims{
   
   
		UserID:   userID,
		Username: username,
		Roles:    roles,
		RegisteredClaims: jwt.RegisteredClaims{
   
   
			ExpiresAt: jwt.NewNumericDate(expirationTime),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    "sso-idp",
			Subject:   userID,
		},
	}
	
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	return token.SignedString(privateKey)
}

// 登录处理
func loginHandler(c *gin.Context) {
   
   
	var loginReq struct {
   
   
		Username string `json:"username" binding:"required"`
		Password string `json:"password" binding:"required"`
		ClientID string `json:"client_id" binding:"required"`
	}
	
	if err := c.ShouldBindJSON(&loginReq); err != nil {
   
   
		c.JSON(http.StatusBadRequest, gin.H{
   
   "error": err.Error()})
		return
	}
	
	// 实际应用中应从数据库验证用户
	// 这里简化处理,假设验证通过
	userID := "user123"
	roles := []string{
   
   "user", "editor"}
	
	// 创建全局会话
	session 

网站公告

今日签到

点亮在社区的每一天
去签到