微服务下认证授权框架的探讨

前言

市面上关于认证授权的框架已经比较丰富了,大都是关于单体应用的认证授权,在分布式架构下,使用比较多的方案是--<应用网关>,网关里集中认证,将认证通过的请求再转发给代理的服务,这种中心化的方式并不适用于微服务,这里讨论另一种方案--<认证中心>,利用jwt去中心化的特性,减轻认证中心的压力,有理解错误的地方,欢迎拍砖,以免误人子弟,有点干货,但是不多

image

需求背景

一个项目拆分为若干个微服务,根据业务形态,大致分为以下几种工程1.纯前端应用示例,一个简单的H5活动页面,商户仅仅需要登录,就可以参与活动2.前后端分离应用示例,如xxx后台,xxxApi,由一个前端项目+一个后端项目组成3.客户端应用示例,控制台项目,如任务调度,挂机服务现在有N个项目,每个项目又由N个微服务组成,微服务之间需要一套统一的权限管理,它需要同时满足商户(客户)在多个项目间无感切换,也需要满足开发者应用之间调用的认证授权示例,xxx开放平台,一般有两个角色,商家和开发者, 开发者创建应用,研发,上线应用, 商家申请应用,使用应用开发者A,注册成为xxx开放平台的开发者,创建了一个测试应用,测试应用依赖其它应用的某些能力(如,短信,短链....),申请获得这些能力后,开发完成,将测试应用发布到应用市场,商家B,申请开通了测试应用和XXX应用,它可以无感的在两个应用间切换(单点登录)

OAuth2.0

OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。......资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。OAuth 2.0 规定了四种获得令牌的流程。你可以选择最适合自己的那一种,向第三方应用颁发令牌。下面就是这四种授权方式。

  • 授权码(authorization-code)

  • 隐藏式(implicit)

  • 密码式(password)

  • 客户端凭证(client credentials)

image

演示效果

  1. https://localhost:6201 认证中心

  2. https://localhost:9001 应用A implicit模式

  3. https://localhost:9002 应用B implicit模式

  4. https://localhost:9003 应用C authorization-code模式

解决的问题

  1. 单点登录

  2. 单点退出

  3. 统一登录中心(通行证)

  4. 用户身份鉴权

  5. 服务的最小作用域为api

找个靠谱点的开源认证授权框架

在.net里,比较靠前的两个框架(IdentityServer4,OpenIddict),这两个都实现了OAuth2.0,相较而言对IdentityServer4更加熟悉点,就基于这个开始了,顺便扫盲,听说后面不开源了,不过对于我来说并没有影响,现有的功能已经完全够用了

IdentityServer4 网上的资料非常多,稍微爬点坑就能搭建起来,并将OAuth2.0的4种认证模式都体验一遍,这里就不多介绍了,这里强烈推荐Skoruba.IdentityServer4.Admin 这个开源项目,方便熟悉ids4里的各种配置,有助于理解

踏坑第一步,弄个自定义的登录页面

把数据持久化到数据库,登录用的是Identity,这个可以根据自己的需求自行拓展,不用也行,我这里还是用的原来的表,只是重写了登录逻辑,方便后面拓展更多的登录方式,看着挺简单,其实一点也不复杂

/// <summary>
/// 登录
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Login(LoginRequest model)
{model.ReturnUrl = model.ReturnUrl ?? "/";var user = await _context.Users.FirstOrDefaultAsync(m => m.UserName == model.UserName && m.PasswordHash == model.Password.Sha256());if (user != null) {AuthenticationProperties props = new AuthenticationProperties{IsPersistent = true,ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(1))};Claim[] claim = new Claim[] {new Claim(ClaimTypes.Role, "admin"),new Claim(ClaimTypes.Name, user.UserName),new Claim(ClaimTypes.MobilePhone, user.PhoneNumber ?? "-"),new Claim("userId", user.Id),new Claim("phone",user.PhoneNumber ?? "-")};await HttpContext.SignInAsync(new IdentityServer4.IdentityServerUser(user.Id) { AdditionalClaims = claim }, props);return Ok(Model.Response.JsonResult.Success(message:"登录成功",returnUrl: model.ReturnUrl));}return Ok(Model.Response.JsonResult.Error(message: "登录失败", returnUrl: model.ReturnUrl));
}

@{Layout = null;
}
<body><div class="login-container"><h2>登录</h2><form id="myForm"><label for="username">用户名:</label><input type="text" id="userName" name="userName" value="test" required><label for="password">密码:</label><input type="password" id="password" name="password" value="123456" required><button type="submit">登录</button></form></div></body>
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.unobtrusive-ajax.js"></script>
<script>document.getElementById("myForm").addEventListener("submit", function (event) {event.preventDefault(); // 阻止表单默认提交行为var inputs = document.querySelectorAll("form input[required]");var hasError = false;// 遍历所有required的input元素inputs.forEach(function (input) {if (input.checkValidity() === false) {// 如果验证失败,标记错误并阻止AJAX请求input.classList.add("error"); // 你可以添加一个错误样式hasError = true;} else {input.classList.remove("error"); // 清除错误样式}});if (!hasError) {// 如果没有错误,执行AJAX请求performAjaxRequest();}});function performAjaxRequest() {const urlParams = new URLSearchParams(window.location.search);const returnUrl = urlParams.get('ReturnUrl') || '';let param = {"userName": $("#userName").val(),"password": $("#password").val(),"returnUrl": returnUrl}$.post("/account/login", param, function (data) {console.log(data)if (data.code != "0") {alert(data.message)} else {window.location.href = data.returnUrl;}})}
</script><style>body {font-family: Arial, sans-serif;background-color: #f0f2f5;display: flex;justify-content: center;align-items: center;height: 100vh;margin: 0;}.login-container {background-color: white;padding: 20px;border-radius: 5px;box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);}input[type="text"], input[type="password"] {width: 100%;padding: 10px;margin-bottom: 15px;border: 1px solid #ddd;border-radius: 3px;}button {width: 100%;padding: 10px;background-color: #007bff;color: white;border: none;border-radius: 3px;cursor: pointer;}button:hover {background-color: #0056b3;}</style>

踏坑第二步,单点登录

implicit这个网上有示例,照着抄就可以了,基本没有坑

var config = {authority: "https://localhost:6201",client_id: "3",redirect_uri: "https://localhost:9001/callback.html",//这里别写错response_type: "id_token token",post_logout_redirect_uri: "https://localhost:9001/logout.html",scope: "openid profile api" //范围一定要写,不然access_token访问资源会401
};

    <script src="/js/oidc-client.js"></script><script src="/js/config.js"></script><script>mgr.signinRedirectCallback().then(function () {window.location = "/index.html";}).catch(function (e) {console.log(e);});</script>

client_credentials

这个有大坑,网上90%的文档都是错的,然后抄来抄去,或者说我的oidc-client.js 版本不对,这里要加入点自己的理解

var config = {authority: "https://localhost:6201",client_id: "20231020001",redirect_uri: "https://localhost:9003/signin-oidc.html",//这里别写错,response_type: "code",post_logout_redirect_uri: "https://localhost:9003/logout.html",scope: "openid offline_access api testScope" //范围一定要写,不然access_token访问资源会401
};

对比这两个模式,验证码模式返回的是code,并不是access_token,所以还用上面的回调页面,肯定报错,熟悉OAuth2.0的同学,都知道缺少一个通过code换取access_token步骤,这里我们从新写回调页面,核心代码就是获取url上的code,然后换取access_token,再将凭证信息写入到缓存

var urlParams = getURLParams();let url = "https://localhost:5002/api/authorization_code";var param = {...urlParams,"redirect_uri":config.redirect_uri}console.log(url)$.post(url,param,function(data){console.log(data)if(data.code != "0"){alert(data.message)}else{let user = new User(data.data);console.log(user)mgr.storeUser(user).then(function(e){window.location.href="https://localhost:9003"})}})function getURLParams() {const searchURL = location.search; // 获取到URL中的参数串const params = new URLSearchParams(searchURL);    const valueObj = Object.fromEntries(params); // fromEntries是es10提出来的方法polyfill和babel都不转换这个方法return valueObj;}

真正的坑点在oidc-client.js写入凭证,各种GPT提问,最终弄出来,再弄不出来,我就要考虑手动写入缓存了,但是为了单点登录里统一管理凭证,还是选择用oidc-client.js内置的方法

//重新定义用户对象var User = function () {function User(_ref) {var id_token = _ref.id_token,session_state = _ref.session_state,access_token = _ref.access_token,token_type = _ref.token_type,scope = _ref.scope,profile = _ref.profile,expires_at = _ref.expires_in,state = _ref.state;this.id_token = id_token;this.session_state = session_state;this.access_token = access_token;this.token_type = token_type;this.scope = scope;this.profile = profile;this.expires_at = expires_at;this.state = state;}User.prototype.toStorageString = function toStorageString() {return JSON.stringify({id_token: this.id_token,session_state: this.session_state,access_token: this.access_token,token_type: this.token_type,scope: this.scope,profile: this.profile,expires_at: this.expires_at});};User.fromStorageString = function fromStorageString(storageString) {return new User(JSON.parse(storageString));};return User;
}();

踏坑第三步,单点退出

不出意外,肯定是有坑的,细心的同学已经发现应用C,单点退出失败了,我们来盘一下这里的逻辑在ids4里面,客户端会配置两个退出通道,FrontChannelLogoutUri(前端退出通道),BackChannelLogoutUri(后端退出通道),怎么调用这个取决于项目,我们这里主要是web项目,所以配置前端退出通道就可以了,实现也很简单,应用退出的时候,重定向到认证中心的统一退出页面,认证中心退出成功后,再使用iframe调用其它应用配置的前端退出通道

统一退出流程图

image

public async Task<IActionResult> Logout(string logoutId)
{await _signInManager.SignOutAsync();var refererUrl = Request.Headers["Referer"].ToString();if (string.IsNullOrEmpty(refererUrl)) {refererUrl = "/account/login";}var frontChannelLogoutUri = await _configDbContext.Clients.AsNoTracking().Where(m => m.Enabled).Where(m=>!string.IsNullOrEmpty(m.FrontChannelLogoutUri)).Select(m=>m.FrontChannelLogoutUri).ToListAsync();ViewBag.FrontChannelLogoutUri = frontChannelLogoutUri;ViewBag.RefererUrl = refererUrl;return View();
}

回到前面应用C没有正常退出的原因,仔细观察,原来oidc-client.js默认的存储策略是将凭证存储在SessionStorage,在浏览器里每个页签的SessionStorage都是独立的,所以iframe里调用退出页面,是无法清除当前页面的凭证的,解决方案就是修改oidc-client.js默认的存储策略,改为LocalStorage,问题解决

class LocalStorageStateStore extends Oidc.WebStorageStateStore {constructor() {super(window.localStorage);}
}//配置信息
var config = {...userStore: new LocalStorageStateStore({ store: localStorage })...
};

踏坑第四步,访问受保护的资源

客户端拿到了access_token,只要客户端包含对应的作用域,就能访问对应的api,不出意外,这里肯定要出点幺蛾子,前面都是铺垫,好戏才刚刚开始问题出在作用域上,同一个客户端,配置了client credentials 与 authorization-code,它们获取的作用域是不一样的,这里对应不同的场景authorization-code 这里涉及到登录,那么作用域一般包含openId,phone.... 用户身份相关的信息,属于前端调用,access_token对用户可见,这里我用前端作用域代替,且作用域必须显示声明(也就是在前端配置文件里写死,可以翻翻上面的config里scope属性)client credentials 不涉及登录,可以理解成后端调用,access_token对用户不可见,这里我用后端作用域代替

那它们的意义(粒度)也是完全不同的,作用域可以有多种用途,所以通过authorization-code获取的access_token,不能直接访问受保护的资源,而是应该调用它的后端服务,这里作用域的意义是指服务本身,config.scope = 'openId a.api b.api',然后再通过凭证里携带的用户身份标识,做具体接口的鉴权通过client credentials获取的access_token,它的作用域意义是指资源服务的具体api,这里我画了个图,便于理解

image

文章转载自:提伯斯

原文链接:https://www.cnblogs.com/tibos/p/18208102

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

【数据库基础-mysql详解之索引的魅力(N叉树)】

索引的魅力目录 &#x1f308;索引的概念&#x1f308;使用场景&#x1f308;索引的使用&#x1f31e;&#x1f31e;&#x1f31e;查看MySQL中的默认索引&#x1f31e;&#x1f31e;&#x1f31e;创建索引&#x1f31e;&#x1f31e;&#x1f31e;删除索引 站在索引背后的那个男…

sheng的学习笔记-docker部署Greenplum

目录 docker安装gp数据库 mac版本 搭建gp数据库 连接数据库 windows版本 搭建gp数据库 连接数据库 docker安装gp数据库 mac版本 搭建gp数据库 打开终端&#xff0c;输入代码&#xff0c;查看版本 ocker search greenplum docker pull projectairws/greenplum docker…

Virtual Box安装Ubuntu及设置

Virtual Box安装Ubuntu及设置 本文包含以下内容&#xff1a; 使用Virtual Box安装Ubuntu Desktop。设置虚拟机中的Ubuntu&#xff0c;使之可访问互联网并可通过SSH访问。 Ubuntu Desktop下载 从官网下载&#xff0c;地址为&#xff1a;Download Ubuntu Desktop | Ubuntu U…

HTTP交互导致ECONNABORTED的原因之一

背景&#xff1a; 本次记录的&#xff0c;是一次使用HTTP交互过程中遇到的问题&#xff0c;问题不大&#xff0c;就是给题目上这个报错补充一种可能的解决方案。 程序大致流程&#xff1a; 1. 设备向服务器A请求信息 2. 拿到回复记录下回复内容中的数据包下载地址等信息 3…

games 101 作业4

games 101 作业4 题目题解作业答案 题目 Bzier 曲线是一种用于计算机图形学的参数曲线。在本次作业中&#xff0c;你需要实 现 de Casteljau 算法来绘制由 4 个控制点表示的 Bzier 曲线 (当你正确实现该 算法时&#xff0c;你可以支持绘制由更多点来控制的 Bzier 曲线)。 你需…

IntelliJ IDEA实用插件:轻松生成时序图和类图

IntelliJ IDEA生成时序图、类图 一、SequenceDiagram1.1 插件安装1.2 插件设置1.3 生成时序图 二、PlantUML Integration2.1 插件安装2.2 插件设置2.3 生成类图 在软件建模课程的学习中&#xff0c;大家学习过多种图形表示方法&#xff0c;这些图形主要用于软件产品设计。在传统…

C++实现定长内存池

项目介绍 本项目实现的是一个高并发的内存池&#xff0c;它的原型是Google的一个开源项目tcmalloc&#xff0c;tcmalloc全称Thread-Caching Malloc&#xff0c;即线程缓存的malloc&#xff0c;实现了高效的多线程内存管理&#xff0c;用于替换系统的内存分配相关函数malloc和fr…

Java面向对象知识总结+思维导图

&#x1f516;面向对象 &#x1f4d6; Java作为面向对象的编程语言&#xff0c;我们首先必须要了解类和对象的概念&#xff0c;本章的所有内容和知识都是围绕类和对象展开的&#xff01; ▐ 思维导图1 ▐ 类和对象的概念 • 简单来说&#xff0c;类就是对具有相同特征的一类事…

【Spring】认识 IoC 容器和 Servlet 容器

认识 IoC 容器和 Servlet 容器 1.认识容器1.1 IoC 容器1.2 loC 的实现方法1.2.1 依赖注入1.2.2 依赖查找 1.3 认识 Servlet 容器 2.用 IoC 管理 Bean2.1 创建一个 Bean2.2 编写 User 的配置类2.3 编写测试类 3.用 Servlet 处理请求3.1 注册 Servlet 类3.2 开启 Servlet 支持 1.…

力扣:1738. 找出第 K 大的异或坐标值

1738. 找出第 K 大的异或坐标值 给你一个二维矩阵 matrix 和一个整数 k &#xff0c;矩阵大小为 m x n 由非负整数组成。 矩阵中坐标 (a, b) 的 值 可由对所有满足 0 < i < a < m 且 0 < j < b < n 的元素 matrix[i][j]&#xff08;下标从 0 开始计数&…

晶圆厂的PE转客户工程师前景怎么样?

知识星球&#xff08;星球名&#xff1a; 芯片制造与封测技术社区&#xff0c;星球号&#xff1a; 63559049&#xff09;里的学员问&#xff1a; 目前在晶圆厂做PE&#xff0c;倒班oncall压力太大把身体搞坏了&#xff0c;现在有一个design house的CE客户工程师的offer&…

跨境选品师不是神话:普通人也能轻松掌握,开启全球贸易新篇章!

随着互联网技术的飞速发展&#xff0c;跨境电商行业已成为全球经济的新增长点。在这个背景下&#xff0c;一个新兴的职业——跨境选品师&#xff0c;逐渐走进了人们的视野。那么&#xff0c;跨境选品师究竟是做什么的?普通人又该如何成为优秀的跨境选品师呢? 一、跨境选品师的…

ABAQUS应用07-实现拉伸和压缩刚度不同的弹簧建模

文章目录 0、背景描述1、步骤 0、背景描述 到目前为止&#xff0c;本文的内容我还没有具体实践过&#xff0c;但是个人认为后期是会用到的。比如说&#xff0c;对于风电机组地基转动刚度的设置&#xff0c;土体就是一种拉压刚度并不相同的材料。所以现在先记录下来&#xff0c…

如何用Java实现SpringCloud Alibaba Sentinel的熔断功能?

在Java中使用Spring Cloud Alibaba Sentinel实现熔断功能的步骤如下&#xff1a; 添加依赖 在项目的pom.xml文件中添加Spring Cloud Alibaba Sentinel的依赖&#xff1a; <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud…

Java进阶学习笔记24——Object类

Object类: Object类是Java中所有类的祖宗类&#xff0c;因此&#xff0c;Java中所有类的对象都可以直接使用Object类中提供的一些方法。 所有类都是Object类的子孙类。 API文档&#xff1a; Object类的成员方法&#xff1a; Object类的常见方法&#xff1a; Student类&…

Arthas反编译与重新加载class

一、背景 因为其他研发部门同事给的产品jar包存在一个问题&#xff0c;就是http底层的超时时间默认为60s&#xff0c;但是最近调用外部接口同步数据&#xff0c;这个数据量太大导致超时超过60s&#xff0c;每次同步都不成功。但是客户目前对此情况特别不满意&#xff0c;需要紧…

勒索病毒的策略与建议

随着网络技术的快速发展&#xff0c;勒索病毒攻击成为全球范围内日益严重的网络安全威胁。勒索病毒通过加密用户文件或锁定系统来勒索赎金&#xff0c;给个人和企业带来了巨大的损失。因此&#xff0c;了解如何应对勒索病毒攻击至关重要。本文将概述一些有效的防范措施和应对策…

2024电工杯数学建模B题完整论文讲解(含每一问python代码+数据)

大家好呀&#xff0c;从发布赛题一直到现在&#xff0c;总算完成了2024电工杯数学建模B题大学生平衡膳食食谱的优化设计及评价完整的成品论文。 本论文可以保证原创&#xff0c;保证高质量。绝不是随便引用一大堆模型和代码复制粘贴进来完全没有应用糊弄人的垃圾半成品论文。 …

Linux网络编程:HTTPS协议

目录 1.预备知识 1.1.加密和解密 1.2.常见加密方式 1.2.1.对称加密 1.2.2.非对称加密 ​编辑 1.3.数据摘要&#xff08;数据指纹&#xff09;和数据签名 1.4.证书 1.4.1.CA认证 1.4.2.证书和数字签名 2.HTTPS协议 2.1.自行设计HTTPS加密方案 2.1.1.只使用对称加密 …

uniapp使用uni.chooseImage选择图片后对其是否符合所需的图片大小和类型进行校验

uni.chooseImage的返回值在H5平台和其他平台的返回值有所差异&#xff0c;具体差异看下图 根据图片可以看出要想判断上传的文件类型是不能直接使用type进行判断的&#xff0c;所以我使用截取字符串的形式来判断&#xff0c;当前上传图片的后缀名是否符合所需要求。 要求&#…