You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于Golang后端为多React应用实现自定义SAML SSO

自定义Golang IdP实现SAML SSO方案(多React应用场景)

一、核心流程梳理

针对你的多子域名React应用+Golang共享后端场景,自建SAML IdP的核心流程如下:

  1. 未认证用户访问任意React应用(aaa/bbb/ccc.myweb.com),前端跳转至统一登录入口(Golang后端的IdP登录页)
  2. 用户提交邮箱密码,Golang后端验证身份后生成SAML断言
  3. IdP将SAML断言通过POST请求发送至对应React应用的ACS(断言消费者服务)端点
  4. React应用验证SAML断言有效性,建立本地会话完成登录
  5. 用户访问其他应用时,前端检测到未登录跳转至IdP,IdP识别已登录会话后直接生成断言返回,实现单点登录

二、Golang IdP核心实现(基于GoChi)

使用github.com/crewjam/saml库作为SAML核心依赖,结合GoChi框架实现IdP功能:

1. 初始化SAML配置

package main

import (
	"crypto/x509"
	"encoding/pem"
	"io/ioutil"
	"net/http"
	"net/url"

	"github.com/crewjam/saml"
	"github.com/crewjam/saml/samlsp"
	"github.com/go-chi/chi/v5"
)

var (
	idp *saml.IdentityProvider
	sps map[string]*saml.ServiceProvider
)

func init() {
	// 加载IdP私钥与证书
	idpKeyBytes, _ := ioutil.ReadFile("./idp-private-key.pem")
	idpCertBytes, _ := ioutil.ReadFile("./idp-cert.pem")
	idpKey, _ := x509.ParsePKCS1PrivateKey(idpKeyBytes)
	idpCert, _ := x509.ParseCertificate(idpCertBytes)

	// 配置IdP核心参数
	idp = &saml.IdentityProvider{
		EntityID:            "https://auth.myweb.com/idp/metadata",
		URL:                 url.URL{Scheme: "https", Host: "auth.myweb.com"},
		PrivateKey:          idpKey,
		Certificate:         idpCert,
		AllowIDPInitiated:   true,
		SignAuthnRequests:   true,
		SignAssertions:      true,
	}

	// 加载三个SP的元数据(从React应用获取或手动生成)
	sps = make(map[string]*saml.ServiceProvider)
	// aaa.myweb.com SP配置
	aaaSPMetaBytes, _ := ioutil.ReadFile("./aaa-sp-metadata.xml")
	aaaSP, _ := samlsp.NewServiceProviderFromMetadata(aaaSPMetaBytes)
	sps["https://aaa.myweb.com/saml/metadata"] = aaaSP

	// bbb.myweb.com SP配置
	bbbSPMetaBytes, _ := ioutil.ReadFile("./bbb-sp-metadata.xml")
	bbbSP, _ := samlsp.NewServiceProviderFromMetadata(bbbSPMetaBytes)
	sps["https://bbb.myweb.com/saml/metadata"] = bbbSP

	// ccc.myweb.com SP配置
	cccSPMetaBytes, _ := ioutil.ReadFile("./ccc-sp-metadata.xml")
	cccSP, _ := samlsp.NewServiceProviderFromMetadata(cccSPMetaBytes)
	sps["https://ccc.myweb.com/saml/metadata"] = cccSP
}

2. 统一登录入口实现

// 登录页面路由
func loginPage(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte(`
		<form method="POST" action="/login">
			<input type="email" name="email" required placeholder="邮箱">
			<input type="password" name="password" required placeholder="密码">
			<input type="hidden" name="RelayState" value="` + r.URL.Query().Get("RelayState") + `">
			<input type="hidden" name="SAMLRequest" value="` + r.URL.Query().Get("SAMLRequest") + `">
			<button type="submit">登录</button>
		</form>
	`))
}

// 登录验证与SAML断言生成
func loginHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	email := r.FormValue("email")
	password := r.FormValue("password")
	samlRequest := r.FormValue("SAMLRequest")
	relayState := r.FormValue("RelayState")

	// PostgreSQL用户验证逻辑
	user, err := validateUser(email, password)
	if err != nil {
		http.Redirect(w, r, "/login?error=invalid_credentials", http.StatusSeeOther)
		return
	}

	// 解析SAML请求,匹配对应SP
	req, err := saml.ParseAuthnRequest(samlRequest)
	if err != nil {
		http.Error(w, "无效的SAML请求", http.StatusBadRequest)
		return
	}
	sp, ok := sps[req.Issuer.Value]
	if !ok {
		http.Error(w, "未知服务提供商", http.StatusBadRequest)
		return
	}

	// 生成SAML断言
	assertion := idp.MakeAssertion(saml.Assertion{
		Subject: saml.Subject{
			NameID: saml.NameID{Value: user.Email},
			SubjectConfirmations: []saml.SubjectConfirmation{
				{
					Method: saml.SubjectConfirmationMethodBearer,
					SubjectConfirmationData: saml.SubjectConfirmationData{
						Recipient: sp.ACSURL.String(),
					},
				},
			},
		},
		Attributes: []saml.Attribute{
			{
				Name:       "email",
				NameFormat: saml.AttributeNameFormatURI,
				Values:     []saml.AttributeValue{{Value: user.Email}},
			},
			{
				Name:       "full_name",
				NameFormat: saml.AttributeNameFormatURI,
				Values:     []saml.AttributeValue{{Value: user.FullName}},
			},
		},
	})

	// 生成SAML响应并提交至SP的ACS端点
	resp := idp.MakeResponse(saml.Response{
		Assertions:    []saml.Assertion{assertion},
		InResponseTo:  req.ID,
		Destination:   sp.ACSURL.String(),
	})
	samlsp.SendResponse(w, r, resp, relayState)
}

// 模拟PostgreSQL用户验证
func validateUser(email, password string) (*User, error) {
	// 替换为实际数据库查询与密码哈希验证逻辑
	return &User{Email: email, FullName: "测试用户"}, nil
}

type User struct {
	Email    string
	FullName string
}

3. IdP元数据端点

func idpMetadataHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/xml")
	w.Write(idp.Metadata())
}

4. GoChi路由注册

func main() {
	r := chi.NewRouter()

	// IdP核心路由
	r.Get("/idp/metadata", idpMetadataHandler)
	r.Get("/login", loginPage)
	r.Post("/login", loginHandler)

	// 原有REST API路由挂载
	r.Mount("/api", apiRouter())

	http.ListenAndServe(":8080", r)
}

func apiRouter() http.Handler {
	r := chi.NewRouter()
	// 原有业务API端点,如用户信息查询等
	r.Get("/user/me", func(w http.ResponseWriter, r *http.Request) {
		// 从会话中获取用户信息并返回
	})
	return r
}

三、React应用(SP)实现要点

每个React应用需实现SAML断言验证与会话管理:

1. ACS端点实现(以Next.js API路由为例)

使用node-saml库处理SAML断言验证:

import saml from 'node-saml';
import fs from 'fs';

const sp = new saml.ServiceProvider({
  entityId: 'https://aaa.myweb.com/saml/metadata',
  assertionConsumerServiceURL: 'https://aaa.myweb.com/api/saml/acs',
  privateKey: fs.readFileSync('./sp-private-key.pem', 'utf8'),
  certificate: fs.readFileSync('./sp-cert.pem', 'utf8'),
  idpMetadata: fs.readFileSync('./idp-metadata.xml', 'utf8'),
});

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).send('不允许的请求方法');
  }

  try {
    const { profile } = await sp.parseResponse(req);
    // 建立本地会话(设置HttpOnly Cookie)
    res.setHeader('Set-Cookie', `user=${JSON.stringify(profile)}; Path=/; Secure; HttpOnly; SameSite=Strict`);
    // 跳转至原访问页面或首页
    res.redirect(req.body.RelayState || '/');
  } catch (err) {
    console.error(err);
    res.status(401).send('无效的SAML断言');
  }
}

2. 前端登录跳转逻辑

import { saml } from '@simonaco/samlify';

const sp = saml.ServiceProvider({
  entityID: 'https://aaa.myweb.com/saml/metadata',
  acsUrl: 'https://aaa.myweb.com/api/saml/acs',
});

// 未登录时跳转至IdP登录页
const redirectToIdPLogin = () => {
  const loginUrl = sp.createLoginRequestURL('https://auth.myweb.com/idp/metadata', {
    RelayState: window.location.pathname,
  });
  window.location.href = loginUrl;
};

// 应用入口处判断会话,无会话则调用redirectToIdPLogin

四、部署与配置要点

  1. 证书生成:用openssl为IdP和每个SP生成RSA密钥对
    # IdP密钥对
    openssl genrsa -out idp-private-key.pem 2048
    openssl req -new -x509 -key idp-private-key.pem -out idp-cert.pem -days 365
    
    # SP密钥对(每个应用一套)
    openssl genrsa -out aaa-sp-private-key.pem 2048
    openssl req -new -x509 -key aaa-sp-private-key.pem -out aaa-sp-cert.pem -days 365
    
  2. HTTPS配置:所有域名(auth.myweb.com、aaa/bbb/ccc.myweb.com)必须启用HTTPS,SAML协议强制要求
  3. 元数据交换:将IdP元数据(https://auth.myweb.com/idp/metadata)提供给每个SP,同时将各SP的元数据配置到IdP中
  4. 会话管理:IdP添加HttpOnly会话Cookie,避免重复登录
    http.SetCookie(w, &http.Cookie{
      Name:     "idp_session",
      Value:    generateSessionID(user.ID),
      HttpOnly: true,
      Secure:   true,
      Path:     "/",
      MaxAge:   86400 * 7, // 7天有效期
    })
    
  5. PostgreSQL集成:在validateUser函数中实现真实的用户查询与密码哈希验证(推荐使用bcrypt存储密码)

五、关键注意事项

  • 必须启用SAML断言签名,防止篡改
  • 正确处理RelayState参数,确保用户登录后返回原访问页面
  • 实现SAML单点登出(SLO),同步所有应用的登出状态
  • 添加完善的错误处理,覆盖无效请求、验证失败等场景

内容的提问来源于stack exchange,提问作者Sharath B Naik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 09:48:09