基于SAML 2.0对接阿里云的SSO(单点登录)

这篇具有很好参考价值的文章主要介绍了基于SAML 2.0对接阿里云的SSO(单点登录)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

公司使用的阿里云作为公有云,每次员工入职或离职时同时需要维护两套账号(一套内部账号,一套阿里云RAM账号),为了让用户能够使用内部账号能访问阿里云,所以决定对接阿里云的SSO
基于SAML 2.0对接阿里云的SSO(单点登录)

  • 主流程介绍
  1. 用户访问阿里云
  2. 阿里云调转至公司内部的SSO(单点登录)
  3. 公司内部SSO让用户进行登录
  4. 认证成功后跳转至阿里云

阿里云SSO

官网介绍

https://help.aliyun.com/document_detail/93684.html

文档解析

我相信不少人跟我一样,在做需求前首先去研究他的文档。结果看了一遍下来却不知所云,如果你也有这样的问题,那么接下来我带你一步一步的解析。

什么是SAML

由于阿里云的SSO采用的是SAML2.0协议,所以第一步你需要了解SAML是什么!
SAML链接:

https://help.sap.com/doc/saphelp_me150/15.0.3VERSIONFORSAPME/zh-CN/17/6d45fc91e84ef1bf0152f2b947dc35/content.htm?no_cache=true

IDP和SP

基于SAML 2.0对接阿里云的SSO(单点登录)
阿里云关于SSO的名词解释太多,由于篇幅原因,这里我只介绍IDP和SP这两个比较重要的概念

  • IDP

身份提供商:
说白了就是对接阿里云SSO的第三方提供的一个身份认证的服务。这个服务你可以使用云厂商的IDP(花钱买),也可以自建企业本地IDP(必须支持SAML2.0协议),而我选择后者(省钱才是王道)

  • SP

服务提供商:
概念阿里云已经解释的比较详细了(然而用户可能还是一脸蒙蔽),在我们这个场景中SP其实指的就是阿里云,如果你要对接华为的SSO的话,这个SP其实指的就是华为云(这么解释的话你应该就好理解了吧)

开始对接

说明:
阿里云SSO有「用户SSO」和「角色SSO」两种对接方式,我们选择「用户SSO」进行对接
基于SAML 2.0对接阿里云的SSO(单点登录)
在接下来的对接工作中,我想你应该已经知道,我们只需要基于SAML2.0来实现自己的IDP即可。由于公司内部有基于OAUTH2实现的SSO,所以我要做的就是在这个SSO服务中嵌入IDP
,当然你也可以单独拉个服务出来实现IDP

开始开发

说明:

  • 以下代码使用golang实现
  • 使用 github.com/crewjam/saml来实现自建IDP
  • 微服务框架: https://ego.gocn.vip/
  • 开发前,请先下载阿里云提供的元数据文件,后续代码中会用到

基于SAML 2.0对接阿里云的SSO(单点登录)

自建IDP

import (
	"crypto"
	"crypto/x509"
	"encoding/pem"
	"encoding/xml"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"

	"github.com/crewjam/saml"
	"github.com/crewjam/saml/samlidp"
	"github.com/gotomicro/ego-component/egorm"
	oauth2dto "github.com/gotomicro/ego-component/eoauth2/storage/dto"
	"github.com/gotomicro/ego/core/econf"
	"go.uber.org/zap"
)

var (
    // 保证saml实例为单例
	once               sync.Once
	samlServerInstance *samlServer
)

type samlServer struct {
    // serviceProviders 存储华为云,或者阿里云的SP实例
	serviceProviders     map[string]*saml.EntityDescriptor
	lock                 sync.Mutex
	// 自建IDP 实例
	idp                  *saml.IdentityProvider
	store                samlidp.Store
	// sp 名称列表
	serviceProviderNames []string
}

// GetSamlServerInstance 获取SamlServer 实例(单例:懒汉)
func GetSamlServerInstance() *samlServer {
	// lazy init
	once.Do(func() {
	    // appHost 你的域名
	    // econf.GetString("domain"): 从配置文件中获取
		appHost, err := url.Parse(econf.GetString("domain"))
		if err != nil {
			panic("get appHost fail:" + err.Error())
		}
		metadataURL := *appHost
		ssoUrl := *appHost
		logoutUrl := *appHost
		// 获取IDP元数据信息路由
		metadataURL.Path = metadataURL.Path + "/sso/third/idpMetadata"
		// IDP认证路由
		ssoUrl.Path = ssoUrl.Path + "/sso/third/saml"
		// IDP退出路由
		logoutUrl.Path = "/sso/logout"
		samlServerInstance = &samlServer{
			serviceProviders: map[string]*saml.EntityDescriptor{},
			idp: &saml.IdentityProvider{
				Key:         rsaPrivateKey()(), // IDP 提供的 rsa  私钥
				Certificate: x509Cert()(),     // IDP 提供的 x509 证书
				MetadataURL: metadataURL,   // 获取IDP元数据信息路由
				SSOURL:      ssoUrl,       // IDP认证路由(登录)
				LogoutURL:   logoutUrl,    // IDP退出路由
			},
			serviceProviderNames: []string{"aliyun"},
		}
		// 实例化SP实例并存储
		samlServerInstance.storeServiceProvider()
		// 初始化SP
		err = samlServerInstance.initializeServiceProviders()
		if err != nil {
			panic("initializeServiceProviders  fail:" + err.Error())
		}
		samlServerInstance.idp.ServiceProviderProvider = samlServerInstance
	})
	return samlServerInstance
}

// IDPMetadata 生成基于 saml 2.0 的idp xml
// 后续会将该IDP xml 上传至阿里云中。使阿里云信任该IDP
func (s *samlServer) IDPMetadata() ([]byte, error) {
	buf, err := xml.MarshalIndent(s.idp.Metadata(), "", "  ")
	if err != nil {
		invoker.Logger.Error("IDPMetadata-MarshalIndent", zap.Error(err))
		return nil, err
	}
	return buf, nil
}



// storeServiceProvider 根据SP提供的元数据文件,实例化SP实例并存储至缓存
// 阿里云作为SP,会提供元数据信息文件,来让你的IDP对阿里云作为SP进行信任
// https://ram.console.aliyun.com/providers
func (s *samlServer) storeServiceProvider() {
	store := &samlidp.MemoryStore{}
	for _, samlName := range s.serviceProviderNames {
		metadata := saml.EntityDescriptor{}
		// 读取从阿里云下载下来的元数据文件(建议线上环境,将该文件保存至k8s的Secret中)
		confKey := fmt.Sprintf("saml.%s_metadata_file", samlName)
		contentByte, err := util.ReadFile(econf.GetString(confKey))
		if err != nil {
			invoker.Logger.Error("storeServiceProvider-ReadFile", zap.Error(err), zap.Any("invalid samlName", samlName))
			continue
		}
		err = xml.Unmarshal(contentByte, &metadata)
		if err != nil {
			invoker.Logger.Error("storeServiceProvider-Unmarshal", zap.Error(err), zap.Any("invalid metadata", string(contentByte)))
			continue
		}
		spKey := fmt.Sprintf("/services/%s", samlName)
		err = store.Put(spKey, samlidp.Service{
			Name:     samlName,
			Metadata: metadata,
		})
		if err != nil {
			invoker.Logger.Error("storeServiceProvider-storePut", zap.Error(err))
			continue
		}
	}
	// 将SP实例存储至内存中
	s.store = store
}

// initializeServiceProviders: 初始化 sp
func (s *samlServer) initializeServiceProviders() error {
	serviceNames, err := s.store.List("/services/")
	if err != nil {
		return err
	}
	for _, serviceName := range serviceNames {
		service := samlidp.Service{}
		if err := s.store.Get(fmt.Sprintf("/services/%s", serviceName), &service); err != nil {
			return err
		}
		s.serviceProviders[service.Metadata.EntityID] = &service.Metadata
	}
	return nil
}

// rsaPrivateKey ras 私钥
func rsaPrivateKey() func() crypto.PrivateKey {
	return func() crypto.PrivateKey {
	    // 该私钥上线时,你可以存储在k8s的Sceret中,本地调试的话,就直接读本地文件
		contentBytes, err := util.ReadFile(econf.GetString("saml.keyFile"))
		if err != nil {
			panic("parse saml.keyFile fail:" + err.Error())
		}
		b, _ := pem.Decode(contentBytes)
		if b == nil {
			panic("Decode saml.keyFile fail")
		}
		k, err := x509.ParsePKCS8PrivateKey(b.Bytes)
		if err != nil {
			panic("ParsePKCS8PrivateKey saml.keyFile fail:" + err.Error())
		}
		return k
	}
}

// x509Cert x509证书
func x509Cert() func() *x509.Certificate {
	return func() *x509.Certificate {
	   // 该证书上线时,你可以存储在k8s的Sceret中,本地调试的话,就直接读本地文件
		contentBytes, err := util.ReadFile(econf.GetString("saml.crtFile"))
		if err != nil {
			panic("parse saml.crtFile fail:" + err.Error())
		}
		b, _ := pem.Decode(contentBytes)
		if b == nil {
			panic("Decode saml.crtFile fail:" + err.Error())
		}
		c, err := x509.ParseCertificate(b.Bytes)
		if err != nil {
			panic("ParseCertificate saml.crtFile fail:" + err.Error())
		}
		return c
	}
}

上传IDP元数据文件至阿里云

此处的目的是建立阿里云对你的IDP的信任
到这里可能有人会问了,企业IDP元数据我在哪获取呢?

上述初始化代码中有这样一个方法

func (s *samlServer) IDPMetadata() ([]byte, error)

你可以调用该方法来获取IDP元数据文件(再包一层HTTP来调用该方法:注意鉴权)
你获取的IDP元数据文件可能是这样的:

基于SAML 2.0对接阿里云的SSO(单点登录)

然后将该文件上传至阿里云中,链接:https://ram.console.aliyun.com/providers
基于SAML 2.0对接阿里云的SSO(单点登录)
注意:以上操作时,请不要开启SSO,不然会影响现有用户的登录(因为此时还没有对接完成,只是建立了互信,IDP认证的接口还没开发)

开发IDP认证接口

说明: 当你访问阿里云时(开启阿里云SSO),阿里云会回调IDP的认证接口,所以接下来的时间我们会去实现该接口
基于SAML 2.0对接阿里云的SSO(单点登录)

// HTTP Route 相关代码省略

// HandlerSamlSSO 处理saml登录
func (s *samlServer) HandlerSamlSSO(c *oacore.Context) {
	var (
		request = c.Request
		writer  = c.Writer
	)

	redirectLogin := func() {
		c.Redirect(302, genRedirectUrl(request))
	}
	// 生成IDP 的 request:更多细节,可以翻看源码
	req, err := saml.NewIdpAuthnRequest(s.idp, request)
	if err != nil {
		invoker.Logger.Error("HandlerSSO-NewIdpAuthnRequest", zap.Error(err))
		c.JSONE(-1, "获取请求数据失败:"+err.Error(), nil)
		return
	}
	// 校验 IDP request 
	if err := req.Validate(); err != nil {
		invoker.Logger.Error("HandlerSSO-Validate", zap.Error(err))
		c.JSONE(-1, "校验请求数据失败:"+err.Error(), nil)
		return
	}
	// 校验用户是否登录
	userByToken, err := c.GetCookieUser()
	if err != nil {
		invoker.Logger.Warn("HandlerSSO-GetUserByParentToken", zap.Error(err))
		// 没有登录的话,跳转至内部的SSO登录页面
		redirectLogin()
		return
	}
	// 构建跳转至SP的断言信息
	samlSession, err := buildSession(userByToken)
	if err != nil {
		invoker.Logger.Error("HandlerSSO-buildSession", zap.Error(err))
		c.JSONE(-1, "获取断言信息失败:"+err.Error(), nil)
		return
	}
	assertionMaker := s.idp.AssertionMaker
	if assertionMaker == nil {
		assertionMaker = saml.DefaultAssertionMaker{}
	}

	if err := assertionMaker.MakeAssertion(req, samlSession); err != nil {
		invoker.Logger.Error("HandlerSSO-MakeAssertion", zap.Error(err))
		c.JSONE(-1, "设置断言失败:"+err.Error(), nil)
		return
	}
	/*
		翻看此处的源码:其实做了两件事情
		1.将断言信息写入表单
		2.提交表单(表单URL指向的是阿里云SSO)
		tmpl := template.Must(template.New("saml-post-form").Parse(`<html>` +
			`<form method="post" action="{{.URL}}" id="SAMLResponseForm">` +
			`<input type="hidden" name="SAMLResponse" value="{{.SAMLResponse}}" />` +
			`<input type="hidden" name="RelayState" value="{{.RelayState}}" />` +
			`<input id="SAMLSubmitButton" type="submit" value="Continue" />` +
			`</form>` +
			`<script>document.getElementById('SAMLSubmitButton').style.visibility='hidden';</script>` +
			`<script>document.getElementById('SAMLResponseForm').submit();</script>` +
			`</html>`))
	*/
	if err := req.WriteResponse(writer); err != nil {
		invoker.Logger.Error("HandlerSSO-WriteResponse", zap.Error(err))
		c.JSONE(-1, "write断言失败:"+err.Error(), nil)
		return
	}
}

// genRedirectUrl: 生成内部SSO系统的认证地址(跳转至内部系统的登录页面)
func genRedirectUrl(request *http.Request) string {
	var (
	    // oauth2 clientID: 配置文件中获取
		clientID    = econf.GetString("saml.clientID")
		// 阿里云跳转时携带的 SAMLRequest
		samlRequest = url.QueryEscape(request.URL.Query().Get("SAMLRequest"))
		// 阿里云跳转时携带的 SAMLRequest
		relayState  = url.QueryEscape(request.URL.Query().Get("RelayState"))
	)
	
	redirectUrl := fmt.Sprintf("/sso/login?SAMLRequest=%s&RelayState=%s&redirect_uri=%s&client_id=%s&response_type=code",
		samlRequest, relayState, econf.GetString("domain")+"/sso/third/saml", clientID, source)
	return redirectUrl
}


// buildSession 构造saml2.0断言所需字段
func buildSession(user *oauth2dto.User) (*saml.Session, error) {
	var (
		sourceType uint8
	)
	// 校验是否给该员工开启了阿里云账号(我们有后台去维护员工的阿里云账号)
	userThirdOpen, err := mysql.UserThirdOpenX(invoker.Db, egorm.Conds{
		"uid":         user.Uid,
	})
	if err != nil {
		invoker.Logger.Error("buildSession-UserThirdOpenX", zap.Error(err))
		return nil, fmt.Errorf("获取用户第三放应用信息失败:%s", err.Error())
	}
	if userThirdOpen.ID <= 0 {
		return nil, errors.New("请联系管理员同步第三方账号")
	}
	// 通用断言部分
	// NameID: 为员工的阿里云RAM账号
	nameId := strings.TrimSpace(userThirdOpen.NameID)
	session := &saml.Session{
		NameID:    nameId,
		UserName:  user.Username,
		UserEmail: user.Email,
	}
	return session, nil
}

验证

说明:到此开发和配置已经完成,接下来我们需要开启阿里云的SSO,然后验证整个流程

  • 开启SSO
    基于SAML 2.0对接阿里云的SSO(单点登录)

  • 使用RAM账号登录
    基于SAML 2.0对接阿里云的SSO(单点登录)

基于SAML 2.0对接阿里云的SSO(单点登录)
基于SAML 2.0对接阿里云的SSO(单点登录)
基于SAML 2.0对接阿里云的SSO(单点登录)

阿里云会携带saml相关请求参数重定向至企业内部的IDP登录页,认证成功后,会调用 func (s *samlServer) HandlerSamlSSO(c *oacore.Context)函数处理并跳转至阿里云

总结

以上就是关于企业内部SSO对接阿里云基于SAML2.0协议SSO的流程,如果解决你的问题,烦请点个赞喽!文章来源地址https://www.toymoban.com/news/detail-409447.html

到了这里,关于基于SAML 2.0对接阿里云的SSO(单点登录)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 飞书-SSO单点登录

    飞书 sso 单点登录 可以参考 飞书js-sdk Documentation - Feishu Open Platform 实现效果 核心代码 注意: 核心代码-封装qrcode方法 在 .vue 中的使用注意点 1.随时更新 currentTab[tab选中的值] 的值 2.首次二维码的更新可能有异步问题,需要借助 nextTick[钩子] 部分核心代码(vue2.0)

    2024年04月29日
    浏览(49)
  • 【单点登录SSO认证中心】

    (2017-09-22更新)GitHub:https://github.com/sheefee/simple-sso 1、http无状态协议 web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次请求/响应对之间没有任何联系 但这

    2024年02月13日
    浏览(43)
  • 八个开源免费单点登录(SSO)系统

    使用SSO服务可以提高多系统使用的用户体验和安全性,用户不必记忆多个密码、不必多次登录浪费时间。下面推荐一些市场上最好的开源SSO系统,可作为商业SSO替代。 单点登录(SSO)是一个登录服务层,通过一次登录访问多个应用。使用SSO服务可以提高多系统使用的用户体验

    2024年02月04日
    浏览(50)
  • 【springboot+vue项目(十五)】基于Oauth2的SSO单点登录(二)vue-element-admin框架改造整合Oauth2.0

    Vue-element-admin 是一个基于 Vue.js 和 Element UI 的后台管理系统框架,提供了丰富的组件和功能,可以帮助开发者快速搭建现代化的后台管理系统。 vue-element-admin/   |-- build/                          # 构建相关配置文件   |    |-- build.js                   # 生产环境构建脚本

    2024年02月20日
    浏览(41)
  • 【分布式技术专题】「单点登录技术架构」一文带领你好好认识以下Saml协议的运作机制和流程模式

    传统上,企业应用程序在公司网络中部署和运行。为了获取有关用户的信息,如用户配置文件和组信息,这些应用程序中的许多都是为与公司目录(如Microsoft Active Directory)集成而构建的。更重要的是,通常使用目录存储和验证用户的凭据。例如,如果您使用在本地运行的Share

    2024年02月05日
    浏览(40)
  • Java实现单点登录(SSO)详解:从理论到实践

    ✨✨谢谢大家捧场,祝屏幕前的小伙伴们每天都有好运相伴左右,一定要天天开心哦!✨✨  🎈🎈作者主页: 喔的嘛呀🎈🎈 ✨✨ 帅哥美女们,我们共同加油!一起进步!✨✨  目录 引言 一、什么是单点登录(SSO)? 二、SSO的工作原理 三、SSO的具体实现 SSO的核心概念

    2024年04月16日
    浏览(66)
  • SaaSpace:8款最好的免费SSO单点登录软件工具

    我们确实花了很多时间用于登录。 这不仅耗时,而且往往是一种令人羞愧的经历。(例如,你经常问自己,“这个密码中的\\\'B\\\'是大写还是小写?”,然后反思“我失去了记忆吗?”) 根据一些报告的研究,单点登录软件为努力跟踪其无数不同登录次数(平均191次)的企业用

    2024年02月06日
    浏览(252)
  • Webfunny前端监控如何接入飞书单点登录(SSO)

    Hello,大家好,欢迎使用** webfunny前端监控和埋点平台 **。今天我们将介绍一下如何接入飞书的登录系统。 友情提示:如果飞书侧已经配置好了,可以直接跳到第六步阅读。 进入飞书开发者后台,创建企业自建应用,命名「webfunny」(名字自己起哈) 应用创建完成后,会生成

    2024年04月27日
    浏览(36)
  • 前端049_单点登录SSO_封装 Axios 与 Mock.js

    安装 Axios ,来向后台发送接口请求 安装 Axios 发送接口请求 创建 src/utils/request.js

    2024年02月08日
    浏览(99)
  • 深入理解SSO原理,项目实践使用一个优秀开源单点登录项目(附源码)

    深入理解SSO原理,项目实践使用一个优秀开源单点登录项目(附源码)。 一、简介 单点登录(Single Sign On),简称为 SSO。 它的解释是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 ❝ 所谓一次登录,处处登录。同样一处退出,处处退出。 ❞

    2024年02月11日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包