2024年4月

1 微服务发展

微服务架构的发展伴随着互联网行业的飞速增长和技术的日新月异。起初,企业为了提升应用的灵活性和可维护性,开始尝试将单体应用拆分为多个服务,这便是面向服务的架构(SOA)的兴起。然而,此时的拆分粒度仍然相对较大,并没有完全实现服务的细粒度划分。

随着Docker和容器技术的兴起,微服务架构真正得到了发展的助力。容器技术为微服务提供了轻量级的隔离环境,使得微服务更容易部署和管理。
每个微服务都可以独立运行、扩展和更新,大大提高了系统的灵活性和可维护性。

进入云原生时代,伴随着Kubernetes等技术的兴起,微服务架构得到了更为完善的支撑。云原生技术为微服务提供了自动化部署、管理和监控的能力,进一步推动了微服务架构的广泛应用

image

2 微服务有哪些特点

微服务提倡将
单一应用程序划分成一组松散耦合的细粒度小型服务,辅助轻量级的协议,互相协调、互相配合,为用户提供最终价值。
所以,微服务(或微服务架构)是一种云原生架构方法,其中单个应用程序由许多松散耦合且可独立部署的较小组件或服务组成。这些服务通常包含如下特点:

2.1 单一职责

微服务架构中的每个节点高度服务化,都是具有业务逻辑的,符合高内聚、低耦合原则以及单一职责原则的单元,包括数据库和数据模型;不同的服务通过“管道”的方式灵活组合,从而构建出庞大的系统。
原来的单体系统逐渐演变成具有单一职责的细粒度服务。
image

2.2 轻量级通信

通过REST API模式或者RPC框架,实现服务间互相协作的轻量级通信机制。参考作者这一篇《
微服务通信之RPC

  • 性能佳
  • 稳定性高
  • 安全性好

image

2.3 独立性

在微服务架构中,每个服务都是独立的业务单元,与其他服务高度解耦,只需要改变当前服务本身,就可以完成独立的开发、测试、部署、运维。
image

2.4 进程隔离

在微服务架构中,应用程序由多个服务组成,每个服务都是高度自治的独立业务实体,可以运行在独立的进程中,不同的服务能非常容易地部署到不同的主机上,实现高度自治和高度隔离。
进程的隔离,还能保证服务达到动态扩缩容的能力,业务高峰期自动增加服务资源以提升并发能力,业务低谷期则可自动释放服务资源以节省开销。

2.5 混合技术栈和混合部署方式

团队可以为不同的服务组件使用不同的技术栈和不同的部署方式(公有云、私有云、混合云)。

2.6 简化治理

组件可以彼此独立地进行扩缩容和治理,从而减少了因必须缩放整个应用程序而产生的浪费和成本,因为单个功能可能面临过多的负载。

2.7 安全可靠,可维护。

从架构上对运维提供友好的支撑,在安全、可维护的基础上规范化发布流程,支持数据存储容灾、业务模块隔离、访问权限控制、编码安全检测等。

3 微服务架构适用的场景

微服务架构的适用场景广泛,尤其在以下情况下表现尤为突出:

1. 业务复杂,模块多且相对独立
:当业务复杂到单体应用难以维护时,将应用拆分为多个微服务是一个明智的选择。每个微服务专注于一个业务领域,实现业务的高度解耦和快速迭代。
2. 团队多,管理隔离
:随着公司规模的扩大,团队数量也在不断增加。每个团队都有自己的管理方式和负责的业务领域。微服务架构可以实现团队自治,提高开发效率。
3. 应用规模大,并发用户多
:微服务架构可以横向分布式扩展,轻松应对应用规模的不断扩大和海量用户增长。
4. 快速迭代、持续交付
:在业务需求快速变化的情况下,微服务架构可以实现快速的开发、测试和部署,支持持续交付和持续集成。

4 微服务架构的优势和挑战

4.1 微服务架构的优势

4.1.1
易于开发和维护

由于每个微服务都是独立的,开发团队可以专注于自己的服务,从而更容易进行开发和维护。

4.1.2
启动速度快

与单体应用相比,微服务的启动速度更快,因为只需要启动所需的服务,而不是整个应用。

4.1.3
局部修改容易部署

当某个服务出现问题或需要更新时,只需要针对该服务进行修改和部署,而不需要重新部署整个应用。

4.1.4
技术栈灵活

每个微服务都可以使用最适合的技术栈进行开发,从而可以充分利用最新的技术和工具。

4.1.5
易于扩展

每个微服务都可以独立进行扩展,从而可以根据需要灵活地调整系统的性能和资源使用。

4.1.6
独立运行和扩展

每个微服务都可以独立地运行和扩展,使得系统能够很容易地水平扩展以处理更多的负载。

4.1.7
提高可用性

通过将应用程序分解为多个小而自治的服务,可以降低单点故障的风险,提高整个系统的可用性。

4.2 微服务架构面临的挑战

4.2.1
分布式系统的固有复杂性

微服务架构是基于分布式的系统,而构建分布式系统必然会带来额外的开销。

  • 性能: 分布式系统是跨进程、跨网络的调用,
    受网络延迟和带宽的影响。
  • 可靠性: 由于高度依赖于网络状况,
    任何一次的远程调用都有可能失败,随着服务的增多还会出现更多的潜在故障点。
    此,如何提高系统的可靠性、降低因网络引起的故障率,是系统构建的一大挑战。
  • 分布式通信: 分布式通信大大增加了功能实现的复杂度,并且
    伴随着定位难、调试难等问题
  • 数据一致性: 需要保证分布式系统的数据强一致性,即
    在 C(一致性)A(可用性)P(分区容错性) 三者之间做出权衡。
    块可以参考我的这篇《
    分布式事务
    》。
  • 安全性问题:微服务架构涉及多个服务之间的网络通信,存在数据泄露、劫持等安全风险,需要实施适当的安全措施。

4.2.2
服务的依赖管理和测试

在单体应用中,通常使用集成测试来验证依赖是否正常。而在微服务架构中,服务数量众多,每个服务都是独立的业务单元,服务主要通过接口进行交互,如何保证它的正常,是测试面临的主要挑战。
所以单元测试和单个服务链路的可用性非常重要。

4.2.3
有效的配置版本管理

在单体系统中,配置可以写在yaml文件,分布式系统中需要统一进行配置管理,同一个服务在不同的场景下对配置的值要求还可能不一样,所以需要引入配置的版本管理、环境管理。

4.2.4
自动化的部署流程

在微服务架构中,每个服务都独立部署,交付周期短且频率高,人工部署已经无法适应业务的快速变化。有效地构建自动化部署体系,配合服务网格、容器技术,是微服务面临的另一个挑战。

4.2.5
对于DevOps更高的要求

在微服务架构的实施过程中,开发人员和运维人员的角色发生了变化,开发者也将承担起整个服务的生命周期的责任,包括部署、链路追踪、监控;因此,按需调整组织架构、构建全功能的团队,也是一个不小的挑战。

1.2.6
运维成本

运维主要包括配置、部署、监控与告警和日志收集四大方面。微服务架构中,每个服务都需要独立地配置、部署、监控和收集日志,成本呈指数级增长。服务化粒度越细,运维成本越高。

怎样去解决这些问题,是微服务架构必须面临的挑战。

5 什么时候需要微服务化?

作为一线大厂架构师,我们也承担着很多旧系统改造的工作,在判断是否使用微服务架构时,以下是一些可能的参考:
1. 流量和并发量
当系统的
流量或并发量达到一定的阈值,比如日活跃用户数量超过百万,或者每秒请求数(QPS)达到数千甚至更高时,传统的单体架构可能难以支撑如此高的负载
。此时,将系统拆分为微服务可以提高系统的吞吐量和响应速度。

2. 迭代频率
如果
业务需求变更非常频繁,例如每周两次以上甚至每天都有新的功能需要上线
,那么微服务架构的灵活性将更加适合这种快速迭代的需求。每个微服务可以独立进行开发、测试和部署,这将大大缩短新功能上线的周期。

3. 系统扩展需求
当系统需要快速扩展以满足业务增长时,微服务架构可以更容易地实现水平扩展。
通过增加更多的服务实例或部署到更多的服务器上,可以线性地提高系统的处理能力。如果预计未来几年内系统需要大幅度扩展,那么使用微服务架构可能是一个明智的选择。

4. 耦合度和依赖关系
如果
旧系统中的模块之间存在高度的耦合和复杂的依赖关系,这可能导致维护和升级变得困难
。通过微服务架构,可以将这些模块拆分为独立的服务,降低耦合度,减少依赖关系,从而提高系统的可维护性和可扩展性。

5. 故障隔离和容错能力
如果系统中的某个模块出现故障,是否会影响到整个系统的正常运行?如果答案是肯定的,那么使用
微服务架构可以提高系统的容错能力。每个微服务都可以独立运行和故障隔离
,一个服务的故障不会影响到其他服务的正常运行。

需要注意的是,
贸然拆分成为服务架构,反而可能导致服务间访问效率变低、服务间调用的可靠性变低、故障问题定位慢、更高的数据一致性保障、很高的机器资源(物力)和运维(人力)成本。所以,适合的才是对的,需要平衡各方面利弊再做决定。

本文收录于
Github.com/niumoo/JavaNotes
,Java 系列文档,数据结构与算法!
本文收录于网站:
https://www.wdbyte.com/
,我的公众号:
程序猿阿朗

引言

想象一下,周五晚上,你打开电脑,打算刷一刷最新上线的剧集,突然弹出网站登录,哎呀,那个超级复杂的密码是什么来着?那一堆数字、字母和符号的大杂烩在我脑海中有好几个版本?能不能有一种简单的方式,不用密码就可以认真登录,这简直不要太棒?这时
扫码登录
出现了,它不仅方便而且更加安全。好比你向安保亮了一下你的 VIP 通行证,便放你通过。

微信作为国民级应用,
微信扫码登录
再常见不过了,它就像你的口袋里的万能钥匙,去哪儿都不怕。不过微信扫码登录也有多种方式,如扫码授权登录,扫码关注公众号登录等。
这篇文章一起聊聊微信公众号二维码登录是怎么回事,它的工作流程是什么,它怎么保证你的身份安全。以及,如果你是一个开发者,如何在自己的网站上增加扫码登录。

公众号扫码登录优势

快捷方便

用户只需打开微信扫一扫,几秒钟内就能完成登录。这简化了传统的输入用户名和密码的繁琐过程,不过前提是你已经安装了必要的 APP。

增强安全性

扫码登录的身份认证在服务端完成,而且如微信公众号扫码登录这种方式,网站只需要拿到一个用户身份标识用于识别用户,不需要存储用户的额外信息。非常安全。

提升用户粘性

通过扫描二维码登录把用户引入公众号关注,在登录的同时还可以为公众号引流,提升用户粘性,同时公众号是一个非常方便的用户触达方式,未来新功能的发布可以及时送达用户。

公众号扫码登录实现原理

要想理清公众号扫码登录的实现原理,首先要知道在扫码登录过程中,有哪些参与方,它们之间的工作流程是怎么样的。这里的参与方有用户、浏览器、网站服务端、微信服务端四个参与方,总体的工作流程如下图,下面会进行详细介绍。

用户
:用户是扫码登录的发起方,点击登录,然后扫描登录二维码。

浏览器
:浏览器为用户展示二维码,然后不断的轮询扫码状态。

服务端
:网站服务端需要向微信服务端获取携带 Ticket 信息的公众号二维码,在微信服务端回调时绑定用户身份信息。

微信服务端
:用户扫码后,会请求到微信服务端,微信服务端会携带扫描的二维码的 Ticket 和用户
身份标识
回调网站服务端。

微信服务端回调网站服务端时,携带的用户身份信息其实只是一串无意义字符串,但是微信可以保证的是同一个微信用户扫码时携带的身份信息字符是相同的,以此识别用户。也因此公众号扫码登录用作身份认证非常安全。

开发准备工作

公众号

首先你要有用于扫码登录的微信公众号,微信公众平台提供了测试平台,可以直接生成测试公众号。

微信测试号:
https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index

开发者文档

获取公众号二维码的过程需要参考微信公众号官方文档,下面几篇内容需要重点关注。

  1. 公众号接口接入指南

    链接:
    https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html

  2. 获取 Access Token

    Access Token 是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用 Access Token 。每次获取有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的 Access Token 失效。

    链接:
    https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

  3. 生成带 Ticket 二维码

    使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。

    链接:
    https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html

  4. 接收事件推送

    在用户扫码后微信服务端会回调网站服务端,开发者需要按照指定消息格式对消息进行验证处理。如获取二维码的 Ticket。

    链接:
    https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html

  5. 回复文本消息

    如果想要在用户扫码完成后自动响应如 “登录成功” 之类的提示语,需要参考此文档。

    链接:
    https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html

网站服务端

想要微信服务端成功回调,服务端必须外网可以访问,同时微信限制了端口只能是 80 或 443 ,因此只有两种选择 。

  1. 拥有自己的云服务器(这里推荐我司阿里云服务器,如果只是学习体验,抢占式实例低配置一小时也就2毛钱左右,可以用于测试)。
  2. 用内网穿透软件生成外网代理(一般 80 端口都需要收费)。

具体开发

配置微信公众号

可以在微信测试号平台上配置用于测试。

链接:
https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index

项目总体结构

项目使用
SpringBoot 3.2.3 + Java 21
进行开发,这里为了方便演示,在一个 Maven 项目中完成所有代码。本文步骤中只会给出关键代码部分,完整代码可以在文末的 GitHub 地址中找到。

下面是项目总体结构:

├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── wdbyte
        │           └── weixin
        │               ├── SpringBootApp.java
        │               ├── config
        │               │   └── JwtFilter.java   # JWT 身份认证拦截器
        │               ├── controller
        │               │   ├── WeixinServerController.java # 微信服务端调用接口
        │               │   └── WeixinUserController.java  # 浏览器调用接口
        │               ├── model
        │               │   ├── ApiResult.java     
        │               │   ├── ReceiveMessage.java  # 微信消息封装类
        │               │   └── WeixinQrCode.java # 微信二维码 Ticket 封装类
        │               ├── service
        │               │   ├── WeixinUserService.java # 微信调用处理类
        │               │   └── impl
        │               │       └── WeixinUserServiceImpl.java
        │               └── util
        │                   ├── AesUtils.java # AES 加密工具类
        │                   ├── ApiResultUtil.java
        │                   ├── HttpUtil.java # HTTP 工具类
        │                   ├── JwtUtil.java # JWT 工具类
        │                   ├── KeyUtils.java 
        │                   ├── WeixinApiUtil.java # 微信 API 工具类,如获取 AccessToken
        │                   ├── WeixinMsgUtil.java # 微信消息工具类
        │                   ├── WeixinQrCodeCacheUtil.java # 微信二维码Ticket缓存
        │                   └── XmlUtil.java 
        └── resources
            ├── application.properties #配置文件
            ├── static
            └── templates

WeixinServerController

WeixinUserController
暴漏了三个 API。

/weixin/check
:用于对接微信服务端,接收微信服务端的调用。

/user/qrcode
: 用于获取二维码图片信息

/user/login/qrcode
: 用于校验是否扫描成功,成功则返回身份认证后的 JWT 字符串。

项目公众号信息配置


application.properties
中配置公众号所需的配置。

server.port=
weixin.appid=
weixin.appsecret=
weixin.token=

ase.util.secret=
key.jwt.secret=

验证签名

开发者提交信息后,微信服务器向填写的 URL 发送 Get 请求,携带参数如下:

参数 描述
signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp 时间戳
nonce 随机数
echostr 随机字符串

开发者需要对 signature 进行校验,判断是否来自微信服务器,公众号相关的其他事件如消息、关注、扫码等一样会回调配置的 URL ,只不过这时是 POST 请求。

验签逻辑查看微信文档:
https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html

Java 实现如下:

// com.wdbyte.weixin.service.impl.WeixinUserServiceImpl.java

@Value("${weixin.token}")
private String token;

@Override
public void checkSignature(String signature, String timestamp, String nonce) {
    String[] arr = new String[] {token, timestamp, nonce};
    Arrays.sort(arr);
    StringBuilder content = new StringBuilder();
    for (String str : arr) {
        content.append(str);
    }
    String tmpStr = DigestUtils.sha1Hex(content.toString());
    if (tmpStr.equals(signature)) {
        log.info("check success");
        return;
    }
    log.error("check fail");
    throw new RuntimeException("check fail");
}

获取 Access Token

获取带有 Ticket 的公众号二维码之前,需要先获取公众号的 Access Token,这是调用微信公众号所有接口的前提。 Access Token 每日调用次数有限,应该进行缓存。

// com.wdbyte.weixin.util.WeixinApiUtil.java
@Value("${weixin.appid}")
public String appId;

@Value("${weixin.appsecret}")
public String appSecret;

private static String ACCESS_TOKEN = null;
private static LocalDateTime ACCESS_TOKEN_EXPIRE_TIME = null;

/**
 * 获取 access token
 *
 * @return
 */
public synchronized String getAccessToken() {
    if (ACCESS_TOKEN != null && ACCESS_TOKEN_EXPIRE_TIME.isAfter(LocalDateTime.now())) {
        return ACCESS_TOKEN;
    }
    String api = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret="
        + appSecret;
    String result = HttpUtil.get(api);
    JSONObject jsonObject = JSON.parseObject(result);
    ACCESS_TOKEN = jsonObject.getString("access_token");
    ACCESS_TOKEN_EXPIRE_TIME = LocalDateTime.now().plusSeconds(jsonObject.getLong("expires_in") - 10);
    return ACCESS_TOKEN;
}

生成登录二维码

使用 Access Token 获取二维码 Ticket 用来换取二维码图片。

// com.wdbyte.weixin.util.WeixinApiUtil.java
private static String QR_CODE_URL_PREFIX = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";

/**
 * 二维码 Ticket 过期时间
 */
private static int QR_CODE_TICKET_TIMEOUT = 10 * 60;


/**
 * 获取二维码 Ticket
 *
 * https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
 *
 * @return
 */
public WeixinQrCode getQrCode() {
    String api = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getAccessToken();
    String jsonBody = String.format("{\n"
        + "  \"expire_seconds\": %d,\n"
        + "  \"action_name\": \"QR_STR_SCENE\",\n"
        + "  \"action_info\": {\n"
        + "    \"scene\": {\n"
        + "      \"scene_str\": \"%s\"\n"
        + "    }\n"
        + "  }\n"
        + "}", QR_CODE_TICKET_TIMEOUT, KeyUtils.uuid32());
    String result = HttpUtil.post(api, jsonBody);
    log.info("get qr code params:{}", jsonBody);
    log.info("get qr code result:{}", result);
    WeixinQrCode weixinQrCode = JSON.parseObject(result, WeixinQrCode.class);
    weixinQrCode.setQrCodeUrl(QR_CODE_URL_PREFIX + URI.create(weixinQrCode.getTicket()).toASCIIString());
    return weixinQrCode;
}
class WeixinQrCode {
    private String ticket;
    private Long expireSeconds;
    private String url;
    private String qrCodeUrl;
}

响应内容格式如下:

{
  "ticket": "gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm3sUw==",
  "expire_seconds": 60,
  "url": "http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"
}

其中 Ticket 就是二维码凭证,用户扫码后微信会把此 Ticket 回调给网站服务端。可以在下面的链接后面拼上 Ticket 换取二维码图片。

https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=

扫码回调与身份绑定

用户扫码后,微信服务端会把二维码鞋带的 Ticket 和用户的身份标识作为消息内容回调到网站服务器。

格式如下:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[FromUser]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[subscribe]]></Event>
  <EventKey><![CDATA[qrscene_123123]]></EventKey>
  <Ticket><![CDATA[TICKET]]></Ticket>
</xml>

其中
FromUserName
是用户身份标识,
EventKey

qrscene_
标识扫码,
Ticket
则是二维码的 Ticket。至此,服务端就可以识别出二维码是被哪个用户扫码了。
绑定 Ticket 和用户身份标识。

注:FromUserName 是唯一的用户身份标识,同一个用户每次扫描的 FromUserName 相同。

浏览器轮询扫描状态

浏览器鞋带 Ticket 信息不断的轮询
/user/login/qrcode
接口查看 Ticket 是否被扫描成功,如果通过 Ticket 可以查到用户身份标识,说明二维码被扫描成功,返回用户信息。登录完成。

扫码登录测试

浏览器不断的轮询
https://api.wdbyte.com/user/login/qrcode?ticket=Ticket值
获取扫码状态。

二维码尚未扫描,则返回:

{
  "code": -1,
  "data": "check faild",
  "message": "error"
}

微信扫码关注公众号。

扫码成功后轮询接口会响应 JWT 格式的身份信息,这里使用了 AES 对 JWT 进行了加密。

{
  "code": 200,
  "data": "mihzE8Z1Y9t2EoppNSzzytV4TOgn+Nc50ORZjsW/oVkxchL4EzGA6rr1tQ0Q7J24Ipm4otjCYf95Nu8JbV31Q/ImKvlta3f5bgvOdWSlO2tNvOwqgzBSItABohbCLVLxjGCci4VtNaEFgQjoDjc1uhwP/GCSohVFc7csO9SxpOm8HKtlRhATjwPrtiQ9iLErfsUs27I0k5OHp55AzuQOYCvza//i3wk8nlv/MDkk7y1nvsZkllyKQGHPB4Ulcraz",
  "message": "success"
}

至此,登录完成。

完整代码:
github.com/niumoo/JavaNotes/tree/master/springboot/springboot-weixin-qrcode-login

一如既往,文章中代码存放在
Github.com/niumoo/javaNotes
.

本文收录于
Github.com/niumoo/JavaNotes
,Java 系列文档,数据结构与算法!
本文收录于网站:
https://www.wdbyte.com/
,我的公众号:
程序猿阿朗

前言

前几日在浏览
devblogs.microsoft.com
的时候,看到了一篇名为
Image to Text with Semantic Kernel and HuggingFace
的文章。这篇文章大致的内容讲的是,使用
Semantic Kernel
结合
HuggingFace
来实现图片内容识别。注意,这里说的是图片内容识别,并非是
OCR
,而是它可以大致的描述图片里的主要内容。我个人对这些还是有点兴趣的,于是就尝试了一下,本文就是我体验过程的记录。

示例

话不多说,直接展示代码。按照文档上说的,使用
HuggingFace ImageToText
构建自己的应用程序时,需要使用以下的包

  • Microsoft.SemanticKernel
  • Microsoft.SemanticKernel.Connectors.HuggingFace

第一个包是
SemanticKernel
包,提供构建
AI
应用的基础能力。第二个包是
HuggingFace
包,提供
HuggingFace

API
,方便我们调用
HuggingFace
的模型。需要注意的是这个包是预发行版,所以在用
VS
添加的时候需要在
VS
勾选
包括预发行版
。使用起来也非常简单,代码如下所示

var kernel = Kernel.CreateBuilder().AddHuggingFaceImageToText("Salesforce/blip-image-captioning-base").Build();
IImageToTextService service = kernel.GetRequiredService<IImageToTextService>();
var imageBinary = File.ReadAllBytes(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "demo.jpg"));
var imageContent = new ImageContent(imageBinary) { MimeType = "image/jpeg" };
var textContent = await service.GetTextContentAsync(imageContent);
Console.WriteLine($"已识别图片中描述的内容: {textContent.Text}");

代码很简单,运行起来试试效果,发现是直接报错了,报错信息如下:

Microsoft.SemanticKernel.HttpOperationException:“由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。 (api-inference.huggingface.co:443)”

原因也很简单,我本地连接不了
huggingface
,这个需要换种上网方式才能解决。看来默认是请求的
api-inference.huggingface.co:443
这个地址,在源码中求证了一下
HuggingFaceClient.cs#L41
,发现确实是这样

internal sealed class HuggingFaceClient
{
    private readonly IStreamJsonParser _streamJsonParser;
    private readonly string _modelId;
    private readonly string? _apiKey;
    private readonly Uri? _endpoint;
    private readonly string _separator;
    private readonly HttpClient _httpClient;
    private readonly ILogger _logger;

    internal HuggingFaceClient(
        string modelId,
        HttpClient httpClient,
        Uri? endpoint = null,
        string? apiKey = null,
        IStreamJsonParser? streamJsonParser = null,
        ILogger? logger = null)
    {
        Verify.NotNullOrWhiteSpace(modelId);
        Verify.NotNull(httpClient);
        //默认请求地址
        endpoint ??= new Uri("https://api-inference.huggingface.co");
        this._separator = endpoint.AbsolutePath.EndsWith("/", StringComparison.InvariantCulture) ? string.Empty : "/";
        this._endpoint = endpoint;
        this._modelId = modelId;
        this._apiKey = apiKey;
        this._httpClient = httpClient;
        this._logger = logger ?? NullLogger.Instance;
        this._streamJsonParser = streamJsonParser ?? new TextGenerationStreamJsonParser();
    }
}

它只是默认情况下请求的
api-inference.huggingface.co
这个地址,如果想要请求其他地址的话,需要自己实现一个
api
,然后通过
SemanticKernel
调用。

曲线实现

上面提到了既然是
huggingface

api
我们访问不到,而且我不是很喜欢这种在线方式,太依赖三方接口的稳定性了,我更喜欢本地可以部署的,这样的话就不用考虑网络和稳定性问题了。于是想到了一个曲线的方式,那是不是可以自己实现一个
api
,然后通过
SemanticKernel
调用呢?答案是肯定的。

blip-image-captioning-base模型

通过上面的示例我们可以看到它使用
ImageToText
图片识别模型使用的是
Salesforce/blip-image-captioning-base
这个模型,我们可以自行下载这个模型到本地。上面说了
huggingface
需要换种上网方式,不过没关系这个国内是有镜像网站的
https://hf-mirror.com/
,找到模型地址
Salesforce/blip-image-captioning-base
选择
Files and versions
标签把里面的所有文件下载到本地文件夹即可,大概是
1.84 G
左右。比如我是放到我的
D:\Users\User\blip-image-captioning-base
文件夹内,目录结构如下所示

这个模型没有特殊要求,我的电脑是
16G内存

i5
处理器都可以运行起来。接下来用调用这个模型试一试,该模型是适配了
transformers
框架,所以调用起来比较加单,代码如下所示

from PIL import Image
from transformers import BlipProcessor, BlipForConditionalGeneration

processor = BlipProcessor.from_pretrained("D:\\Users\\User\\blip-image-captioning-base")
model = BlipForConditionalGeneration.from_pretrained("D:\\Users\\User\\blip-image-captioning-base")

img_url = '01f8115545963d0000019ae943aaad.jpg@1280w_1l_2o_100sh.jpg'
raw_image = Image.open(img_url).convert('RGB')

inputs = processor(raw_image, return_tensors="pt")
out = model.generate(**inputs)
en_text = processor.decode(out[0], skip_special_tokens=True)
print(f'已识别图片中描述的内容:{en_text}')

然后我使用了我本地的一张图片

运行这段代码之后输出信息如下所示

已识别图片中描述的内容:a kitten is standing on a tree stump

识别的结果描述的和图片内容大致来说是一致的,看来简单的图片效果还是不错的。不过美中不足的是,它说的是英文,给中国人看说英文这明显不符合设定。所以还是得想办法把英文翻译成中文。

opus-mt-en-zh模型

上面我们看到了
blip-image-captioning-base
模型效果确实还可以,只是它返回的是英文内容,这个对于英文不足六级的人来说读起来确实不方便。得想办法解决把英文翻译成中文的问题。因为不想调用翻译接口,所以这里我还是想使用模型的方式来解决这个问题。使用
Bing
搜索了一番,发现推荐的
opus-mt-en-zh
模型效果不错,于是打算试一试。还是在
hf-mirror.com
上下载模型到本地文件夹内,方式方法如上面的
blip-image-captioning-base
模型一致。它的大小大概在
1.41 GB
左右,也是
CPU
可运行的,比如我的是下载到本地
D:\Users\User\opus-mt-en-zh
路径下,内容如下所示

接下来还是老规矩,调用一下这个模型看看效果,不过在
huggingface
对应的仓库里并没有给出如何使用模型的示例,于是去
stackoverflow
上找到两个类似的内容参考了一下

通过上面的连接可以看到,非常好的地方就是,这个模型也是兼容
transformers
框架的,所以调用起来非常简单,把上面的英文内容拿过来试一试, 代码如下所示

from transformers import AutoTokenizer, AutoModelWithLMHead

model = AutoModelWithLMHead.from_pretrained("D:\\Users\\User\\opus-mt-en-zh")
tokenizer = AutoTokenizer.from_pretrained("D:\\Users\\User\\opus-mt-en-zh")
# 英文文本
en_text='a kitten is standing on a tree stump'

encoded = tokenizer([en_text], return_tensors="pt")
translation = model.generate(**encoded)
# 翻译后的中文内容
zh_text = tokenizer.batch_decode(translation, skip_special_tokens=True)[0]
print(f'已识别图片中描述的内容:\r\n英文:{en_text}\r\n中文:{zh_text}')

运行这段代码之后输出信息如下所示

已识别图片中描述的内容:
英文:a kitten is standing on a tree stump
中文:一只小猫站在树桩上

这下看着舒服了,至少不用借助翻译工具了。模型的部分到此就差不多了,接下来看如何整合一下模型的问题。

结合Microsoft.SemanticKernel.Connectors.HuggingFace

上面我们调研了图片内容识别的模型和英文翻译的模型,接下来我们看一下如何使用
Microsoft.SemanticKernel.Connectors.HuggingFace
去整合我们本地的模型。我们通过上面了解到了他说基于
http
的方式去调用了,这就很明确了。只需要知道调用的路径、请求参数、返回参数就可以自己写接口来模拟了。这个就需要去看一下
SemanticKernel
里面涉及的代码了。核心类就是
HuggingFaceClient
类,我们来看下它的
GenerateTextAsync
方法的代码

public async Task<IReadOnlyList<TextContent>> GenerateTextAsync(
        string prompt,
        PromptExecutionSettings? executionSettings,
        CancellationToken cancellationToken)
{
	string modelId = executionSettings?.ModelId ?? this._modelId;
	var endpoint = this.GetTextGenerationEndpoint(modelId);
	var request = this.CreateTextRequest(prompt, executionSettings);
	using var httpRequestMessage = this.CreatePost(request, endpoint, this._apiKey);

	string body = await this.SendRequestAndGetStringBodyAsync(httpRequestMessage, cancellationToken)
		.ConfigureAwait(false);

	var response = DeserializeResponse<TextGenerationResponse>(body);
	var textContents = GetTextContentFromResponse(response, modelId);

	return textContents;
}

//组装请求路径方法
private Uri GetTextGenerationEndpoint(string modelId)
	=> new($"{this._endpoint}{this._separator}models/{modelId}");

private HttpRequestMessage CreateImageToTextRequest(ImageContent content, PromptExecutionSettings? executionSettings)
{
	var endpoint = this.GetImageToTextGenerationEndpoint(executionSettings?.ModelId ?? this._modelId);

	var imageContent = new ByteArrayContent(content.Data?.ToArray());
	imageContent.Headers.ContentType = new(content.MimeType);

	var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
	{
		Content = imageContent
	};

	this.SetRequestHeaders(request);

}

private Uri GetImageToTextGenerationEndpoint(string modelId)
	=> new($"{this._endpoint}{this._separator}models/{modelId}");

通过上面的
GenerateTextAsync
方法代码我们可以得到我们自定义接口时所需要的全部信息

  • 首先是请求路径问题, 我们通过
    GetTextGenerationEndpoint

    GetImageToTextGenerationEndpoint
    方法可以看到,拼接的路径地址
    服务地址/models/模型id
    ,比如我们上面调用的是
    Salesforce/blip-image-captioning-base
    模型,拼接的路径就是
    models/Salesforce/blip-image-captioning-base
  • 其次通过
    CreateImageToTextRequest
    方法我们可以得知,请求参数的类型是
    ByteArrayContent
    ,请求参数的
    ContentType

    image/jpeg
    。也就是把我们的图片内容转换成字节数组放到请求
    body
    请求体里即可,然后
    POST
    到具体的服务里即可。
  • 通过
    TextGenerationResponse
    返回类型我们可以知道这个承载的是返回参数的类型里。

我们来看下
TextGenerationResponse
类的定义

internal sealed class TextGenerationResponse : List<GeneratedTextItem>
{
    internal sealed class GeneratedTextItem
    {
        [JsonPropertyName("generated_text")]
        public string? GeneratedText { get; set; }
    }
}

这个参数比较简单,就是返回一个包含
generated_text
字段的数组即可对应成
json格式
的话就是
[{"generated_text":"识别结果"}]
。接下来我们需要做的是把模型整合换成
http接口
,这样的话
Microsoft.SemanticKernel.Connectors.HuggingFace
就可以调用这个接口了。这里我选择使用的是python的
fastapi
web框架去整合成
webapi
服务,其他框架也可以,只要入参返回的结果把握住就可以,整合后效果如下所示

import io
import uvicorn
from fastapi import FastAPI, Request
from PIL import Image
from transformers import BlipProcessor, BlipForConditionalGeneration, AutoTokenizer, AutoModelWithLMHead

app = FastAPI()

# 图片内容识别模型
processor = BlipProcessor.from_pretrained("D:\\Users\\User\\blip-image-captioning-base")
blipModel = BlipForConditionalGeneration.from_pretrained("D:\\Users\\User\\blip-image-captioning-base")

# 英文翻译模型
tokenizer = AutoTokenizer.from_pretrained("D:\\Users\\User\\opus-mt-en-zh")
opusModel = AutoModelWithLMHead.from_pretrained("D:\\Users\\User\\opus-mt-en-zh")

# 定义接口函数
@app.post("/models/Salesforce/blip-image-captioning-base", summary="图片内容识别")
async def blip_image_captioning_base(request: Request):
    # 获取请求参数
    request_object_content: bytes = await request.body()
    # 转换图片内容
    raw_image = Image.open(io.BytesIO(request_object_content)).convert('RGB')

    # 识别图片内容
    inputs = processor(raw_image, return_tensors="pt")
    out = blipModel.generate(**inputs)
    en_text = processor.decode(out[0], skip_special_tokens=True)
    
    # 英译汉
    encoded = tokenizer([en_text], return_tensors="pt")
    translation = opusModel.generate(**encoded)
    zh_text = tokenizer.batch_decode(translation, skip_special_tokens=True)[0]
    return [{"generated_text": zh_text}]


if __name__ == '__main__':
    # 运行fastapi程序
    uvicorn.run(app="snownlpdemo:app", host="0.0.0.0", port=8000, reload=True)

这里我们把服务暴露到
8000
端口上去,等待服务启动成功即可,然后我们去改造
Microsoft.SemanticKernel.Connectors.HuggingFace
的代码如下所示

//这里我们传递刚才自行构建的fastapi服务地址
var kernel = Kernel.CreateBuilder().AddHuggingFaceImageToText("Salesforce/blip-image-captioning-base", new Uri("http://127.0.0.1:8000")).Build();
IImageToTextService service = kernel.GetRequiredService<IImageToTextService>();
var imageBinary = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "01f8115545963d0000019ae943aaad.jpg@1280w_1l_2o_100sh.jpg"));
var imageContent = new ImageContent(imageBinary) { MimeType = "image/jpeg" };
var textContent = await service.GetTextContentAsync(imageContent);
Console.WriteLine($"已识别图片中描述的内容: {textContent.Text}");

这样的话代码改造完成,需要注意的是得先运行
fastapi
服务等待服务启动成功之后,再去然后运行
dotnet
项目,运行起来效果如下所示

已识别图片中描述的内容: 一只小猫站在树桩上

改造成插件

我们使用上面的方式是比较生硬古板的,熟悉
SemanticKernel
的同学都清楚它是支持自定插件的,这样的话它可以根据我们的提示词来分析调用具体的插件,从而实现调用我们自定义的接口。这是一个非常实用的功能,让
SemanticKernel
的调用更加灵活,是对
AIGC
能力的扩展,可以让他调用我们想调用的接口或者服务等等。话不多说,我们定义一个插件让它承载我们识别图片的内容,这样的话就可以通过
SemanticKernel
的调用方式去调用这个插件了。定义插件的代码如下所示

public class ImageToTextPlugin
{
    private IImageToTextService _service;
    public ImageToTextPlugin(IImageToTextService service)
    {
        _service = service;
    }

    [KernelFunction]
    [Description("根据图片路径分析图片内容")]
    public async Task<string> GetImageContent([Description("图片路径")] string imagePath)
    {
        var imageBinary = File.ReadAllBytes(imagePath);
        var imageContent = new ImageContent(imageBinary) { MimeType = "image/jpeg" };
        var textContent = await _service.GetTextContentAsync(imageContent);
        return $"图片[{imagePath}]分析内容为:{textContent.Text!}";
    }
}

这里需要注意的是我们定义的方法的
Description
和参数的
Description
,其中
GetImageContent
方法的
Description

SemanticKernel
的提示词,这样在调用的时候就可以通过提示词来调用这个方法了。参数
imagePath

Description
这样
OpenAI
就知道如何在提示词里提取出来对应的参数信息了。好了接下来我们看下如何使用这个插件

using HttpClient httpClient = new HttpClient(new RedirectingHandler());
var executionSettings = new OpenAIPromptExecutionSettings()
{
    ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions,
    Temperature = 1.0
};
var builder = Kernel.CreateBuilder().AddHuggingFaceImageToText("Salesforce/blip-image-captioning-base", new Uri("http://127.0.0.1:8000"));
var kernel = builder.Build();
ImageToTextPlugin imageToTextPlugin = new ImageToTextPlugin(kernel.GetRequiredService<IImageToTextService>());
kernel.Plugins.AddFromObject(imageToTextPlugin);

var chatCompletionService = new OpenAIChatCompletionService("gpt-3.5-turbo-0125", "你的apiKey", httpClient: httpClient);

Console.WriteLine("现在你可以开始和我聊天了,输入quit退出。等待你的问题:");
do
{
    var prompt = Console.ReadLine();
    if (!string.IsNullOrWhiteSpace(prompt))
    {
        if (prompt.ToLowerInvariant() == "quit")
        {
            Console.WriteLine("非常感谢!下次见。");
            break;
        }
        else
        {
            var history = new ChatHistory();
            history.AddUserMessage(prompt);
            //调用gpt的chat接口
            var result = await chatCompletionService.GetChatMessageContentAsync(history,
                        executionSettings: executionSettings,
                        kernel: kernel);
            //判断gpt返回的结果是否是调用插件
            var functionCall = ((OpenAIChatMessageContent)result).GetOpenAIFunctionToolCalls().FirstOrDefault();
            if (functionCall != null)
            {
                kernel.Plugins.TryGetFunctionAndArguments(functionCall, out KernelFunction? pluginFunction, out KernelArguments? arguments);
                var content = await kernel.InvokeAsync(pluginFunction!, arguments);
                Console.WriteLine(content);
            }
            else
            {
                //不是调用插件这直接输出返回结果
                Console.WriteLine(result.Content);
            }
        }
    }
} while (true);

这里需要注意自定义的
RedirectingHandler
,如果你不是使用
OpenAI
的接口而是自己对接或者代理的
OpenAI
的接口,就需要自行定义
HttpClientHandler
来修改请求的
GPT
的服务地址。

public class RedirectingHandler : HttpClientHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.RequestUri = new UriBuilder(request.RequestUri!) { Scheme = "http", Host = "你的服务地址", Path= "/v1/chat/completions" }.Uri;
        return base.SendAsync(request, cancellationToken);
    }
}

这样的话我们就可以在于
GPT
的交互中调用我们自定义的插件了,当我们输入相关的提示词
OpenAI
的接口就可以根据提示词和插件信息返回调用哪个插件。使用了几张我本地的图片试了一下效果还是不错的,能分析出大致的图片内容,如下所示

这样使用起来就比较灵活了,在对话的过程中就可以使用本地的功能,不得不说有了插件化的能力
SemanticKernel
的功能就更加丰富了。关于插件化的实现原理也是比较简单,这是利用
OpenAI
对话接口的能力,我们只需要定义好插件和相关的提示词就可以,比如我们上面示例,使用
Fiddler

Charles
拦截一下发出的请求即可,它是发起的
HTTP请求
,请求格式如下

{
    "messages": [
        {
            "content": "Assistant is a large language model.",
            "role": "system"
        },
        {
            "content": "请帮我分析这张图片的内容D:\\Software\\AI.Lossless.Zoomer-2.1.0-x64\\Release\\output\\20200519160906.png",
            "role": "user"
        }
    ],
    "temperature": 1,
    "top_p": 1,
    "n": 1,
    "presence_penalty": 0,
    "frequency_penalty": 0,
    "model": "gpt-3.5-turbo-0125",
    "tools": [
        {
            "function": {
                "name": "ImageToTextPlugin-GetImageContent",
                "description": "根据图片路径分析图片内容",
                "parameters": {
                    "type": "object",
                    "required": [
                        "imagePath"
                    ],
                    "properties": {
                        "imagePath": {
                            "type": "string",
                            "description": "图片路径"
                        }
                    }
                }
            },
            "type": "function"
        }
    ],
    "tool_choice": "auto"
}

通过请求
OpenAI

/v1/chat/completions
接口的请求参数我们可以大致了解它的工作原理,
SemanticKernel
通过扫描我们定义的插件的元数据比如
类_方法

方法的描述

参数的描述
来放入请求的
JSON
数据里,我们定义的
Description
里的描述作为提示词拆分来具体匹配插件的依据。接下来我们再来看一下这个接口的返回参数的内容

{
    "id": "chatcmpl-996IuJbsTrXHcHAM3dqtguwNi9M3Z",
    "object": "chat.completion",
    "created": 1711956212,
    "model": "gpt-35-turbo",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": null,
                "tool_calls": [
                    {
                        "id": "call_4aN9xUhly2cEbNmzRcIh1it0",
                        "type": "function",
                        "function": {
                            "name": "ImageToTextPlugin-GetImageContent",
                            "arguments": "{\"imagePath\":\"D:\\\\Software\\\\AI.Lossless.Zoomer-2.1.0-x64\\\\Release\\\\output\\\\20200519160906.png\"}"
                        }
                    }
                ]
            },
            "finish_reason": "tool_calls"
        }
    ],
    "usage": {
        "prompt_tokens": 884,
        "completion_tokens": 49,
        "total_tokens": 933
    },
    "system_fingerprint": "fp_2f57f81c11"
}

OpenAI
接口给我们返回了它选择的插件信息,告诉我们可以调用
ImageToTextPlugin-GetImageContent
这个方法,传递的参数则是
{\"imagePath\":\"D:\\\\Software\\\\AI.Lossless.Zoomer-2.1.0-x64\\\\Release\\\\output\\\\20200519160906.png\"}
,这是
GPT
帮我们分析的结果,
SemanticKernel
根据这个信息来调用我们本地的插件,执行具体操作。这里
GPT
的起到的作用就是,我们请求的时候提交插件的元数据,
GPT
根据提示词和插件的元数据帮我分析我们可以调用哪个插件,并且把插件参数帮我们分析出来,这样我们就可以根据返回的插件元数据来调用我们本地的插件了。

需要注意的,目前我尝试的是只有
OpenAI

AzureOpenAI
提供的对话接口支持插件的能力,国内的模型我试了一下比如
文心一言

讯飞星火

通义千问

百川
都不支持,至少通过
OneApi
对接过来的不支持,不知道是不是我姿势不对。

参考连接

以下是学习研究过程中参考的一些连接,在这里展示出来供大家参考。涉及到学习参考、解决问题、查找资源相关。毕竟人生地不熟的,需要找到方向

总结

本文缘起来于在
devblogs
上看到的一篇文章,感觉比较有趣,便动手实践一下。其中遇到了问题,便部署本地模型来实现,最终实现了
Microsoft.SemanticKernel.Connectors.HuggingFace
调用本地模型实现图片内容识别。最终把它定义成一个插件,这样在
SemanticKernel
中就可以通过调用插件的方式来调用本地模型,实现图片内容识别。这些可以在本地运行的实现特定功能的模型还是比较有意思的,模型本身不大,本地可运行,适合初学者或者有兴趣的人使用。

我始终倡导大家积极接触和学习新技术。这并不意味着我们必须深入钻研,毕竟人的精力有限,无法将所有精力都投入到这些领域。但至少,我们应该保持好奇心,对这些新技术有所了解,理解其基本原理。这样,当有一天我们需要应用这些技术时,就能更加得心应手。即使我们不能成为某个领域的专家,但对这些技术的了解也会成为我们思考的一部分,让我们在解决问题时拥有更多的选择和思路。因此,不要害怕尝试新事物,保持好奇心和学习态度,这将是我们不断进步的关键。


前言

如果大家有接触过ADFS或者认证协议,肯定会对五花八门的名词看的眼花缭乱,比如WS-FED、SAML、SAML Token、OAuth、OpenID Connect、Kerbros以及NTLM等, 但实际上我们可以高屋建瓴的学习它们。

拆分

作为程序员或者工程师,我们都擅长将问题拆分和类比,在认证协议上我们同样可以如此分为三方面,登录协议,验证过程以及令牌类型。举个例子,当我们坐飞机时,首先去柜台办理登记手续,出示自己的身份证,然后工作人员会返回给你机票,然后可以拿着身份证去登机口登机。那么这三个方面都是什么?

  1. 登录协议,整个登机的过程就是一个登录协议,规定了整套登机的流程。而WS-FED、SAML、OAuth、OpenID Connect属于登录协议。

  2. 验证过程,在柜台出示身份证,然后交换机票的这个过程被称为验证过程。普通的password认证,以及Kerbros和NTLM属于验证过程。

  3. 令牌,在整个流程中,机票就是属于令牌。 而在认证协议中令牌也有很多种,最常见的是OAuth中的JWT,Json Web Token。需要注意SAML Token此时属于令牌而不是协议。

WS-FED

WS-FED是ADFS Server的登录协议,从三方面来解释这个协议

  1. 登录协议,属于WS-FED

  2. 验证过程,支持多种验证过程,包括Forms-based即password, Kerbros 和NTLM。

  3. 令牌,注意用WS-Fed时,令牌一般是SAML 1.1 Token

那么WS-FED的URL一般是这样的,
https://sts.example.com/adfs/ls/
? wa=wsignin1.0 & wtrealm=... & wctx=...

参数分别代表的内容如下:

  • wa=wsignin1.0,通知ADFS server登录,这是一个登录操作

  • Wtrealm, 通知ADFS server, 我要获取什么, 比如跟ADFS Server配置的relying party相对应

  • wctx, 认证之后返回去的session data

拓展知识:

Kerbros和NTLM都是windows认证,其中NTLM时属于比较旧的协议,相对于Kerbros是不安全的协议,所以一般作为Kerbros的Fallback

SAML

SAML也是ADFS Server的登录协议,还是从三方面来解释这个协议

  1. 登录协议,属于SAML

  2. 验证过程,支持多种验证过程,包括Forms-based即password, Kerbros 和NTLM。

  3. 令牌,注意用WS-Fed时,令牌一般是SAML 2.0 Token

那么SAML的URL一般是这样的,
https://sts.example.com/adfs/ls/
? SAMLRequest=... & SigAlg=... & RelayState=... & Signature=...

参数分别代表的内容如下:

  • SAMLRequest, Base64格式,请求内容

  • SigAlg, 签名算法

  • Signature, 对request的签名

  • RelayState, 认证之后返回去的session data

OAuth和OpenID Connect

OAuth 是一种开放标准的授权协议,允许用户让第三方应用访问他们存储在另外服务提供商上的信息而无需将用户名和密码提供给第三方应用。OAuth 作为授权框架,允许第三方应用限定的访问权限,这样用户就不需要与第三方应用共享登录凭据。

举例来说,一个网站(称为客户端)可能允许用户通过Github账户登录,用户可以点击一键登录的按钮,然后被重定向到Github的登录页。在那里,用户输入自己的登录凭据,然后Github会提示用户是否授权那个网站访问其某些信息。一旦用户同意,Github会将用户重定向回原来的网站,同时提供一个授权码。最后,该网站可以使用这个授权码向 Github 请求访问令牌,并使用这个令牌来获取用户信息。

OpenID Connect 是建立在 OAuth 2.0 协议之上的一个身份层,提供了一种身份验证的方式。它允许客户端应用程序依靠授权服务器来验证最终用户的身份,并获取基本的个人资料信息。OpenID Connect 是一个简单的身份验证协议,并且可以和OAuth 2.0无缝集成。
与OAuth 2.0 主要是为了授权访问资源不同,OpenID Connect 提供了更丰富的身份认证信息,通常包含用户的ID令牌和用户信息(比如用户的姓名、邮箱地址等)。

还是从三方面来解释这个协议

  1. 登录协议, OAuth, OIDC

  2. 验证过程,支持多种验证过程,包括Forms-based即password, MFA等

  3. 令牌,JWT即Json Web Token

OAuth 2.0支持很多flow,比如Code flow, Client Credentials Grant flow, Device Code flow等, Code flow的URL一般是

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&response_mode=query
&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read
&state=12345

而OpenID Connect的URL一般是

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&response_type=id_token
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&response_mode=form_post
&scope=openid
&state=12345
&nonce=678910

JVM—对象的创建流程与内存分配

创建流程

对象创建的流程图如下:

对象的创建流程图

对象的内存分配方式

内存分配的方式有两种:

  • 指针碰撞(Bump the Pointer)
  • 空闲列表(Free List)
分配方式 说明 收集器
指针碰撞(Bump the Pointer) 内存地址是连续的(新生代) Serial和ParNew收集器
空闲列表(Free List) 内存地址不连续(老年代) CMS收集器和Mark-Sweep收集器

内存分配方式1

内存分配方式2

指针碰撞

指针碰撞示意图如下:

指针碰撞示意图

内存分配安全问题:

虚拟机给A线程分配内存的过程中,指针未修改,此时B线程同时使用了该内存,就会出现问题。

解决方式:

  • CAS乐观锁:JVM虚拟机采用CAS失败重试的方式保证更新操作的原子性;
  • TLAB (Thread Local Allocation Buffer)本地线程分配缓存,预分配。

分配主流程

首先从TLAB里面分配,如果分配不到,再使用CAS从堆里面划分。

对象如何进入老年代

对象进入老年代流程如下:

对象如何进入老年代

  • 新对象大多数默认都进入Eden;

  • 对象进入老年代的四种情况:


    • 年龄太大
      MinorGC15次

      -XX:MaxTenuringThreshold
      】;

    • 动态年龄判断:MinorGC后会动态判断年龄,将符合要求对象移入老年代;

      MinorGC之后,发现Survivor区中的一批对象的总大小大于了这块Survivor区的50%,那么就会将此时大于等于这批对象年龄最大值的所有对象,直接进入老年代。

      例子: Survivor区中有一批对象,年龄分别为年龄1+年龄2+年龄n的多个对象,对象总和大小超过了Survivor区域的50%,此时就会把年龄n及以上的对象都放入老年代。希望那些可能是长期存活的对象,尽早进入老年代。
      比率可以由-XX:TargetsurvivorRatio指定
      
    • 大对象直接进入老年代1M【
      -XX:PretenureSizeThreshold
      】;(前提是Serial和ParNew收集器)

      为了避免大对象分配内存时的复制操作降低效率。

      避免了Eden和Survivor区的复制。

    • MinorGC后存活对象太多无法放入Survivor。

空间担保机制

空间担保机制:当新生代无法分配内存的时候,我们想把新生代的老对象转移到老年代,然后把新对象放入腾空的新生代。此种机制我们称之为内存担保。

空间担保流程图如下:

空间担保流程图

对象内存布局

对象内存布局示意图如下:

对象内存布局

对象里的三个区

堆内存中,一个对象在内存中存储的布局可以分为三块区域:

堆内存中,一个对象在内存中存储的布局可以分为三块区域:

  • 对象头(Header) : Java对象头占8byte。如果是数组则占12byte。因为JVM里数组size需要使用4byte存储。


    • 标记字段MarkWord:


      • 用于存储对象自身的运行时数据,它是synchronized实现轻量级锁和偏向锁的关键。

      • 默认存储:对象HashCode、GC分代年龄、锁状态等等信息。

      • 为了节省空间,也会随着锁标志位的变化,存储数据发生变化。

        锁标志位的变化

    • 类型指针KlassPoint:


      • 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
      • 开启指针压缩存储空间4byte,不开启8byte。
      • JDK1.6+默认开启
    • 数组长度:如果对象是数组,则记录数组长度,占4个byte,如果对象不是数组则不存在。

    • 对齐填充:保证数组的大小永远是8byte的整数倍。

  • 实例数据(Instance Data):生成对象的时候,对象的非静态成员变量也会存入堆空间

  • 对齐填充(Padding) :JVM内对象都采用8byte对齐,不够8byte的会自动补齐。

如何访问一个对象

有两种方式:

  1. 句柄:稳定,对象被移动只要修改句柄中的地址

  2. 直接指针:访问速度快,节省了一次指针定位的开销

句柄方式访问对象

通过直接指针访问对象