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

news2024/11/19 15:26:49

前言

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

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

相关文章

elementui中 表格使用树形数据且固定一列时展开子集移入时背景色不全问题(父级和子级所展示的字段是不一样的时候)

原来的效果 修改后实现效果 解决- 需要修改elementui的依赖包中lib/element-ui.common.js中的源码 将js中此处代码改完下面的代码 watch: {// dont trigger getter of currentRow in getCellClass. see https://jsfiddle.net/oe2b4hqt/// update DOM manually. see https:/…

【单片机】STM32F070F6P6 开发指南(一)STM32建立HAL工程

文章目录 一、基础入门二、工程初步建立三、HSE 和 LSE 时钟源设置四、时钟系统&#xff08;时钟树&#xff09;配置五、GPIO 功能引脚配置六、配置 Debug 选项七、生成工程源码八、生成工程源码九、用户程序下载 一、基础入门 f0 pack下载&#xff1a; https://www.keil.arm…

关于XtremIO 全闪存储维护的一些坑(建议)

XtremIO 是EMC过去主推的一款全闪存储系统&#xff0c;号称性能小怪兽&#xff0c;对付那些对于性能要求极高的业务场景是比较合适的&#xff0c;先后推出了1代和2代产品&#xff0c;目前这个产品好像未来的演进到了PowerStor或者PowerMax全闪&#xff0c;应该不独立发展这个产…

Leetcode260

260. 只出现一次的数字 III - 力扣&#xff08;LeetCode&#xff09; class Solution {public int[] singleNumber(int[] nums) {//通过异或操作,使得最终结果为两个只出现一次的元素的异或值int filterResult 0;for(int num:nums){filterResult^num;}//计算首个1(从右侧开始)…

[JDK工具-6] jmap java内存映射工具

文章目录 1. 介绍2. 主要选项3. 生成java堆转储快照 jmap -dump4. 显示堆详细信息 jmap -heap pid5. 显示堆中对象统计信息 jmap -histo pid jmap(Memory Map for Java) 1. 介绍 位置&#xff1a;jdk\bin 作用&#xff1a; jdk安装后会自带一些小工具&#xff0c;jmap命令(Mem…

渗透工具CobaltStrike工具的下载和安装

一、CobalStrike简介 Cobalt Strike(简称为CS)是一款基于java的渗透测试工具&#xff0c;专业的团队作战的渗透测试工具。CS使用了C/S架构&#xff0c;它分为客户端(Client)和服务端(Server)&#xff0c;服务端只要一个&#xff0c;客户端可有多个&#xff0c;多人连接服务端后…

模型蒸馏笔记

文章目录 一、什么是模型蒸馏二、如何蒸馏三、实践四、参考文献 一、什么是模型蒸馏 Hinton在NIPS2014提出了知识蒸馏&#xff08;Knowledge Distillation&#xff09;的概念&#xff0c;旨在把一个大模型或者多个模型ensemble学到的知识迁移到另一个轻量级单模型上&#xff0…

Intel HDSLB 高性能四层负载均衡器 — 基本原理和部署配置

目录 文章目录 目录前言HDSLB-DPVS 的基本原理LVSDPDKDPVSHDSLB-DPVS HDSLB 的部署配置硬件要求软件要求编译安装 DPDK编译安装 HDSLB-DPVS配置大页内存配置网卡配置 HDSLB-DPVS启动 HDSLB-DPVS 测试 HDSLB-DPVS Two-arm Full-NAT 模式问题分析最后 前言 在上一篇《Intel HDSL…

[LLM]从GPT-4o原理到下一代人机交互技术

一 定义 GPT-4o作为OpenAI推出的一款多模态大型语言模型&#xff0c;代表了这一交互技术的重要发展方向。 GPT-4o是OpenAI推出的最新旗舰级人工智能模型&#xff0c;它是GPT系列的一个重要升级&#xff0c;其中的"o"代表"Omni"&#xff0c;中文意思是“全…

民宿bug

前端 后端 1 订单管理 订单日期已过&#xff0c;状态没有变成已完成

xgboost项目实战-保险赔偿额预测与信用卡评分预测001

目录 算法代码 原理 算法流程 xgb.train中的参数介绍 params min_child_weight gamma 技巧 算法代码 代码获取方式&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1QV7nMC5ds5wSh-M9kuiwew?pwdx48l 提取码&#xff1a;x48l 特征直方图统计&#xff1a; fig, …

Advanced Installer 问题集锦

1、界面在主题中显示的图标&#xff0c;如logo、发布者名称、产品名称就算在设计界面时删除&#xff0c;但是下次打开工程依然存在 解决办法&#xff1a;“可见”属性设置为禁用 2、在不关闭软件的情况下&#xff0c;使用"文件->打开"来切换项目&#xff0c;再次…

我让gpt4o给我推荐了一千多次书 得到了这些数据

事情是这样的&#xff0c;我们公司不是有个读书小组嘛&#xff0c;但是今年大家都忙于工作&#xff0c;忽视了读书这件事&#xff0c;所以我就想着搞个群机器人&#xff0c;让它明天定时向群里推荐一本书&#xff0c;用来唤起大家对读书的兴趣。但在调试的过程中就发现gpt4o老喜…

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

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

(已开源-ICRA2023) High Resolution Point Clouds from mmWave Radar

本文提出了一种用于生成高分辨率毫米波雷达点云的方法&#xff1a;RadarHD&#xff0c;端到端的神经网络&#xff0c;用于从低分辨率雷达构建类似激光雷达的点云。本文通过在大量原始雷达数据上训练 RadarHD 模型&#xff0c;同时这些雷达数据有对应配对的激光雷达点云数据。本…

Vue3实战笔记(37)—粒子特效登录页面

文章目录 前言一、粒子特效登录页总结 前言 上头了&#xff0c;再来一个粒子特效登录页面。 一、粒子特效登录页 登录页&#xff1a; <template><div><vue-particles id"tsparticles" particles-loaded"particlesLoaded" :options"…

ML307R OpenCPU GPIO使用

一、GPIO使用流程图 二、函数介绍 三、GPIO 点亮LED 四、代码下载地址 一、GPIO使用流程图 这个图是官网找到的&#xff0c;ML307R GPIO引脚电平默认为1.8V&#xff0c;需注意和外部电路的电平匹配&#xff0c;具体可参考《ML307R_硬件设计手册_OpenCPU版本适用.pdf》中的描…

MLM之CogVLM2:CogVLM2(基于Llama-3-8B-Instruct 模型进行微调)的简介、安装和使用方法、案例应用之详细攻略

MLM之CogVLM2&#xff1a;CogVLM2(基于Llama-3-8B-Instruct 模型进行微调)的简介、安装和使用方法、案例应用之详细攻略 目录 CogVLM2的简介 1、更新日志 2、CogVLM2 系列开源模型的详细信息 3、Benchmark 4、项目结构 5、模型协议 CogVLM2的安装和使用方法 1、模型微调…

智慧社区管理系统:打造便捷、安全、和谐的新型社区生态

项目背景 在信息化、智能化浪潮席卷全球的今天&#xff0c;人们对于生活品质的需求日益提升&#xff0c;期待居住环境能与科技深度融合&#xff0c;实现高效、舒适、安全的生活体验。在此背景下&#xff0c;智慧社区管理系统应运而生&#xff0c;旨在借助现代信息技术手段&…