结合OIDC和Cookie实现SSO

news2024/12/27 13:08:39

结合OIDC和Cookie实现SSO

1 什么是SSO

SSO(Single Sign On,即单点登录),允许用户在多个网站或者应用程序之间使用一组凭据(例如用户名和密码)进行身份验证。用户只需要在登录一个网站或者应用程序后,就可以访问其他网站或者应用程序,而无需再输入凭据。

Tips

重点是在多个网站之间使用一组凭据,并且用户只需要登录一个网站或应用程序,其他网站或者应用程序就无需再输入凭据了。

目前实现SSO的协议或标准很多,如SAML、OAuth、LADP和OIDC等,都能实现SSO。

2 什么是OIDC

OIDC是一个协议,简单来讲就是OIDC规定有一个中心的Provider能够认证用户的凭据并且授权,即会返回AccessToken和IDToken等给受信任的Client,因为IDToken是JWT格式且包含登录用户的唯一标识,所以Client能够轻松地解析IDToken获取用户信息甚至存储到自身的数据库中。

这里选用OIDC的原因是Client能够很好地解析IDToken获取用户信息

这里的OIDC Provider的实现是golang的dex库:https://dexidp.io/docs/getting-started/

3 什么是Cookie

Cookie是一种在Web服务器和Web浏览器之间传递的小型文本。当访问Web应用程序或者浏览器时,Web服务器可能通过设置Cookie将一些信息存储到用户的浏览器上。接着Web浏览器在发送HTTP请求时,会将该网站相关的Cookie一并发送给Web服务器,Web服务器可以非常轻松的读取这些Cookie。

Cookie通常由一个名称(Name)、一个值(Value)、一个过期时间(Expires)和一个域名(Domain)组成。名称和值指定了Cookie中存储的信息,过期时间指定了Cookie的有效期,域名指定了允许访问该Cookie的域名。

这里选用Cookie的原因是利用了Cookie的域的特性:如果Cookie的域是.example.com,那么a.example.comb.example.com都能够访问到这个域。

所以我如果在A网站的服务器中设置Cookie的域为.example.com,那么在B网站中可以使用到该Cookie,反之亦然。

Tips

如果想要结合OIDC和Cookie实现SSO,那么网站的域名应该拥有相同的父域名。

4 需求

现在学校有一个课程系统(lessons)和书籍系统(books),要求就是用户在要求登录一次后,访问另一系统就不需要再登录了。

5 实现

使用OIDC Provider进行用户认证和授权,返回AccessToken和IDToken给Client,并且Client要求浏览器使用Cookie保存AccessToken和IDToken,并且域设置为课程系统和书籍系统都能访问到的域。在访问课程系统和书籍系统对应的服务器的时候读取存放在Cookie中的token并且访问后端。

要点:

  • Client从OIDC Provider处获得token,获取到token保存到Cookie中
  • Client在接受来自浏览器的请求时读取Cookie获得token,验证token并获取用户个人信息。

6 Books Client

获得token并写入Cookie

const (
	OidcProvider = "http://sso.college.edu:5556/dex"
	ClientId     = "books-college"
	ClientSecret = "books-college-secret"
	RedirectURL  = "http://books.college.edu:8000/callback"
)

func Login() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		provider, err := oidc.NewProvider(ctx, OidcProvider)
		if err != nil {
			http.Error(w, fmt.Sprintf("init oidc provider failed: %s", err), http.StatusInternalServerError)
			return
		}

		oauth2Config := Oauth2Config(provider)
		url := oauth2Config.AuthCodeURL("state")
		http.Redirect(w, r, url, http.StatusFound)
	}
}

func LoginCallback() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		provider, err := oidc.NewProvider(ctx, OidcProvider)
		if err != nil {
			http.Error(w, fmt.Sprintf("init oidc provider failed: %s", err), http.StatusInternalServerError)
			return
		}
		config := Oauth2Config(provider)
		oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"))
		if err != nil {
			http.Error(w, fmt.Sprintf("exchange token with server failed: %s", err), http.StatusUnauthorized)
			return
		}

		rawIDToken, ok := oauth2Token.Extra("id_token").(string)
		if !ok {
			http.Error(w, fmt.Sprintf("get rawIDToken with token failed"), http.StatusUnauthorized)
			return
		}
		idTokenVerifier := provider.Verifier(&oidc.Config{ClientID: ClientId})
		idToken, err := idTokenVerifier.Verify(ctx, rawIDToken)
		if err != nil {
			http.Error(w, fmt.Sprintf("verify IDToken with oidc provider failed: %s", err), http.StatusUnauthorized)
			return
		}

		setTokenIntoCookie(w, oauth2Token)
		bytes, _ := json.Marshal(idToken)
		w.Write(bytes)
	}
}

将token写入Cookie中并设置合适的域

这样books.college.edulessons.college.edu都能访问到该Cookie

const (
	CookieDomain = ".college.edu"
)

func setTokenIntoCookie(w http.ResponseWriter, oauth2Token *oauth2.Token) {
	rawIDToken, _ := oauth2Token.Extra("id_token").(string)
	cookies := []*http.Cookie{
		{Name: "access_token", Value: oauth2Token.AccessToken},
		{Name: "token_type", Value: oauth2Token.TokenType},
		{Name: "refresh_token", Value: oauth2Token.RefreshToken},
		{Name: "expiry", Value: oauth2Token.Expiry.Format(time.RFC3339)},
		{Name: "id_token", Value: rawIDToken},
	}
	for _, c := range cookies {
		c.Domain = CookieDomain
		c.Path = "/"
		c.MaxAge = 60 * 5 // 5 minutes
		c.HttpOnly = true
		http.SetCookie(w, c)
	}
}

访问接口时,读取来自Cookie中的token

如果读取token失败(Cookie中不存在token,token过期等)则要求用户重新登录

func MyBook() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		ui, err := auth.GetUserInfo(ctx, r)
		if err != nil {
			http.Redirect(w, r, "/login", http.StatusFound)
			return
		}

		msg := fmt.Sprintf("These are your books, %s!", ui.Name)
		w.Write([]byte(msg))
	}
}

验证token是否合法,并解析token获得用户的个人信息

func GetUserInfo(ctx context.Context, r *http.Request) (*userinfo.UserInfo, error) {
	token, err := getTokenFromCookie(r)
	if err != nil {
		return nil, fmt.Errorf("get userinfo failed: %v", err)
	}

	provider, err := oidc.NewProvider(ctx, OidcProvider)
	if err != nil {
		return nil, fmt.Errorf("initialize provider failed: %v", err)
	}
	idTokenVerifier := provider.Verifier(&oidc.Config{SkipClientIDCheck: true})
	idToken, err := idTokenVerifier.Verify(ctx, token)
	if err != nil {
		return nil, fmt.Errorf("verify rawIDToken failed: %v", err)
	}

	var ui *userinfo.UserInfo
	if err = idToken.Claims(&ui); err != nil {
		return nil, fmt.Errorf("parse idToken failed: %v", err)
	}

	return ui, nil
}

func getTokenFromCookie(r *http.Request) (string, error) {
	rawExpiry, err := r.Cookie("expiry")
	if err != nil {
		return "", fmt.Errorf("get token from cookie failed: %v", err)
	}
	expiry, err := time.Parse(time.RFC3339, rawExpiry.Value)
	if err != nil {
		return "", fmt.Errorf("parse expiry which is from cookie failed: %v", err)
	}
	if expiry.Before(time.Now()) {
		return "", fmt.Errorf("token is expired")
	}

	rawIDToken, err := r.Cookie("id_token")
	if err != nil {
		return "", fmt.Errorf("get token from cookie failed: %v", err)
	}

	return rawIDToken.Value, nil
}

7 Lessons Client

课程系统和书籍系统同理。

8 演示测试

  1. 配置host模拟真实环境

    ## sso-demo
    127.0.0.1	books.college.edu
    127.0.0.1	lessons.college.edu
    127.0.0.1	sso.college.edu
    
  2. 配置dex的config-dev.yaml,将books和lessons加入staticClients

    staticClients:
    - id: books-college
      secret: books-college-secret
      name: 'Books College'
      redirectURIs:
          - 'http://books.college.edu:8000/callback'
    - id: lessons-college
      secret: lessons-college-secret
      name: 'Lessons College'
      redirectURIs:
          - 'http://lessons.college.edu:8001/callback'
    
  3. 首次访问books:http://books.college.edu:8000/,要求登录

    在这里插入图片描述

  4. 登录完成

    在这里插入图片描述

  5. 访问lessons:http://lessons.college.edu:8001/,成功访问并且不需要登录

    在这里插入图片描述

  6. 再次访问books

    在这里插入图片描述

至此,实现了只需要在某一系统中登录过一次,在另外的系统就不需要再次登录直接就能进行访问了。

缺陷

  • 依赖Cookie

    显而易见,该实现依赖于Cookie中存储token,并且在访问Web服务器的时候携带Cookie。在无法使用Cookie或浏览器禁用Cookie的时候就需要使用其他的方法了,如URL参数或者Web Storage等。

  • 父域名必须相同

    由于浏览器限制,只有父域名相同才能使用同一以.开头的域的Cookie。

  • 没有记录登录状态

    以上代码没有在OIDC Provider处记录用户的登录状态,即如果用户在某一系统中退出了账号,但是只要其他任何地方的浏览器Cookie中存有该token,仍然可以使用token进行访问,所以用户其实并没有完全退出,只是在某一个浏览器中退出了而已。解决办法是在OIDC Provider中添加对用户的登录状态管理即可。

演示代码

https://github.com/FanGaoXS/sso-demo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/789322.html

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

相关文章

【920信号与系统笔记】第四章 连续时间系统的频域分析

第四章 连续时间系统的频域分析 4.1引言4.2信号通过系统的频域分析方法频域系统函数H(jw)系统在周期性信号激励下的频域分析系统在非周期信号激励下的频域分析周期信号和非周期信号分析方法比较 4.1引言 频域分析法 1.步骤 1.时域求解响应的问题通过傅里叶级数或者傅里叶变换转…

【MATLAB】GM(1,1) 灰色预测模型及算法

一、灰色预测模型概念 灰色预测是一种对含有不确定因素的系统进行预测的方法。 灰色预测通过鉴别系统因素之间发展趋势的相异程度,即进行关联分析,并对原始数据进行生成处理来寻找系统变动的规律,生成有较强规律性的数据序列,然后…

NFT和数字藏品的安全方案解析

一、NFT和数字藏品 01 NFT是什么? NFT 是Non-Fungible Tokens 的缩写,意思是不可互换的代币,它是相对于可互换的代币而言的。不可互换的代币也称为非同质代币。什么是可互换的代币?比如BTC(比特币)、ETH&…

用i18next使你的应用国际化-React

ref: https://www.i18next.com/ i18next是一个用JavaScript编写的国际化框架。 i18next为您提供了一个完整的解决方案,本地化您的产品从web端到移动端和桌面端。 在react项目中安i18next依赖: i18nextreact-i18nexti18next-browser-languagedetector&…

详细介绍如何使用 PyTorch 和 Lightning 增强医学多标签(人类蛋白质)图像分类-附源码

文末提供免费的原代码下载链接 在医疗诊断这一关键领域,快速、准确的图像分类在帮助医疗保健专业人员的决策中发挥着至关重要的作用。深度学习的出现,加上 PyTorch 等强大的框架,使得应用前沿模型来处理复杂的任务(例如医学多标签图像分类)成为可能。在本次演示中,我们将…

最新基于Citespace、vosviewer、R语言的文献计量学可视化分析技术及全流程文献可视化SCI论文高效写作方法

文献计量学是指用数学和统计学的方法,定量地分析一切知识载体的交叉科学。它是集数学、统计学、文献学为一体,注重量化的综合性知识体系。特别是,信息可视化技术手段和方法的运用,可直观的展示主题的研究发展历程、研究现状、研究…

logback-spring.xml日志配置文件详解

目录 前言logback-spring.xml 配置 前言 打印日志是一个系统的基本功能&#xff0c;系统出现异常可以通过查找日志弄清楚是什么原因&#xff0c;从而更加快速地定位问题&#xff0c;修复系统。 logback-spring.xml 配置 文件位置 具体配置 <?xml version"1.0"…

Vlan端口隔离(第二十四课)

一、端口隔离 1、端口隔离技术概述 1)端口隔离技术出现背景:为了实现报文之间的二层隔离,可以将不同的端口加入不同的VLAN,但这样会浪费有限的VLAN ID资源。 2)端口隔离的作用:采用端口隔离功能,可以实现同一VLAN内端口之间的隔离。 3)如何实现端口隔离功能:只需要…

Windows如何安装Django及如何创建项目

目录 1、Windows安装Django--pip命令行 2、创建项目 2.1、终端创建项目 2.2、在Pycharm中创建项目 2.3、二者创建的项目有何不同 2.4、项目目录说明 1、Windows安装Django--pip命令行 安装Django有两种方式&#xff1a; pip命令行【推荐--简单】手动安装【稍微复杂一丢丢…

【ruby on rails】M1遇到的一些安装问题

1. homebrew位置变了 原来的Cellar Homebrew Caskroom 都是在 /usr/local/下面 M1在/opt/homebrew下面 2. 装ruby M1电脑安装ruby&#xff0c;装不上的问题 RUBY_CFLAGS"-w" rbenv install 2.7.43. 装puma报错 gem install puma -v 5.5.2 -- --with-openssl-dir…

AtcoderABC311场

A - First ABCA - First ABC 题目大意 需要找到满足条件的最小字符数量。条件是字符串S中出现了A、B和C各至少一次。 思路分析 可以使用一个unordered_set来存储已经出现的字符&#xff0c;每次遍历字符串S时&#xff0c;将字符加入集合中。当集合中的元素数量达到3时&#…

Python元祖及字典小练习

目录 1. 元组 2. 字典 资料获取方法 1. 元组 和list的区别&#xff0c;不能改变元组中的对象&#xff0c;对象里面的值可以修改 元组中包含容器类&#xff08;列表&#xff0c;字典等&#xff09; 如果直接&#xff08;&#xff09;是数学运算&#xff0c; 如果&#xf…

爬虫002_python程序的终端运行_文件运行_ipython的使用---python工作笔记020

用python运行一个文件,就是要写一个.py结尾的文件 然后保存 然后直接cmd中,python 然后写上py文件的路径就可以了 然后看一下内容 看一下终端中运行,直接输入python进入python环境,然后写python代码 回车运行 退出可以用exit()

LiveGBS流媒体平台GB/T28181功能-视频直播流媒体平台分屏展示设备树分组树记录上次分屏播放记录

LiveGBS视频直播流媒体平台分屏展示设备树分组树记录上次分屏播放记录 1、分屏展示1.1、单屏1.2、四分屏1.3、九分屏1.4、十六分屏 2、分屏记录3、搭建GB28181视频直播平台 1、分屏展示 LiveGBS分屏页面支持&#xff0c;多画面播放&#xff0c;支持单屏、四分屏、九分屏、十六…

【无标题】机器学习常识阅读笔记

原博客链接&#xff1a;https://blog.csdn.net/minfanphd/category_12328466.html 总共24篇博客内容&#xff0c;最近两天集种看了一遍。小有收获&#xff0c;了解了一些机器学习的概念。大部分概念原来听过&#xff0c;但是有些概念还是第一次见。比如U-Net&#xff0c;多示例…

使用IDEA打jar包的详细图文教程

1. 点击intellij idea左上角的“File”菜单 -> Project Structure 2. 点击"Artifacts" -> 绿色的"" -> “JAR” -> Empty 3. Name栏填入自定义的名字&#xff0c;Output ditectory 选择 jar 包目标目录&#xff0c;Available Elements 里右击…

vue中预览静态pdf文件

方法 // pdf预览 viewFileCompare() { const pdfUrl "/static/wjbd.pdf"; window.open(pdfUrl); }, // 下载 downloadFile(){ var a document.createElement("a"); a.href "/static/wjbd.pdf"; a.…

王道考研数据结构第二章知识点总结

2.1 线性表的定义和基本操作 2.2.1 顺序表的定义 2.2.1 顺序表的插入删除 注意&#xff1a;插入操作中插入一个元素是从后往前移(从最后一个开始处理)&#xff1b;而删除操作中是从前往后移(从需要删除的后一个开始移)。 2.2.2 顺序表的查找 2.3.1 单链表的定义 2.3.2-1 单链…

CCLINK转profinet网关cclink转modbus

在工业自动化领域&#xff0c;不同品牌的PLC控制系统之间的数据交互是一个重要的问题。比如说&#xff0c;如果我们需要将三菱PLC的数据和西门子PLC的数据进行交互&#xff0c;我们应该如何处理呢&#xff1f;在这方面&#xff0c;捷米的JM-PN-CCLK通讯网关为我们提供了一种解决…

Python中的Cookie模块有什么功能

什么是Cookie Cookie&#xff08;或HTTP Cookie&#xff09;是一小段由Web服务器发送到Web浏览器并保存在用户本地计算机上的数据。它通常用于跟踪和识别用户的会话信息&#xff0c;以提供个性化的用户体验。 Cookie的工作原理 1. 当用户访问一个网站时&#xff0c;服务器会生…