如何基于Golang后端为多React应用实现自定义SAML SSO
自定义Golang IdP实现SAML SSO方案(多React应用场景)
一、核心流程梳理
针对你的多子域名React应用+Golang共享后端场景,自建SAML IdP的核心流程如下:
- 未认证用户访问任意React应用(aaa/bbb/ccc.myweb.com),前端跳转至统一登录入口(Golang后端的IdP登录页)
- 用户提交邮箱密码,Golang后端验证身份后生成SAML断言
- IdP将SAML断言通过POST请求发送至对应React应用的ACS(断言消费者服务)端点
- React应用验证SAML断言有效性,建立本地会话完成登录
- 用户访问其他应用时,前端检测到未登录跳转至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
四、部署与配置要点
- 证书生成:用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 - HTTPS配置:所有域名(auth.myweb.com、aaa/bbb/ccc.myweb.com)必须启用HTTPS,SAML协议强制要求
- 元数据交换:将IdP元数据(
https://auth.myweb.com/idp/metadata)提供给每个SP,同时将各SP的元数据配置到IdP中 - 会话管理:IdP添加HttpOnly会话Cookie,避免重复登录
http.SetCookie(w, &http.Cookie{ Name: "idp_session", Value: generateSessionID(user.ID), HttpOnly: true, Secure: true, Path: "/", MaxAge: 86400 * 7, // 7天有效期 }) - PostgreSQL集成:在
validateUser函数中实现真实的用户查询与密码哈希验证(推荐使用bcrypt存储密码)
五、关键注意事项
- 必须启用SAML断言签名,防止篡改
- 正确处理RelayState参数,确保用户登录后返回原访问页面
- 实现SAML单点登出(SLO),同步所有应用的登出状态
- 添加完善的错误处理,覆盖无效请求、验证失败等场景
内容的提问来源于stack exchange,提问作者Sharath B Naik
相关产品推荐
相关产品推荐

