微服务下认证授权框架的探讨
前言
市面上关于认证授权的框架已经比较丰富了,大都是关于单体应用的认证授权,在分布式架构下,使用比较多的方案是--<应用网关>,网关里集中认证,将认证通过的请求再转发给代理的服务,这种中心化的方式并不适用于微服务,这里讨论另一种方案--<认证中心>,利用jwt去中心化的特性,减轻认证中心的压力,有理解错误的地方,欢迎拍砖,以免误人子弟,有点干货,但是不多
需求背景
一个项目拆分为若干个微服务,根据业务形态,大致分为以下几种工程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)
演示效果
-
https://localhost:6201 认证中心
-
https://localhost:9001 应用A implicit模式
-
https://localhost:9002 应用B implicit模式
-
https://localhost:9003 应用C authorization-code模式
解决的问题
-
单点登录
-
单点退出
-
统一登录中心(通行证)
-
用户身份鉴权
-
服务的最小作用域为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调用其它应用配置的前端退出通道
统一退出流程图
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,这里我画了个图,便于理解
文章转载自:提伯斯
原文链接:https://www.cnblogs.com/tibos/p/18208102
体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构
相关文章:

微服务下认证授权框架的探讨
前言 市面上关于认证授权的框架已经比较丰富了,大都是关于单体应用的认证授权,在分布式架构下,使用比较多的方案是--<应用网关>,网关里集中认证,将认证通过的请求再转发给代理的服务,这种中心化的方式并不适用于微服务,这里讨论另一种方案--<认证中心>,利用jwt去中…...

使用 ASM 修改字段类型,解决闪退问题
问题 我的问题是什么? 在桥接类 UnityBridgeActivity 中处理不同 unity 版本调用 mUnityPlayer.destroy(); 闪退问题。 闪退日志如: 闪退日志说在 UnityBridgeActivity中找不到类型为 UnityPlayer 的属性 mUnityPlayer。 我们知道,Android…...

【python】python社交交友平台系统设计与实现(源码+数据库)【独一无二】
👉博__主👈:米码收割机 👉技__能👈:C/Python语言 👉公众号👈:测试开发自动化【获取源码商业合作】 👉荣__誉👈:阿里云博客专家博主、5…...

Linux 实验报告3-4
(大家好,今天我们来学习Linux的相关知识,大家可以在评论区进行互动答疑哦~加油!💕) 目录 实验三 vi编辑器 一、实验目的 二、实验内容 三、主要实验步骤 实验报告 1.进入 vi。 2.建立一个文件&…...

网络安全之BGP详解
BGP;边界网关协议 使用范围;BGP范围,在AS之间使用的协议。 协议的特点(算法):路径矢量型,没有算法。 协议是否传递网络掩码:传递网络掩码,支持VLSM,CIDR …...
【MySQL精通之路】SQL优化(1)-查询优化(8)-嵌套联接优化
主博客: 【MySQL精通之路】SQL优化(1)-查询优化-CSDN博客 上一篇: 【MySQL精通之路】SQL优化(1)-查询优化(7)-嵌套循环联接-CSDN博客 下一篇: 【MySQL精通之路】SQL优化(1)-查询优化(9)-外部联接优化-CSDN博客 与SQL标准相比,…...
30V降8V、12V、24V3.5A车充降压芯片IC H4112 5V-30V
H4112确实是一款功能强大的异步降压型DC-DC转换器,它具备多种出色的特性和优势,使得它在电源管理领域有着广泛的应用。以下是对H4112主要特性和功能的详细解释: 内置30V耐压MOS: H4112内部集成了30V耐压的MOS管,这有…...

保护共享资源的方法(互斥锁)
我最近开了几个专栏,诚信互三! > |||《算法专栏》::刷题教程来自网站《代码随想录》。||| > |||《C专栏》::记录我学习C的经历,看完你一定会有收获。||| > |||《Linux专栏》࿱…...

树的非递归遍历(层序)
层序是采用队列的方式来遍历的 就比如说上面这颗树 他层序的就是:1 24 356 void LevelOrder(BTNode* root) {Que q;QueueInit(&q);if (root){QueuePush(&q, root);}while (!QueueEmpty(&q)){BTNode* front QueueFront(&q);QueuePop(&q);print…...
解决SpringBoot使用@Transactional进行RestTemplate远程调用导致查询数据记录为null的bug
开启事务过程中,如果远程调用查询当前已经开启但没有提交的事务,就会查不到数据。 示例代码 import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.transaction.annotation.Transactional; import o…...
pl/sql基础语法操作
oracle pl/sql语言(procedural language/sql)是结合了结构化查询与oracle自身过程控制为一体的强大语言。 语法执行块 语法结构: [ declare 可选 声明变量部分--declaration statements (1);]begin --执行部分--executable statements (2)…...
Vue 父组件向子组件传递数据
1、在子组件中,你需要声明你期望从父组件接收哪些props。这可以通过props选项完成,可以是一个数组或对象形式: export default {props: [message],props:{message:String }props: {message: String, // 类型检查count: {type: Nu…...

二十五、openlayers官网示例CustomOverviewMap解析——实现鹰眼地图、预览窗口、小窗窗口地图、旋转控件
官网demo地址: Custom Overview Map 这个示例展示了如何在地图上增加一个小窗窗口的地图并跟随着地图的旋转而旋转视角。 首先加载了一个地图。其中 DragRotateAndZoom是一个交互事件,它可以实现按住shift键鼠标拖拽旋转地图。 const map new Map({int…...
K8S Secret管理之SealedSecrets
1 关于K8S Secret 我们通常将应用程序使用的密码、API密钥保存在K8S Secret中,然后应用去引用。对于这些敏感信息,安全性是至关重要的,而传统的存储方式可能会导致密钥在存储、传输或使用过程中受到威胁,例如在git中明文存储密码…...
Gone框架介绍25 - Redis模块参考文档
文章目录 Redis 参考文档配置项import 和 bury使用分布是缓存 redis.Cache接口定义使用示例 使用分布式锁 redis.Locker接口定义使用示例 操作Key,使用 redis.Key接口定义 使用 Provider 注入 redis 接口使用示例 直接使用redis连接池接口定义使用示例 Redis 参考文…...
SpringBoot前置知识02-spring注解发展史
springboot前置知识01-spring注解发展史 spring1.x spring配置只能通过xml配置文件的方式注入bean,需要根据业务分配配置文件,通过import标签关联。 spring1.2版本出现Transactional注解 <?xml version"1.0" encoding"UTF-8"?> <be…...

C++ TCP发送Socket数据
DEVC需要加入ws2_32库 #include <iostream> #include <winsock2.h>#pragma comment(lib, "ws2_32.lib")void sendData(const char* ip, int port, const char* data) {WSADATA wsaData;SOCKET sockfd;struct sockaddr_in server_addr;// 初始化Winsock…...

鸿蒙HarmonyOS开发中的易混点归纳-持续补充中
相关文章目录 鸿蒙HarmonyOS开发术语全解:小白也能看懂! 文章目录 相关文章目录前言一、build()函数和Builder装饰器?二、自定义组件和系统组件(内置组件)三、组件和页面四、自定义弹窗和其他弹窗总结 前言 一、build…...

ue引擎游戏开发笔记(45)——添加游戏音效
1.需求分析: 截至目前,我们仍然在一个无声的世界游玩游戏,所以有必要为游戏增添一些声音,例如开火声,子弹撞击声等等。 2.操作实现: 1.这是一个较为简单的功能,类似特效的实现方法,…...

202472读书笔记|《首先你要快乐,其次都是其次》——快乐至上,允许一切发生
202472读书笔记|《首先你要快乐,其次都是其次》——快乐至上,允许一切发生 《首先你要快乐,其次都是其次》作者林小仙,挺轻松的小漫画,清新的文字。 生而为人,我很抱歉,大可不必。 生活已经很难…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...