开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

盘点认证协议 普及篇之 OTP 和短信认证方式

发表于 2021-03-26

总文档 :文章目录

Github : github.com/black-ant

  • 纯约束型协议 : OAuth , SAML , OIDC , CAS ,LTPA
  • 服务器类协议 : RADIUS , Kerberos , ADFS
  • 认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)
  • 认证服务器(附带) : AD , LDAP , ADFS

这一篇主要是闲谈一下 OTP 和短信验证码 ,他们2者的逻辑其实是一致的

一 . OTP 认证

1.1 什么是 OTP 认证

OTP 在很多地方都能找到它的痕迹 , 例如支付宝的6位随机密码 , 其实也是一种很复杂的 OTP 方式 .

OTG 是动态密码 ,每个令牌有不同的ID 与其绑定 ,令牌根据该 ID 和当前 时间 计算出 6位 随机代码 ,服务端 会根据 该 ID 生成随机码 ,如果相同则判断正确,

OTP 安全的核心在于密钥 , 每个人通过对应账户生成的密钥是不同的 . 当他们用同一个算法加密时 , 会生成不同的随机密码 .

Goole 身份验证器案例 :

image.png

可以看到 , 其中有三个输入条件 : 账户名 + 密钥 + 密钥类型 , 这就带出了 OTP 的多种类型

1.2 OTP 类型

OTP类型 :

  • 时间性 : 以时间为参数
  • 事件性 : 以次数为参数
  • 挑战数 : 以异步挑战数作为参数 ,即服务端下发 挑战码

根据不同的类型 , OTP 又提供了多种不同的 OTP 算法 ,主要有2种 :

  • TOTP(Time-Based One-Time Password,基于时间的一次性密码)
  • HOTP(HMAC-based One-Time Password,一种基于HMAC的一次性口令算法)

我们来看一下他们的细节 :

OTP 算法 :

1
2
3
4
5
6
java复制代码// 计算公式 : OTP(K,C) =Truncate ( HMAC - SHA - 1 ( K , C ) )

K : 密钥串 ,ID
C :参数
HMAC-SHA-1 : 使用SHA-1做HMAC
Truncate :截取加密后的串,并取加密后串的哪些字段组成一个数字

HMAC-SHA-1 模式 :

  1. HMAC-SHA-1加密后的长度得到一个20字节的密串
  2. 取这个20字节的密串的最后一个字节,取这字节的低4位,作为截取加密串的下标偏移量
  3. 按照下标偏移量开始,获取4个字节,按照大端方式组成一个整数
  4. 截取这个整数的后6位或者8位转成字符串返回

TOTP 算法

1
2
3
4
5
java复制代码// TOTP : TOTP只是将其中的参数C变成了由时间戳产生的数字。
C = (T - T0) / X;
T : 当前时间戳
T0 : 取值为 0
x : 步数 ,多久参数一个动态密码

TOPT_Algorithm.png

HOTP 算法
详情参考 : HOTP和TOTP算法图解 @ www.jianshu.com/p/a7b900e8e…

HTOP 是基于计数器的算法 , 服务端和客户端共用一个密钥 , HOTP 的问题在于怎么保证计数器的同步,

一句话说明: 每次请求和验证 HOTP 时,移动因子将根据计数器递增。生成的代码是有效的,直到您主动请求另一个代码并由身份验证服务器进行验证。每次验证代码和用户获得访问权限时,OTP 生成器和服务器都会同步。

对于 HOTP,通过下图我们已经看到输入算法的主要有两个元素,一个是共享密钥,另外一个是计数。在 RFC 算法中用一下字母表示:

K 共享密钥,这个密钥的要求是每个 HOTP 的生成器都必须是唯一的。一般我们都是通过一些随机生成种子的库来实现。
C 计数器,RFC 中把它称为移动元素(moving factor)是一个 8个 byte的数值,而且需要服务器和客户端同步。

另外一个参数比较好理解,Digit 表示产生的验证码的位数

最后两个参数可能暂时不好理解,我们先放在这,等用到在解释

T 称为限制参数(Throttling Parameter 表示当用户尝试 T 次 OTP 授权后不成功将拒绝该用户的连接。

s 称为重新同步参数(Resynchronization Parameter 表示服务器将通过累加计数器,来尝试多次验证输入的一次性密码,而这个尝试的次数及为 s。该参数主要是有效的容忍用户在客户端无意中生成了额外不用于验证的验证码,导致客户端和服务端不一致,但同时也限制了用户无限制的生成不用于验证的一次性密码。

OTP_algorithm_steps.png

1.3 OTP 超前步数问题及方案

1
2
3
4
5
6
7
java复制代码
> 1 服务端的次数落后客户端的次数 , 服务端会匹配多次 , 以达到和客户端同步的次数 ,匹配成功
> 2 服务端超前客户端

// 以下图存在bug , 服务端超前于客户端时,讲不在能验证!!!!!!!!!!!!!
// 解决方案 : 服务器的值应该只能在成功后进行递增 , 只有用户登录成功后 , 才可以更新用户的计数
// 服务端会允许一定次数的计数器 ,但是如果超过限度 , 程序会报错

HOTP_verification_process.png

1.4 OTP 总结

OTP 有其便利性 , 可以避免繁多的密码问题 , 但是这意味着需要一个 Client 端去实现一套 OTP 逻辑 , 相对于短信验证码 , 它显得更加复杂 , 问题也会更多 . 但他有存在的必要

不过相对于认证 , 如果作为支付宝那样的支付方式 ,其实也是很不错的选择 .

局限和优势 :
虽然两者都比完全不使用 MFA 安全得多,但是 HOTP 和 TOTP 都有其局限性和优势。

  • TOTP (两种技术中较新的一种)易于使用和实现,但是基于时间的元素确实有可能出现时间漂移(密码创建和使用之间的滞后)。如果用户没有立即输入 TOTP,有可能在他们输入之前就过期了。因此,服务器必须考虑到这一点,并使用户可以轻松地再次尝试,而不必自动锁定它们。
  • HOTP 没有基于时间的限制,所以它比较用户友好,但是可能更容易受到穷举法的影响。这是因为 HOTP 有效的窗口可能比较长。某些形式的 HOTP 通过在其代码中添加一个基于时间的组件来解释这一漏洞,这在一定程度上模糊了这两种类型 OTP 之间的界限。

二 . 短信验证码

为什么有了短信验证码 , 还会有OTP ?

现阶段的黑客技术种已经找到了截获这些短信代码的创造性方法,无论是通过SIM 卡欺诈还是其他类型的黑客手段,帮助他们获取你的短信。

  • 也有可能联系你的供应商,将你的电话号码转移到一个新的电话上
  • 利用用于漫游的连接系统 SS7的问题拦截网络上的 SMS 消息

虽然基于 sms 的 MFAs 可能比没有 MFA 要好,但是它们的安全性比手机上的认证应用程序或者使用密钥代码生成器要差得多。

这就意味着短信验证码其实并不是绝对安全 (PS : 破解也存在局限性 ,没有违规操作 ,也没有可乘之机)

相对于首次登录就使用验证码 , 很多短信验证码场景是在首次认证的二次认证(MFAS)后进行使用 ,但是其实这也只是多加了一层

**因为这个原因 , 所以才需要 OTP 来完成更安全的功能 . **

总结和感谢

文章部分参考于 www.jianshu.com/p/a7b900e8e… , 想看更多的可以看看原博主的文章

这篇文章不算长 , 毕竟 OTP 的东西其实就那么点 , 现阶段已经有很多现成的包实现了相关的代码 ,其实复用是没有关系的 , 因为只要密钥不公布 , 问题都不大 .

包括 Google Auth , 他们使用的都是同一套算法 , 我们一般会在用户创建时就为其特定生成一个密钥 , 通过保密的途径提供给用户 ,自行录入即可.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你不知道的 Golang 打包配置文件 Go主题月

发表于 2021-03-26

众所周知,Golang 适合写 CLI 工具,但你可能还不知道 Golang 还可以打包配置文件。

背景

最近在写一个涉及到管理阿里云 ECS 的 CLI 工具,这里当然就要考虑阿里云资源使用的安全性了,要求阿里云账号的 AccessKeyId 和 AccessKeySecret 不能下发给 CLI 工具的使用者。

Alibaba Cload

所以这里选择将一份包含 AccessKeyId 和 AccessKeySecret 的配置文件打包进了 CLI 工具中,CLI 工具的使用者默认将使用已经打包了的配置文件,当然也可以通过指定配置文件或传递参数的方式使用新的配置信息。

实现

工具

这里将介绍 Golang 的一个可以把任意文件转换成 Go 代码的库 go-bindata,可以用于嵌入二进制文件到 Go 程序中。同时,也支持在转换成原始的字节切片前使用 gzip 进行压缩文件数据。

关于该工具的具体介绍请跳转至 github.com/go-bindata/…

打包

使用 go-bindata 工具将包含敏感信息的配置文件转换成 Go 的源代码,下面是项目 Makefile 的部分内容,工具名称就叫 mycli 吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Makefile复制代码NAME = mycli
CONFIG = configs/config.yaml

.PHONY: build

build:
cp $(CONFIG) config.yaml
mkdir -p cmd/mycli/asset
go-bindata -pkg asset -o cmd/mycli/asset/asset.go \
scripts/... \
config.yaml

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/linux/mycli cmd/mycli/*.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/darwin/mycli cmd/mycli/*.go

chmod +x ./bin/linux/mycli ./bin/darwin/mycli
rm -f config.yaml mycli
ln -s bin/linux/mycli mycli

其中将文件转换成 Go 源代码的部分如下:

1
2
3
bash复制代码go-bindata -pkg asset -o cmd/mycli/asset/asset.go \
scripts/... \
config.yaml

关于 go-bindata 命令行工具的选项说明:

  • -pkg 指定 package 名称,调用的写法将变成 asset.Asset("config.yaml")
  • -o 指定生成的 Go 源代码存放的位置

生成的 asset.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
go复制代码// Code generated by go-bindata.
// sources:
// scripts/create.sh
// scripts/sub/delete.sh
// config.yaml
// DO NOT EDIT!

package asset

func bindataRead(data []byte, name string) ([]byte, error) {
...
}

type asset struct {
bytes []byte
info os.FileInfo
}

type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}

func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}

...

调用

使用 Asset 方法进行加载打包好的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码const preloadConfigFile = "config.yaml"

type Config struct {
...
}

func PreloadConfig() (*Config, error) {
b, err := asset.Asset(preloadConfigFile)
if err != nil {
return nil, fmt.Errorf("failed to read config: %v", err)
}
var config *Config
err = yaml.Unmarshal(b, &config)
return config, err
}

总结

使用 go-bindata 将文件转换成 Go 的源代码,然后编译成二进制文件,最终只需要将二进制文件交给使用者,通过这种方式可以减少工具的使用者对一些敏感信息的直接接触,保障资源的安全性。

其实,真正要做到对资源访问的完全把控,可以将 CLI 工具再次进行封装成 Jenkins job 类似的可视化操作界面,既方便使用者使用,又可以限制使用者对工具的使用范围,包括传递给 CLI 工具的参数。

原文链接:k8scat.com/posts/build…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点认证协议 普及篇之LTPA

发表于 2021-03-26

总文档 :文章目录

Github : github.com/black-ant

  • 纯约束型协议 : OAuth , SAML , OIDC , CAS ,LTPA
  • 服务器类协议 : RADIUS , Kerberos , ADFS
  • 认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)
  • 认证服务器(附带) : AD , LDAP , ADFS

这一篇聊一聊 LTPA 这个协议 , 这个算是一个很少见的协议 , 专属于 IBM , 我们只是简单的说说它…

一 . 前言

ltpa 全称 Lightweight Third-Party Authentication , 即轻量级第三方认证 . 这个协议看起来很复杂 , 其实使用的时候会感觉很简单 , 用法和 JWT 很类似.

LTPA 是 一项 IBM 协议 ,用于 在 WebSphere®Application Server 中提供基于 cookie 或二进制安全性令牌的认证机制 ,其支持 单点登录 SSO .

整个流程中包括多个服务器 , 例如WebSphere® 和 DataPower®。要对其中一个或多个服务器实现单点登录解决方案,您可以将 WebSEAL 配置为支持 LTPA 认证 .

目的 : LTPA 令牌认证的目的是将 LTPA 令牌从第一个 Web Service(其认证生成客户机)流动到下游 Web Service , 简单点说就是 IDP Server 生成令牌 , 下发到下游服务 .

Cookie 作用 : 具有有效 LTPA cookie 的用户可以访问与第一个服务器属于同一身份验证域的服务器,并将自动进行身份验证。

Cookie 本身包含有关已经验证的用户、用户要验证的领域(例如 LDAP 服务器)和时间戳的信息。所有这些信息之二用共享的3DES 密钥加密,并由公/私密密钥对签名。这一切都很好,直到您试图执行一些故障排除,并意识到无法查看这些 cookie 的内部。

二 . 深入知识点

2.1 流程分析

宏观流程: User —> ISAM SSO (WebSeal) - LTPA goes here —-> backend server WAS

宏观解释 :

  • 后端WAS服务器本身不做任何身份验证。
  • 用户对SSO服务器进行身份验证,SSO服务器将加密的LTPA令牌发送到后端服务器,该服务器包含用户名(通常只有用 户名、组成员,但从不包含密码)。
  • 后端服务器解密LTPA并将其作为LTPA并信任它。
  • 然后后端应用服务器(WAS)将此身份验证详细信息传递给应用程序。

ltpa_simple_client_server.jpg

流程详情:

  1. 当未经认证的用户对 WebSEAL 受保护资源发出请求时,WebSEAL 将首先确定是否提供了 LTPA Cookie。
    • 如果提供了 LTPA Cookie,那么它将验证此 Cookie 的内容,并在验证成功后根据此 Cookie 中包含的用户名和到期时间创建新会话。
    • 如果未提供 LTPA Cookie,那么 WebSEAL 将继续使用其他已配置的认证机制对用户进行认证。
  2. 完成认证操作后,将在 HTTP 响应中插入新的 LTPA Cookie,并将其传递回客户机以供其他支持 LTPA 的认证服务器使用。

2.2 LTPA 架构

LTPA 协议是基于 WebSphere 实现的 !

WebSphere 是什么 :
WebSphere 是一个IBM 产品 , 它支持在一个因特网域中的一组web 服务器间使用单一登录的认证策略 ,通过密码术 可支持分布式环境的安全性 ,web 用户只需对 WebSphere Application Server 或 Domino 服务器认证一次 ,认证将会通过服务器进行共享.

**PS: 和联合认证有相同的思想 .. **

一个通过有效的LTPA Cookie能够在同一个认证域中所有服务器被自动认证。此Cookie中包含认证信息和时间戳。这些信息通过共享的3DES Key进行了bis 加密。使用公共密钥/私有密钥进行签名。

2.3 LTPA Token 令牌

2.3.1 令牌的属性

LTPA 令牌包含如下属性 :

  • title : 必须 ,策略的标题 ,字符串
  • description : 非必须 , 对策略的描述 ,字符串
  • key : LTPA 秘钥 ,必须 ,用于生成 LTPA 令牌 的LTPA 秘钥名称 - 包括 :
    • my-ltpa-key (策略默认为 1.0),my-ltpa-key:2.0.0 (使用特定版本),my-ltpa-key:latest ( 最新版本 )
    • authenticatedUserName : 已认证用户名
    • tokenVersion : 令牌版本
    • tokenOutput : 令牌用于放置已生成令牌的位置 (cookie 头 ,WSSec 头 )
    • tokenExpiry : 整数 ,令牌过期时间

2.3.2 令牌的案例

1
2
3
4
5
6
java复制代码首先,这个 cookie 由以下部分组成,以%进行分隔:
- 用户信息,格式为u:user\:<RealmName>/<UserDN>,
如:u:user\:VGOLiveRealm/CN=squallzhong,O=VGOLive Technology
- 过期时间
- 签名信息,如:
u:user\:VGOLiveRealm/CN=squallzhong,O=VGOLive Technology%1301558320666%Cy2CAeru5kEElGj0hrvYsKW2ZVsvvcu6Un573aeX55OO4G3EMYWc0e/ZbqDp1z7MS+dLzniuUH4sYWCMpnKdm7ZGabwmV+WcraBl+y+yzwcl722gHVMOnDZAW7U3jEay9Tk2yG4yXkMWU+617xndpVxke2jtS5wIyVVM3q7UDPw=

2.3.3 令牌的区别

LTPA (Version 1): www.ibm.com/websphere/a…

LTPA2: www.ibm.com/websphere/a…

LtpaToken
LtpaToken 用于与 WebSphere Application Server 的前发行版进行互操作。此令牌仅包含认证身份属性。
LtpaToken 针对 WebSphere Application Server V5.1.0.2 之前的发行版(对于 z/OS®)或 V5.1.1(对于分布式系统)生成。

LtpaToken2
LtpaToken2 包含更强的加密功能,并且您能够向令牌添加多个属性。此令牌包含认证身份和其他信息(例如,属性)。属性用于联系原始登录服务器和唯一高速缓存密钥。如果在确定唯一性时要考虑除身份以外的其他内容,还将使用属性来查找主题。

注意 :
为了允许运行不同版本WebSphere Application Server的服务器之间的互操作性,默认情况下,在将绑定配置为期望LTPA2令牌时,Version 7.0及更高版本的JAX-WS web服务安全运行时可以成功地使用LTPA Version 1令牌。但是,您可以将JAX-WS运行时的绑定配置为只接受LTPA2令牌。有关更多信息,请参阅有关身份验证生成器或使用者令牌设置的文档。

IBM LTPA 文档支持

2.4 LTPA Cookie

Cookie 加密方式

LTPA cookie 在 DESede/ECB/PKCS5Padding 模式下使用3DES 密钥进行加密。真正的密钥也是在 DESede/ECB/PKCS5Padding 模式下使用3DES 加密的,其中使用用0X0最多24字节填充的所提供密码的 SHA 散列。要解密实际令牌,可以获取密码,生成一个3DES 密钥,解密加密密钥,然后解密 cookie 数据。还有一个公钥/私钥对用于对 cookie 进行签名。

  • 使用 3DES 秘钥 进行 DESede/ECB/PKCS5P 加密
  • 秘钥采用 DESede/ECB/PKCS5P进行加密
  • 通过提供的密码进行 SHA Hash
  • 对生成的24字节秘钥 进行 Base64 编码

Cookie 的元素 @ my.oschina.net/psuyun/blog…

  • LTPA token 版本(4字节)
  • 创建时间(8字节)
  • 过期时间(8字节)
  • 用户名(可变长度)
  • Domino LTPA 密钥(20字节)

在与 Domino 做 SSO 的时候,会使用 LTPA Token的认证方式,本文描述它的生成原理,通过它我们可以自己编码生成身份认证的 cookie,实现 SSO。

首先,这个 cookie 由以下部分组成

  • LTPA token 版本(4字节)
  • 创建时间(8字节)
  • 过期时间(8字节)
  • 用户名(可变长度)
  • Domino LTPA 密钥(20字节)

接下来分别说明各部分的具体内容:

  • LTPA token 版本目前 Domino 只有一种值:0x0001
  • 创建时间为以十六进制方式表示的Unix time,例如:2009-04-09 13:52:42 (GMT +8) = 1239256362 = 49DD8D2A。
  • 过期时间=创建时间 + SSO 配置文档的过期时间(LTPA_TokenExpiration域)
  • 用户名为 Names 中用户文档的FullName域值;如:Squall Zhong/Digiwin
  • Domino LTPA 密钥通过 Base64编码后,保存在 SSO 配置文档的LTPA_DominoSecret域中

当然不能将密钥直接发送给浏览器,所以将上述部分合并起来(如上图),计算 SHA-1 校验和

然后用 SHA-1 校验和替换掉 Domino LTPA 密钥,最后再将内容通过 Base64 编码,形成最终的 cookie 发送给浏览器。这样如果 cookie 中的任何内容被修改,校验和就不对了,达到了防篡改的效果。所以最终LTPA Cookie所得到的值为以下公式组成:

  • SHA-1=LTPA版本号+创建时间+过期时间+用户名+Domino LTPA 密钥
  • LTPA Cookie= Base64(LTPA版本号+创建时间+过期时间+用户名+SHA-1)

三 . 实践

3.1 一个 LTPA 的格式

1
2
3
4
5
6
7
8
9
java复制代码[token] = BASE64([header][creation time][expiration time][username][SHA-1 hash])

Header:  LtpaToken 版本(长度4),Domino的固定为[0x00][0x01][0x02][0x03]
Creation time: 创建时间戳(长度8),格式为Unix time比如[2010-03-12 00:21:49]为4B99189D
expiration time:过期时间戳(长度8) 同上
username: 用户名(长度不定)
SHA-1 hash:SHA-1校验和(长度20)
- 由前面所说的密钥和其余的Token资料合并而成,合成公式如下
- [SHA-1 hash] = SHA-1([header][creation time][expiration time][username][shared secret])

001.gif

002.gif

3.2 Domain 解析 Token

  1. Base64解码LtpaToken。
  2. 截取最前面20字节,最后面20字节,中间部分就是用户名。如果用户名在本系统中不正确,返回无效的LtpaToken。
  3. 截取最后面20字节,是SHA-1校验和。用Token中的其余部分和本系统中的密钥生成新的SHA-1校验和,如果2个校验和不匹配。返回无效的LtpaToken。
  4. 当前服务器时间必须大于创建时间,小于失效时间。否则返回无效的LtpaToken。
  5. 最后解析通过了,完成用户的登录

3.3 Java 实现 LTPA Cookie 解析

转载自 @ www.cnblogs.com/cmt/p/14580…

解析方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
java复制代码
// LTPA 3DES 密钥
String ltpa3DESKey = "7dH4i81YepbVe+gF9XVUzE4C1Ca5g6A4Q69OFobJV9g=";
// LTPA 密钥密码
String ltpaPassword = "Passw0rd";
try {
// 获得加密key
byte[] secretKey = getSecretKey(ltpa3DESKey, ltpaPassword);
// 使用加密key解密ltpa Cookie
String ltpaPlaintext = new String(decryptLtpaToken(tokenCipher,
secretKey));
displayTokenData(ltpaPlaintext);
} catch (Exception e) {
System.out.println("Caught inner: " + e);
}

//获得安全Key
private static byte[] getSecretKey(String ltpa3DESKey, String password)
throws Exception {
// 使用SHA获得key密码的hash值
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(password.getBytes());
byte[] hash3DES = new byte[24];
System.arraycopy(md.digest(), 0, hash3DES, 0, 20);
// 使用0替换后4个字节
Arrays.fill(hash3DES, 20, 24, (byte) 0);
// BASE64解码 ltpa3DESKey
byte[] decode3DES = Base64.decodeBase64(ltpa3DESKey.getBytes());
// 使用key密码hash值解密已Base64解码的ltpa3DESKey
return decrypt(decode3DES, hash3DES);
}
//解密LtpaToken
public static byte[] decryptLtpaToken(String encryptedLtpaToken, byte[] key)
throws Exception {
// Base64解码LTPAToken
final byte[] ltpaByteArray = Base64.decodeBase64(encryptedLtpaToken
.getBytes());
// 使用key解密已Base64解码的LTPAToken
return decrypt(ltpaByteArray, key);
}
// DESede/ECB/PKC5Padding解方法
public static byte[] decrypt(byte[] ciphertext, byte[] key)
throws Exception {
final Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
final KeySpec keySpec = new DESedeKeySpec(key);
final Key secretKey = SecretKeyFactory.getInstance("TripleDES")
.generateSecret(keySpec);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(ciphertext);
}

生成 LTPA Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
java复制代码
/**
02 * 为指定用户创建有效的LTPA Token.创建时间为<tt>now</tt>.
03 *
04 * @param username
05 * - 用户名,注:使用用户全称,如:CN=SquallZhong/O=VGOLive Technology
06 * @param creationTime
07 * - 创建时间
08 * @param durationMinutes
09 * - 到期时间,单位:分钟
10@param ltpaSecretStr
11 * - Domino Ltpa 加密字符串
12 * @return - 返回已Base64编码的Ltpa Cookie.
13 * @throws NoSuchAlgorithmException
14 * @throws Base64DecodeException
15 */
16 public static String createLtpaToken(String username,
17 GregorianCalendar creationTime, int durationMinutes,
18 String ltpaSecretStr) throws NoSuchAlgorithmException {
19 // Base64解码ltpaSecretStr
20 byte[] ltpaSecret = Base64.decodeBase64(ltpaSecretStr.getBytes());
21 // 用户名字节数组
22 byte[] usernameArray = username.getBytes();
23 byte[] workingBuffer = new byte[preUserDataLength
24 + usernameArray.length + ltpaSecret.length];
25
26 // 设置ltpaToken版本至workingBuffer
27 System.arraycopy(ltpaTokenVersion, 0, workingBuffer, 0,
28 ltpaTokenVersion.length);
29 // 获得过期时间,过期时间=当前时间+到期时间(分钟)
30 GregorianCalendar expirationDate = (GregorianCalendar) creationTime
31 .clone();
32 expirationDate.add(Calendar.MINUTE, durationMinutes);
33
34 // 转换创建时间至16进制字符串
35 String hex = dateStringFiller
36 + Integer.toHexString(
37 (int) (creationTime.getTimeInMillis() / 1000))
38 .toUpperCase();
39 // 设置创建时间至workingBuffer
40 System.arraycopy(hex.getBytes(), hex.getBytes().length
41 - dateStringLength, workingBuffer, creationDatePosition,
42 dateStringLength);
43
44 // 转换过期时间至16进制字符串
45 hex = dateStringFiller
46 + Integer.toHexString(
47 (int) (expirationDate.getTimeInMillis() / 1000))
48 .toUpperCase();
49 // 设置过期时间至workingBuffer
50 System.arraycopy(hex.getBytes(), hex.getBytes().length
51 - dateStringLength, workingBuffer, expirationDatePosition,
52 dateStringLength);
53
54 // 设置用户全称至workingBuffer
55 System.arraycopy(usernameArray, 0, workingBuffer, preUserDataLength,
56 usernameArray.length);
57
58 // 设置已Base64解码ltpaSecret至workingBuffer
59 System.arraycopy(ltpaSecret, 0, workingBuffer, preUserDataLength
60 + usernameArray.length, ltpaSecret.length);
61 // 创建Hash字符串
62 byte[] hash = createHash(workingBuffer);
63
64 // ltpaToken版本+开始时间(16进制)+到期时间(16进制)+用户全名+SHA-1(ltpaToken版本+开始时间(16进制)+到期时间(16进制)+用户全名)
65 byte[] outputBuffer = new byte[preUserDataLength + usernameArray.length
66 + hashLength];
67 System.arraycopy(workingBuffer, 0, outputBuffer, 0, preUserDataLength
68 + usernameArray.length);
69 System.arraycopy(hash, 0, outputBuffer, preUserDataLength
70 + usernameArray.length, hashLength);
71 // 返回已Base64编码的outputBuffer
72 return new String(Base64.encodeBase64(outputBuffer));
73 }
74…...

通过F5 BIG-IP创建Domino LTPAToken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码
when RULE_INIT {
set cookie_name "LtpaToken" # Don't change this
set ltpa_version "\x00\x01\x02\x03" # Don't change this
set ltpa_secret "b64encodedsecretkey" # Set this to the LTPA secrey key from your Lotus Domino LTPA configuration
set ltpa_timeout "1800" # Set this to the timeout value from your Lotus Domino LTPA configuration
}

when HTTP_REQUEST {
#
# Do your usual F5 HTTP authentication here
#

# Initial values
set creation_time_temp [clock seconds]
set creation_time [format %X $creation_time_temp]
set expr_time_temp [expr { $creation_time_temp + $::ltpa_timeout}]
set expr_time [format %X $expr_time_temp]
set username [HTTP::username]
set ltpa_secret_decode [b64decode $::ltpa_secret]

# First part of token
set cookie_data_raw {}
append cookie_data_raw $::ltpa_version
append cookie_data_raw $creation_time
append cookie_data_raw $expr_time
append cookie_data_raw $username
append cookie_data_raw $ltpa_secret_decode

# SHA1 of first part of token
set sha_cookie_raw [sha1 $cookie_data_raw]

# Final not yet encoded token
set ltpa_token_raw {}
append ltpa_token_raw $::ltpa_version
append ltpa_token_raw $creation_time
append ltpa_token_raw $expr_time
append ltpa_token_raw $username
append ltpa_token_raw $sha_cookie_raw

# Final Base64 encoded token
set ltpa_token_final [b64encode $ltpa_token_raw]

# Insert the cookie
HTTP::cookie insert name $::cookie_name value $ltpa_token_final
}

# Remove Authorization HTTP header to avoid using basic authentication
if { [HTTP::header exists "Authorization"] } {
HTTP::header remove "Authorization"
}
}

总结

LTPA 是一个企业痕迹很重的协议 ,它基本上归属于 IBM , 这就意味着使用如果有困难 , 文档不一定能解决 , 而请求服务支持可不是一个简单的事情 .

当然 ,在使用中 , ltpa cookie 也可以被当成一种 JWT Token 的一种生成方式 , 将其放在Cookie 中 ,再基于 SSO 认证 , 这不算一个 LTPA 体系 , 仅仅是使用了 LTPA 的 Cookie 生成方式 .

感谢

LTPA Cookie原理

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

71 Go语言从入门到精通:Cobra介绍 1、Cobra

发表于 2021-03-26

最近一直在看 Istio (一个 Service Mesh 框架)相关的东西,当看到其源码时发现了一个新东西 Cobra,一查却发现这是个好东西,用的地方可不少,比如:Docker、Kubernetes等都有它的身影存在。为了更好的了解这些开源框架(如,Istio、Kubernetes 等),势必需要对 Cobra 做一个详细的了解,后续可能用到它的地方会很多。今天就 Cobra 做一个整体的介绍,让我们对它能有所认识,或许今后你的项目中也会用到它。

1、Cobra 概述

Cobra 是一个 Golang 包,它提供了简单的接口来创建命令行程序。同时,Cobra 也是一个应用程序,用来生成应用框架,从而开发以 Cobra 为基础的应用。

Cobra.png

1.1 主要功能

cobra 的主要功能如下:

  • 简易的子命令行模式,如 app server, app fetch 等等。
  • 完全兼容 posix 命令行模式。
  • 嵌套子命令 subcommand。
  • 支持全局,局部,串联 flags。
  • 使用 cobra 很容易生成应用程序和命令(cobra init appname 和 cobra add cmdname)。
  • 提供智能化的提示(如,输出 app srver 命令,将提示 你是要输入 app server 吗?)。
  • 自动生成 commands 和 flags 的帮助信息。
  • 自动生成详细的 help 信息,如 app -help。
  • 自动识别帮助 flag、 -h,--help。
  • 自动生成应用程序在 bash 下命令自动完成功能。
  • 自动生成应用程序的 man 手册。
  • 命令行别名。
  • 自定义 help 和 usage 信息。
  • 可选的与 viper 的紧密集成。

对于命令行程序而言,上面这些功能简直就是量身打造。

1.2 应用举例

Cobra 被用于许多Go项目中,例如:Kubernetes、Hugo和Github CLI等,更多广泛使用的项目有:

(来源于:github.com/spf13/cobra…)

  • Arduino CLI
  • Bleve
  • CockroachDB
  • Cosmos SDK
  • Delve
  • Docker (distribution)
  • Etcd
  • Gardener
  • Giant Swarm’s gsctl
  • Git Bump
  • Github CLI
  • GitHub Labeler
  • Golangci-lint
  • GopherJS
  • Helm
  • Hugo
  • Istio
  • Kool
  • Kubernetes
  • Linkerd
  • Mattermost-server
  • Metal Stack CLI
  • Moby (former Docker)
  • Nanobox/Nanopack
  • OpenShift
  • Ory Hydra
  • Ory Kratos
  • Pouch
  • ProjectAtomic (enterprise)
  • Prototool
  • Random
  • Rclone
  • Skaffold
  • Tendermint
  • Twitch CLI
  • Werf

看了这些,一个字“赞”,两个字“优秀”!

了解了 Cobra 后,再去看这些 Kubernetes、etcd、Registry 等开源项目的代码时,也就大概知道如何去看了,这也就是我学习 Cobra 的目的。

2、概念

Cobra是基于命令(commands)、参数(arguments )、选项(flags)而创建的。

在具体了解、使用Cobra前有一些概念需要提前知晓一下:命令(commands)、参数(arguments )、选项(flags)这几个概念。

  • commands: 命令代表行为。
  • arguments : 参数代表命令行参数。
  • flags: 选项代表对命令行为的改变,即命令行选项。

最好的命令行程序在实际使用时,就应该像在读一段优美的语句,能够更加直观的知道如何与用户进行交互。执行命令行程序应该遵循一般的格式: APPNAME VERB NOUN --ADJECTIVE 或 APPNAME COMMAND ARG --FLAG

比如下面的示例:

1
2
3
4
5
ini复制代码# server是 commands,port 是 flag
hugo server --port=1313

# clone 是 commands,URL 是 arguments,brae 是 flag
git clone URL --bare

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Dart翻译】从零开始建立一个Dart服务器 设置 创建一

发表于 2021-03-26

原文地址:medium.com/flutter-com…

原文作者:suragch.medium.com/

发布时间:2020年12月5日-8分钟阅读

无框架的服务器端Dart系列中的第1部分。

image.png

我真的很喜欢能够使用和我写Flutter应用时一样的语言来编写服务器代码。做到这一点的两个主要框架是Aqueduct和Angel。不幸的是,Angel正在被废弃,Aqueduct已经有一段时间没有发布稳定的版本了(截至2020年12月)。

既然A的都出来了,那就到了B计划的时候了。

框架虽好,但总有一点魔力。作为Dart核心库之一的dart:io库,已经包含了制作HTTP服务器所需的低级类和函数。所以本文将教你如何自己创建这样一个服务器。

如果成功的话,这可能会变成一系列的文章。或者如果Aqueduct或者其他Dart框架重新上线,那么我可能会给你指出这个方向。

让我们开始吧。完整的代码在文章的最后,如果你迷路的话。

设置

我想你已经安装了Dart。我正在使用Dart 2.12,这样我就可以习惯使用不可空类型进行编码。目前它自带Flutter的测试版。

1
sh复制代码flutter channel beta

或者使用测试版的Dart SDK。

现在在命令行中创建一个新的Dart项目,名为my_server(或任何你喜欢的)。

1
sh复制代码dart create my_server

用你喜欢的IDE打开那个文件夹。VS Code和IntelliJ都有一个Dart插件。如果你还没有安装,请安装它。

创建一个服务器

用下面的代码替换my_server.dart。

1
2
3
4
5
6
7
8
9
10
dart复制代码import 'dart:io';
Future<void> main() async {
final server = await createServer();
print('Server started: ${server.address} port ${server.port}');
}
Future<HttpServer> createServer() async {
final address = InternetAddress.loopbackIPv4;
const port = 4040;
return await HttpServer.bind(address, port);
}

这将创建一个监听本地主机IP地址(127.0.0.1)、端口为4040的服务器。

你可以像这样从终端启动你的服务器。

1
dart复制代码dart run bin/my_server.dart

你应该看到以下的打印输出。

1
arduino复制代码Server started: InternetAddress('127.0.0.1', IPv4) port 4040

恭喜你!你已经做了一个Dart服务器。你已经建立了一个Dart服务器。这很容易,不是吗?

你已经启动了服务器,但是它还没有真正做任何事情。按Control+C键强制关闭你的程序。

处理HTTP请求

HTTP请求包括GET、POST、PUT和DELETE等。如果你对它们不熟悉,那么你可以在Becoming a backend developer - Part 1: 基础概念阅读更多。

用下面的代码替换主函数。

1
2
3
4
5
dart复制代码Future<void> main() async {
final server = await createServer();
print('Server started: ${server.address} port ${server.port}');
await handleRequests(server);
}

并在my_server.dart中添加handleRequests作为顶层函数。

1
2
3
4
5
6
dart复制代码Future<void> handleRequests(HttpServer server) async {
await for (HttpRequest request in server) {
request.response.write('Hello from a Dart server');
await request.response.close();
}
}

注释。

  • HttpServer类实现了Stream<HttpRequest>。这意味着你可以把它作为一个流来处理一个个进来的请求。
  • 你的handleRequests方法会忽略请求的类型。它只是对所有的东西给出相同的响应:首先写一个字符串(将在响应体中返回),然后关闭响应,将其发回给请求者。

再次运行你的程序,然后在浏览器中打开以下地址。

  • http://localhost:4040/

你应该看到以下结果。

image.png

你的浏览器为此发出了一个GET请求。接下来你将看到如何路由不同类型的请求。

路由不同类型的请求

我们不要对每个请求都一视同仁,而是按类型进行路由。用下面的代码替换handleRequests方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码Future<void> handleRequests(HttpServer server) async {
await for (HttpRequest request in server) {
switch (request.method) {
case 'GET':
handleGet(request);
break;
case 'POST':
handlePost(request);
break;
default:
handleDefault(request);
}
}
}

现在,对于每一个进来的请求,你都要把它路由到一个不同的方法。接下来的几节将探讨如何处理GET、POST和其他任何方法。

我们将首先实现handleGet,所以暂时注释掉handlePost和handleDefault。

1
2
3
4
5
arduino复制代码      case 'POST':
// handlePost(request);
break;
default:
// handleDefault(request);

处理GET请求

在my_server.dart中添加以下顶层代码。

1
2
3
4
5
6
dart复制代码var myStringStorage = 'Hello from a Dart server';
void handleGet(HttpRequest request) {
request.response
..write(myStringStorage)
..close();
}

注释。

  • myStringStorage全局变量在这里代表一个数据库。我们在这里从这个变量中读取数据,并将在下一节中向它写入数据。
  • GET请求不应该改变服务器状态,所以我们只需要在响应中传回myStringStorage的值。

保存你的工作,重新启动服务器。然后在浏览器中再次打开以下地址。

  • http://localhost:4040/

你应该看到和之前一样的结果。

image.png

处理POST请求

POST请求的目的是向服务器添加新资源。

将handleRequests中的handlePost行取消标注,然后在my_server.dart中添加以下顶层函数。

1
2
3
4
5
6
dart复制代码Future<void> handlePost(HttpRequest request) async {
myStringStorage = await utf8.decoder.bind(request).join();
request.response
..write('Got it. Thanks.')
..close();
}

你还需要添加以下导入。

1
dart复制代码import 'dart:convert';

注释:

  • handlePost主体中的第一行从请求中获取传入的数据块,将它们转换为UTF-8格式的字符串,并将它们连接成一个单一的字符串。
  • 一旦有了这个字符串,这个方法就用它来更新全局变量myStringStorage的值。这象征着向数据库写入数据。
  • 因为我们实际上只是更新一个现有的值,而不是创建一个新的值,所以定义REST API使用PUT而不是POST可能更有意义。但对于我们的例子来说,POST是可以的。
  • 还没有任何安全性。如果你把这个服务器放在网上,世界上任何人都可以更新myStringStorage。在未来的文章中,我想谈谈认证和授权。现在你可以阅读服务器端Dart的认证。

保存你的工作并重新启动服务器。

你不能用浏览器进行POST请求,所以你需要另一种工具,比如curl或Postman。你可以通过阅读这篇文章来了解这些和其他的方法来进行POST请求。

我将使用Postman来进行POST请求。打开Postman,执行以下步骤。

image.png

  • 选择POST作为请求类型。
  • 在地址栏中写上http://localhost:4040。
  • 选择Body选项卡。
  • 写上任何字符串,例如Hello。
  • 点击发送按钮。

你应该会收到来自服务器的200 OK响应,并在响应的正文中写上Got it. Thanks.在响应的正文中。

image.png

在Postman中看到的来自服务器的响应

如果你运行另一个GET请求(无论是在你的浏览器或Postman),你应该看到myStringStorage的更新值。

image.png

很好!你成功地从客户端更新了服务器。

处理其他请求

客户端还可以发出很多其他的HTTP请求–比如PUT、PATCH、DELETE等等。你可以在你的路由器中添加更多的方法来处理它们中的任何一个。然而,在这里我们只想说,在我们的服务器上不允许有其他请求。
取消对handleDefault这一行的标注,然后添加下面的顶层方法。

1
2
3
4
5
6
dart复制代码void handleDefault(HttpRequest request) {
request.response
..statusCode = HttpStatus.methodNotAllowed
..write('Unsupported request: ${request.method}.')
..close();
}

注释:这次你将状态码设置为methodNotAllowed。

  • 这次你把状态代码设置为 methodNotAllowed. 这相当于一个405的代码。
  • 你在响应中写一个错误,然后把它发回给客户端。

保存你的工作并重新启动服务器。

在Postman中发送一个PUT请求来测试它。

image.png

Postman中看到的服务器错误响应

干得好 Good work. 你已经为建立自己的服务器开了个好头。你会在下面找到完整的代码。不过首先,看看接下来要采取的一些步骤。

继续

你可以到官方的Dart服务器文档中详细阅读本文所涉及的大部分概念。你也应该看看Dart团队维护的http_server和shelf包。

除非你只支持少量公共数据的简单GET请求,否则在你使用Dart作为一个真正的应用的后端之前,有几个主要的缺失部分需要解决。Dart是一个真正的应用程序的后端:

  • 数据库。你需要能够从Dart与数据库进行通信,以存储和检索资源。
  • 认证:你需要能够从Dart与数据库进行通信以存储和检索资源。你需要能够隐藏私人数据,只允许授权用户更新服务器上的资源。
  • 部署:你需要能够隐藏私人数据,并且只允许授权用户更新服务器上的资源。在你的本地机器上运行服务器是很好的,但它最终需要从外部世界访问。

虽然在技术上并不是必须的,但以下主题也会是有用的知道。

  • 测试: 如果你不测试你的服务器代码,你就不能确定做一个改变不会破坏它。
  • 文件。你要返回给客户的可能不仅仅是数据库中的字符串。最终你也需要服务于文件。
  • 并发性。如果你的服务器有一个以上的核心,你可能会使用它。为此你要在另一个隔离区上启动你的服务器。
  • CI/CD:手动上传服务器代码的变化,过一段时间就会变得有点老。如果能建立一个系统,在服务器有变化时自动运行测试和更新,那就更好了。

没有承诺,但我想继续写关于这些主题的文章,这样你就可以学会如何自己做这些事情,而不需要依赖一个框架。但即使你真的使用了框架,知道事情是如何工作的,也会让你更有效率。

完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
dart复制代码import 'dart:convert';
import 'dart:io';

Future<void> main() async {
final server = await createServer();
print('Server started: ${server.address} port ${server.port}');
await handleRequests(server);
}

Future<HttpServer> createServer() async {
final address = InternetAddress.loopbackIPv4;
const port = 4040;
return await HttpServer.bind(address, port);
}

Future<void> handleRequests(HttpServer server) async {
await for (HttpRequest request in server) {
switch (request.method) {
case 'GET':
handleGet(request);
break;
case 'POST':
handlePost(request);
break;
default:
handleDefault(request);
}
}
}

var myStringStorage = 'Hello from a Dart server';

void handleGet(HttpRequest request) {
request.response
..write(myStringStorage)
..close();
}

Future<void> handlePost(HttpRequest request) async {
myStringStorage = await utf8.decoder.bind(request).join();
request.response
..write('Got it. Thanks.')
..close();
}

void handleDefault(HttpRequest request) {
request.response
..statusCode = HttpStatus.methodNotAllowed
..write('Unsupported request: ${request.method}.')
..close();
}

www.deepl.com 翻译

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GitOps 一种云原生的持续交付模型 什么是GitOp

发表于 2021-03-26

在此之前您可能听说过“GitOps”,但并不知道它到底是什么,除了GitOps,您可能还听说过DevOps,或者AIOps、GOps等,是的,现在是“Ops”盛行的时代。

GitOps是一种实现持续交付的模型,它的核心思想是将应用系统的声明性基础架构和应用程序存放在Git的版本控制库中。Choerodon猪齿鱼在构建持续交付流水线时参考了GitOps,并进行了实践,俗话说“兵马未动,理论先行”,在本文中,将重点阐述GitOps工作流程的原理和模式,以及将它们应用在生产和大规模运行Kubernetes中的一些实践经验。在下一篇文章中,将介绍Choerodon猪齿鱼是如何实践和落地GitOps,从而构建了一个可重复且可靠的交付过程。

GitOps,90%的最佳实践,10%有意思的新东西需要我们去构建。 —— 《GitOps - Operations by Pull Request》

来自:www.weave.works

这篇文章是根据Weave Cloud的几篇关于GitOps的文章翻译整理而来:

  • GitOps: Operations by Pull Request
  • The GitOps Pipeline - Part 2
  • GitOps - Part 3: Observability
  • GitOps - Part 4: Application Delivery Compliance and Secure CICD

主要内容

  • 什么是GitOps?
  • GitOps的主要优点
  • GitOps的应用场景——适合云原生的持续交付
  • GitOps的基本原则
  • 最佳实践
  • 拉式流水线——Pull Request操作
  • GitOps工作流
  • 可视化
  • 应用交付的合规性和安全的CI/CD
  • GitOps带来的价值

什么是GitOps?

GitOps是一种持续交付的方式。它的核心思想是将应用系统的声明性基础架构和应用程序存放在Git版本库中。

将Git作为交付流水线的核心,每个开发人员都可以提交拉取请求(Pull Request)并使用Git来加速和简化Kubernetes的应用程序部署和运维任务。通过使用像Git这样的简单熟悉工具,开发人员可以更高效地将注意力集中在创建新功能而不是运维相关任务上(例如,应用系统安装、配置、迁移等)。

GitOps: versioned CI/CD on top of declarative infrastructure. Stop scripting and start shipping. t.co/SgUlHgNrnY — Kelsey Hightower (@kelseyhightower) January 17, 2018

GitOps的主要优点

通过GitOps,当使用Git提交基础架构代码更改时,自动化的交付流水线会将这些更改应用到应用程序的实际基础架构上。但是GitOps的想法远不止于此——它还会使用工具将整个应用程序的实际生产状态与基础架构源代码进行比较,然后它会告诉集群哪些基础架构源代码与实际环境不匹配。

通过应用GitOps最佳实践,应用系统的基础架构和应用程序代码都有“真实来源”——其实是将基础架构和应用程序代码都存放在gitlab、或者github等版本控制系统上。这使开发团队可以提高开发和部署速度并提高应用系统可靠性。

将GitOps理论方法应用在持续交付流水线上,有诸多优势和特点:

  • 安全的云原生CI/CD管道模型
  • 更快的平均部署时间和平均恢复时间
  • 稳定且可重现的回滚(例如,根据Git恢复/回滚/ fork)
  • 与监控和可视化工具相结合,对已经部署的应用进行全方位的监控

GitOps应用场景——满足云原生环境下的持续交付

作为CI / CD流水线的方案,GitOps被描述为软件开发过程的“圣杯”。 由于没有单一工具可以完成流水线中所需的所有工作,因此可以自由地为流水线的不同部分选择最佳工具。可以从开源生态系统中选择一组工具,也可以从封闭源中选择一组工具,或者根据使用情况,甚至可以将它们组合在一起,其实,创建流水线最困难的部分是将所有部件粘合在一起。

不管如何选择构造自己的交付流水线,将基于Git(或者其他版本控制工具)的GitOps最佳实践应用在交付流水线中都是一个不二选择,这将使构建持续交付流水线,以及后续的推广变得更加容易,这不仅从技术角度而且从文化角度来看都是如此。

当然,GitOps也不是万能的,它也有相应的应用场景。

不可变基础设施

应用都需要运行在多台机器上,它们被组织成不同的环境,例如开发环境、测试环境和生产环境等等。需要将相同的应用部署到不同的机器上。通常需要系统管理员确保所有的机器都处于相同的状态。接着所有的修改、补丁、升级需要在所有的机器中进行。随着时间的推移,很难再确保所有的机器处于相同的状态,同时越来越容易出错。这就是传统的可变架构中经常出现的问题。这时我们有了不可变架构,它将整个机器环境打包成一个单一的不可变单元,而不是传统方式仅仅打包应用。这个单元包含了之前所说的整个环境栈和应用所有的修改、补丁和升级,这就解决了前面的问题。 —— 摘自InfoQ的《关于不可变架构以及为什么需要不可变架构》作者 百占辉

“不可变基础设施”这一概念不是刚刚冒出来的,它也不是必须需要容器技术。然而,通过容器,它变得更易于理解,更加实用,并引起了业内广泛注意。“不可变基础设施”让我们以全新的方式理解和面对应用系统,尤其是使以微服务为代表的分布式系统在部署、运营等方面变得不那么复杂,而有很好的可控性。

那么,如何比较方便地在实际的生产过程中应用“不可变基础设施”,这给业界也提出了另外一个问题。GitOps是在具体Kubernetes的应用实践中出现的,GitOps需要依托于“不可变基础架构”才能发挥其作用。在一定程度上说,“不可变基础架构”为GitOps的出现创造了必要的条件,反过来GitOps应用Kubernetes的容器编排能力,能够迅速的使用镜像搭建出应用系统所需的组件。

声明性容器编排

Kubermetes作为一个云原生的工具,可以把它的“声明性”看作是“代码”,声明意味着配置由一组事实而不是一组指令组成,例如,“有十个redis服务器”,而不是“启动十个redis服务器,告诉我它是否有效”。

借助Kubermetes的声明性特点,应用系统的整个配置文件集可以在Git库中进行版本控制。通过使用Git库,应用程序更容易部署到Kubernetes中,以及进行版本回滚。更重要的是,当灾难发生时,群集的基础架构可以从Git库中可靠且快速地恢复。

Kubernetes等云原生工具的声明性体现在可以对实例、容器、网络、存储、CPU等配置通过一组代码方便的表达出来,Kubernetes等云原生工具可以利用这些配置代码运行出来一套基于容器的应用系统,例如YMAL,

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: nginx-deployment

spec:

replicas: 1

template:

metadata:

labels:

app: nginx

spec:

containers:

  • name: nginx

image: registry.choerodon.com.cn/operation-choerodon-dev/nginx-demo:1.13.5-alpine

ports:

  • containerPort: 80

GitOps充分利用了不可变基础设施和声明性容器编排,通过GitOps可以轻松地管理多个部署。为了最大限度地降低部署后的变更风险,无论是有意还是偶然的“配置偏差”,GitOps构建了一个可重复且可靠的部署过程,在整个应用系统宕机或者损坏情况下,为快速且完全恢复提供了所需条件。

GitOps的基本原则

以下是几条在云原生环境中,GitOps的原则:

● 任何能够被描述的内容都必须存储在Git库中

通过使用Git作为存储声明性基础架构和应用程序代码的存储仓库,可以方便地监控集群,以及检查比较实际环境的状态与代码库上的状态是否一致。所以,我们的目标是描述系统相关的所有内容:策略,代码,配置,甚至监控事件和版本控制等,并且将这些内容全部存储在版本库中,在通过版本库中的内容构建系统的基础架构或者应用程序的时候,如果没有成功,则可以迅速的回滚,并且重新来过。

● 不应直接使用Kubectl

作为一般规则,不提倡在命令行中直接使用kubectl命令操作执行部署基础架构或应用程序到集群中。还有一些开发者使用CI工具驱动应用程序的部署,但如果这样做,可能会给生产环境带来潜在不可预测的风险。

● 调用Kubernetes 的API的接口或者控制器应该遵循 Operator 模式

调用Kubernetes 的API的接口或者控制器应该遵循 Operator 模式(什么是Operator 模式?),集群的状态和Git库中的配置文件等要保持一致,并且查看分析它们之间的状态差异。

最佳实践

以Git作为事实的唯一真实来源

Git是每个开发人员工具包的一部分。学习起来感觉自然而且不那么令人生畏,而且工具本身也非常简单。 通过使用Git作为应用系统的事实来源,几乎可以操作所有东西。例如,版本控制,历史记录,评审和回滚都是通过Git进行的,而无需使用像kubectl这样的工具。

所以,Git是GitOps形成的最基础的内容,就像第一条原则“任何能够被描述的内容都必须存储在Git库中 ”描述的那样:通过使用Git作为存储声明性基础架构和应用程序代码的存储仓库,可以方便地监控集群,以及检查比较实际环境的状态与代码库上的状态是否一致。所以,我们的目标是描述系统相关的所有内容:策略,代码,配置,甚至监控事件和版本控制等,并且将这些内容全部存储在版本库中,在通过版本库中的内容构建系统的基础架构或者应用程序的时候,如果没有成功,则可以迅速的回滚,并且重新来过。

拉式流水线——Pull Request操作

▌推送流水线

目前大多数CI / CD工具都使用基于推送的模型。基于推送的流水线意味着代码从CI系统开始,通过一系列构建测试等最终生成镜像,最后手动使用“kubectl”将任何更改推送到Kubernetes集群。

很多开发人员不愿意在CI中启动CD部署流程,或者使用命令行工具操作启动CD部署流程的原因可能是这样做会将集群的用户和密码等公布出去。虽然可以有措施保护CI / CD 脚本和命令行,但是这些操作毕竟还是在集群外部非可信区工作的。所以,类似做法是不可取的,会给系统安全带来潜在的风险。

具有集群外读/写(R/W)权限的典型推送流水线:

  • CI运行测试,输出传递到容器映像存储库。
  • CD系统自动部署容器(或根据请求,即手动)。

▌拉式流水线

在GitOps中,镜像被拉出并且凭证保留在集群中:

Git库是拉式流水线模式的核心,它存储应用程序和配置文件集。开发人员将更新的代码推送到Git代码库; CI工具获取更改并最终构建Docker镜像。GitOps检测到有镜像,从存储库中提取新镜像,然后在Git配置仓库中更新其YAML。然后,GitOps会检测到群集已过期,并从配置库中提取已更改的清单,并将新镜像部署到群集。

GitOps的流水线

在上节中介绍了GitOps采用拉式模式构建交付流水线,本节将详细地介绍在构建GitOps流水时需要注意哪些事情,有哪些最佳实践。

▌GitOps流水线

这是一个新图,显示部署上游的所有内容都围绕Git库工作的。在“拉式流水线”中讲过,开发人员将更新的代码推送到Git代码库,CI工具获取更改并最终构建Docker镜像。GitOps的Config Update检测到有镜像,从存储库中提取新镜像,然后在Git配置仓库中更新其YAML。然后,GitOps的Deploy Operator会检测到群集已过期,并从配置库中提取已更改的清单,并将新镜像部署到群集。

使用群集内部的Deploy Operator,群集凭据不会在生产环境之外公开。一旦将Deploy Operator安装到集群与Git仓库建立连接,线上环境中的任何更改都将通过具有完全回滚的Git pull请求以及Git提供的方便审计日志完成。

▌自动git→集群同步

由于没有单一工具可以完成流水线中所需的所有工作,可以从开源生态系统中选择一组工具,也可以从封闭源中选择一组工具,或者根据使用情况,甚至可以将它们组合在一起,其实,创建流水线最困难的部分是将所有部件粘合在一起。要实现GitOps,必须要开发出新的组件,用于粘合这些工具,实现拉式交付流水线。

**部署和发布自动化是应用落实GitOps,并使交付流水线工作的基础。**GitOps不仅要保证,当开发人员通过Git更新配置文件集的时候,GitOps流水线要自动根据最新的配置文件状态更新线上环境,而且GitOps还要能够实时比对Git库中配置文件集最新的状态与线上环境最新的状态保持一致。

在上节中提到了两个名词:Config Update 和 Deploy Operator,根据GitOps的实践,Config Update 和 Deploy Operator是需要进行设计开发的,它们是实现GitOps流水线必须的关键组件。GitOps赋予了它们神奇的魔法,它们既是自动化容器升级和发布到线上环境的工具,可能也要负责服务、部署、网络策略甚至路由规则等任务。因此,Config Update 和 Deploy Operator是映射代码,服务和运行集群之间所有关系的“粘合剂”。

当然,您可以根据具体的设计,赋予各种其他的功能,但是自动同步是一定需要的,确保如果对存储库进行任何更改,这些更改将自动部署到线上环境中。

▌仅部署容器和配置

GitOps建议不直接将应用程序部署到线上环境中,而是将应用程序和相关配置打包成镜像,并存储到镜像库中,最后,通过镜像的方式生成容器,并部署到线上环境中。

容器为什么如此重要?在GitOps模型中,我们使用不可变基础架构模式。一旦代码在Git中提交,GitOps就不希望任何其他内容发生变化,这样可以最大限度地降低系统潜在不确定性、不一致性风险。例如,需要将相同的应用部署到不同的机器上。通常需要系统管理员确保所有的机器都处于相同的状态。接着所有的修改、补丁、升级需要在所有的机器中进行。随着时间的推移,很难再确保所有的机器处于相同的状态,同时越来越容易出错。然而,容器是比较完美地解决了这个问题,当然,使用虚拟机是可以的,显然使用容器更加方便。

GitOps的可观察性

“可观察性就像生产中的驱动测试一样。如果你不知道如何确定它是否正常工作,请勿接受 pull request。@mipsytipsy “ - Adriano Bastos

在GitOps中,使用Git库来存储应用系统的配置文集和应用程序,它确保开发人员将所有对于应用系统的配置和程序的新增、修改等都通过Git库进行版本控制,使Git成为配置和程序的唯一真实来源。而GitOps的可观察性则是确保线上环境的真实状态与Git库中的保持一致性。本章节将给大家介绍GitOps的可观察性。

▌可观察性是另一个真理来源

在GitOps中,我们使用Git作为系统所需状态的真实来源。例如,如果应用系统宕机,GitOps可以回滚到之前正确状态。而可观察性是系统实际运行状态的真实来源,系统开发人员或者运维人员可以监控系统的状态。这是一张显示流程的图片。

▌通过观察需寻找问题的答案

如果大家使用Kubernetes作为云原生环境和容器编排工具,相信大家会有这样的感触,虽然Kubernetes是一个非常棒的编排容器平台,但是随之而来的缺乏友好的可视化管理界面给开发人员或者运维人员带来诸多不便。例如:

  • 我的部署成功了吗?我的系统现在处于工作的状态,我现在可以回家吗?
  • 我的系统与以前有什么不同?我可以使用Git或我们的系统历史记录来检查吗?
  • 我的改变是否改善了整体用户体验?(与系统正确性相对)
  • 我在信息中心找不到我的新服务(例如RED指标)
  • 这个故障是否与我上次的服务更新事件有关,还是和其他操作有关系?

大家可能会想到通过监控服务器的CPU、内存、网络等,以及应用的日志,甚至微服务的调用链等来解决问题。是的,这个没有错,能够得到一些反馈信息,但是使用过类似监控的开发人员或者运维人员也会感觉,这些监控仪表盘给我们大量冗繁的信息,需要认真地甄别,而且有很多信息在这些仪表盘中是获得不到的。这意味着需要创建新的仪表盘,用于监控新的指标和内容。

▌GitOps的可观察性

可观察性可被视为Kubernetes 持续交付周期的主要驱动因素之一,因为它描述了在任何给定时间系统的实际运行状态。观察运行系统以便理解和控制它。新功能和修复程序被推送到git并触发部署管道,当准备好发布时,可以实时查看正在运行的集群。此时,开发人员可以根据此反馈返回到管道的开头,或者将映像部署并释放到生产集群。

在这里GitOps引入一个新的工具:Diffs,用来监控对比系统状态。即:

  • 验证当前线上系统的状态是否和Git库中描述的状态一致,例如,我上一次发布是否符合期望?
  • 提醒开发人员不一致状态,以及相应的明细信息。

在文章前面讲过,在Git库中存储的实际上是“声明性基础设施”,例如Kubernetes的YAML文件,用以构建应用系统所需的各种组件、域名、网络等配置信息。Diffs需要读取Git库中配置信息,同时,通过API等读取集群的相应信息,并进行比对。

例如,Kubernetes集群:所需的Kubernetes状态可能是“有4个redis服务器”。Diffs定期检查群集,并在数量从4变化时发出警报。一般而言,Diffs将YAML文件转换为运行状态查询。

GitOps是面向发布的操作模型,请参见下图。交付速度取决于团队在此周期中绕过各个阶段的速度。

应用交付合规性和安全性

由于以安全的方式跟踪和记录更改,因此合规性和审计变得微不足道。使用Diffs等比较工具还可以将Git库中定义的集群状态与实际运行的集群进行比较,从而确保更改与实际情况相符。

▌在Git中记录所有的操作日志

通过上面文章的叙述,开发人员或者运维人员通过Git操作系统配置和应用程序的新建和更新等。通过Git客户端git commit /git merge的所有操作都会Git库记录下来,审计员可以查看Git,看看谁做了任何更改,何时以及为何以及如何影响正在运行的系统部署。当然,可以根据自身的需求定制不同的交付合规性。相较于直接进入服务器操作或者通过Kubctl操作集群,Git记录了每一个操作步骤,这些可以为合规性和审计提供完整的操作日志。

▌角色和权限控制

几乎所有的Git库都提供角色和权限控制,与开发和运维无关的人员没有权限操作Git库。而不是直接把服务器或者集群的操作权限散发出去,这样特别容易引起安全泄露。

GitOps带来的好处

▌更加快速地开发

借助GitOps的最佳实践,开发人员可以使用熟悉的Git工具,便捷地将应用程序和其对应的配置文件集持续部署到Kubernetes等云原生环境,提高业务的敏捷度,快速地相应用户的需求,有助于增加企业市场的竞争力。

▌更好地进行运维

借助GitOps,可以实现一个完整的端到端的交付流水线。不仅可以实现拉式的持续集成流水线和持续部署流水线,而且系统的运维操作可以通过Git来完成。

▌更强大的安全保证

几乎所有的Git库都提供角色和权限控制,与开发和运维无关的人员没有权限操作Git库。而不是直接把服务器或者集群的操作权限散发出去,这样特别容易引起安全泄露。

▌更容易合规的审计

由于以安全的方式跟踪和记录更改,因此合规性和审计变得微不足道。使用Diffs等比较工具还可以将集群状态的可信定义与实际运行的集群进行比较,从而确保跟踪和可审计的更改与实际情况相符。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySql 你知道什么情况下适合使用Join 联表查询吗 ?

发表于 2021-03-25

志在巅峰的攀登者,不会陶醉在沿途的某个脚印之中,在码农的世界里,优美的应用体验,来源于程序员对细节的处理以及自我要求的境界,年轻人也是忙忙碌碌的码农中一员,每天、每周,都会留下一些脚印,就是这些创作的内容,有一种执着,就是不知为什么,如果你迷茫,不妨来瞅瞅码农的轨迹。

  • 优美的音乐节奏带你浏览这个效果的编码过程
  • 坚持每一天,是每个有理想青年的追求
  • 追寻年轻人的脚步,也许你的答案就在这里
  • 如果你迷茫 不妨来瞅瞅这里

1 前言

如下我这里有两张表,表t1为某活动的报名信息表,部分建表 DDL 如下:

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `t1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`activity_id` bigint(20) DEFAULT '0' COMMENT '关联的活动信息',
`user_id` bigint(20) DEFAULT '0' COMMENT '报名人的ID',
`create_time` datetime DEFAULT NULL COMMENT '报名时间',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 COMMENT='活动报名表';

表t2为 投票信息表,也就是说 t2表中保存的是给t1表中的报名用户投票记录信息,部分建表 DDL 如下:

1
2
3
4
5
6
7
8
9
sql复制代码
CREATE TABLE `t2` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`activity_user_id` bigint(20) DEFAULT '0' COMMENT '关联的活动报名信息 t1表中的id',
`vote_user_id` bigint(20) DEFAULT '0' COMMENT '投票者的信息',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `activity_user_id`(`activity_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=226 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户投票记录表';

现在 我 t1 表中有 10 条数据
在这里插入图片描述
t2表中 84 条数据,如图所示

在这里插入图片描述

现在有一需求就是查询 户的投票记录以及报名信息,那么我们需要从 t1表中获取报名信息,然后再从t2表中获取每个用户的投票记录。

那么无非就是有两种查询思维,一种是先取t1,再循环取t2,另一种是使用 join ,那到底使用哪种,你是怎么决定的呢???

1 我们先来看看 循环查询

在不使用join的情况下,我们需要先从t1表中查出这用户的报名信息,然后循环从t2表中查询投票信息,这个过程如下


  • 执行select * from t1 ,每一行数据记为 C,这一步会对t1表进行全表扫描,我们t1表中是10条数据,全表扫描10行

在这里插入图片描述

type = ALL,全表扫描,MYSQL扫描全表来找到匹配的行

  • 然后循环遍历这 10 行数据,从每一行 数据 C 中取出字段 id 的值; 执行select * from t2 where activity_user_id=id;(activity_user_id走的是索引树搜索) 把返回的结果和 C 构成结果集的一行。

在表t2中,满足 t1表中id为12的有49条数据
在这里插入图片描述
这个过程中 扫描 49行数据

在这里插入图片描述

type = ref ,使用非唯一性索引或者唯一索引的前缀扫描,返回匹配某个单独值的记录行。

满足 id 为13的有 35条数

在这里插入图片描述
这个过程中扫描35行数据

在这里插入图片描述

然后 t1 表中其他 8条数据在表 t2中没有记录,所以查询过程中各扫描一行。

在这个过程中,这样查询下来,需要在业务代码中自己组装循环查询,t1表扫描 10行,t2表扫描 35 + 49 + 8 = 92,查询完成总共扫描 102行数据。


2 使用 join 时

当使用 join 时,可以这样写 :(使用 STRAIGHT_JOIN 保证固定联表顺序)

1
2
3
4
5
sql复制代码SELECT
*
FROM
t1 STRAIGHT_JOIN t2
ON (t1.id = t2.activity_user_id )

满足条件的有 84 条数据
在这里插入图片描述

这个语句的执行流程是这样的:

  • 第一步 从表 t1 中读入一行数据 C;
  • 第二步从数据行 C 中,取出 id 字段到表 t2 的 activity_user_id 索引树中搜索;
  • 第三步 取出表 t2 中满足条件的行,跟 C 组成一行,作为结果集的一部分;
  • 第四步 重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。

在这里插入图片描述

这个过程是先遍历表 t1,然后根据从表 t1 中取出的每行数据中的 id 值,去表 t2 中查找满 足条件的记录,这个过程称为 “Index Nested-Loop Join”,简称 NLJ。

在这个过种中,t1表是驱动表,是走全表扫描,t2是被驱动表,是走树搜索,所以在 join过程中,应该让小表作驱动表。

此时 我们将 t2表中的 activity_user_id 索引删除

在这里插入图片描述
我们再查询一下

在这里插入图片描述
我们可以清楚的看到当不走索引搜索时,t1与t2都走了全表扫描,

执行过程如下

  • 第一步扫描表 t1,顺序读取数据行放入 join_buffer 中,假设放完第 3 行 join_buffer 满了,继续 第二步操作;
  • 第二步 扫描表 t2,把 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回;
  • 第三步 清空 join_buffer;
  • 第四步 继续扫描表 t1,顺序读取最后的 7 行数据放入 join_buffer 中,继续执行第 二 步。

这时候由于表 t1 被分成了两次放入 join_buffer 中,导致表 t2 会被扫描两次,这个过程就是 “Block Nested-Loop Join”。

显然 这两种情况 “Index Nested-Loop Join” 与 “Block Nested-Loop Join” 分析得出,如果可以使用到被驱动表中的索引,就可以使用 join 来查询。

如果无法使用到被驱动表的索引查询,这样可能要扫描被驱动表很多次,会占用大量的系统资源,所以这种情况下 join 尽量不要用。


完毕

不局限于思维,不局限语言限制,才是编程的最高境界。

以小编的性格,肯定是要录制一套视频的,随后会上传

有兴趣 你可以关注一下 西瓜视频 — 早起的年轻人

当然也有微信公众号的每日积累分享 (biglead) 我的大前端生涯

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang实践之博客(三) Web框架Gin的使用|Go主

发表于 2021-03-25

前言

今天完成项目基本骨架搭建及使用Gin作为web框架

目标

  • 完成基础骨架的搭建
  • 引入Gin作为Web开发框架
  • Gin路由的简单使用

项目结构

image.png

使用go get 下载Gin

终端执行指令go get -u github.com/gin-gonic/gin

image.png

下载完成后在go.mod中可查看到依赖
image.png

Gin的Hello World

在main.go输入代码:

1
2
3
4
5
6
7
8
9
10
11
go复制代码package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/hi", func(c *gin.Context) {
c.JSON(200, "hello world")
})
r.Run(":8888") // 监听并在 127.0.0.1:8888 上启动服务
}

右键启动项目

image.png

访问127.0.0.1:8888

image.png

控制器

Controller–>新建目录HomeControoler–>新建文件HomeController.go

输入代码:

1
2
3
4
5
6
7
8
9
10
11
go复制代码package HomeControoler

import "github.com/gin-gonic/gin"

func Index(c *gin.Context) {
c.JSON(200,"hello world")
}

func Hi(c *gin.Context) {
c.JSON(200,"hi")
}

路由器

Routers–>新建文件HomeController.go
输入代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package Routers

import (
"github.com/gin-gonic/gin"
"golang-blog/Controller/HomeControoler"
)

func Init(router *gin.Engine) {
home := router.Group("Home")

// 1.首位多余元素会被删除(../ or //);
//2.然后路由会对新的路径进行不区分大小写的查找;
//3.如果能正常找到对应的handler,路由就会重定向到正确的handler上并返回301或者307.(比如: 用户访问/FOO 和 /..//Foo可能会被重定向到/foo这个路由上)
router.RedirectFixedPath = true

{
home.GET("/", HomeControoler.Index)
home.GET("/hi",HomeControoler.Hi)
}

router.Run(":8888") // 监听并在 127.0.0.1:8888 上启动服务
}

Url忽略大小写

router.RedirectFixedPath = true

main代码调整

1
2
3
4
5
6
7
8
9
10
11
go复制代码package main

import (
"github.com/gin-gonic/gin"
"golang-blog/Routers"
)

func main() {
router := gin.Default()
Routers.Init(router)
}

调整后的目录结构

image.png

启动项目

请求http://127.0.0.1:8888/Home/

image.png

请求http://127.0.0.1:8888/Home/Hi

image.png

项目地址

github.com/panle666/Go…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【JVM进阶之路】四:直面内存溢出和内存泄漏 1、内存溢出

发表于 2021-03-25

在Java中,和内存相关的问题主要有两种,内存溢出和内存泄漏。

  • 内存溢出(Out Of Memory) :就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。
  • 内存泄露 (Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。

1、内存溢出

在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能。

JDK 1.8内存区域

1.1、Java堆溢出

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

我们来看一个代码的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}

接下来,我们来设置一下程序启动时的JVM参数。限制内存大小为20M,不允许扩展,并通过参数-XX:+HeapDumpOnOutOf-MemoryError 让虚拟机Dump出内存堆转储快照。

在Idea中设置JVM启动参数如下图:

Idea设置JVM参数

运行一下:

堆内存溢出异常

Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”。 Java堆文件快照文件dump到了java_pid18728.hprof文件。

要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如JProfiler、Eclipse Memory Analyzer等)对Dump出来的堆转储快照进行分析。

看到内存占用信息如下:

Jprofiler 打开的堆转储快照文件

然后可以查看代码问题如下:

Jprofiler查看代码问题

常见堆JVM相关参数:

-XX:PrintFlagsInitial: 查看所有参数的默认初始值-XX:PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms: 初始堆空间内存(默认为物理内存的1/64)
-Xmx: 最大堆空间内存(默认为物理内存的1/4)
-Xmn: 设置新生代大小(初始值及最大值)
-XX:NewRatio: 配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认15)
-XX:+PrintGCDetails:输出详细的GC处理日志
打印GC简要信息:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保

1.2、虚拟机栈和本地方法栈溢出

HotSpot虚拟机中将虚拟机栈和本地方法栈合二为一,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈,有两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常。

1.2.1、StackOverflowError

HotSpot虚拟机不支持栈的动态扩展,在HotSpot虚拟机中,以下两种情况都会导致StackOverflowError。

  • 栈容量过小

如下,使用Xss参数减少栈内存容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* vm参数:-Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}

}

运行结果:

栈内存溢出

  • 栈帧太大

如下,通过一长串变量,来占用局部变量表空间。

carbon

运行结果:

image-20210324211958180

无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候, HotSpot虚拟机抛出的都是StackOverflowError异常。

1.2.2、OutOfMemoryError

虽然不支持动态扩展栈,但是通过不断建立线程的方式,也可以在HotSpot上产生内存溢出异常。

需要注意,这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。因为操作系统给每个进程的内存时有限的,线程数一多,自然会超过进程的容量。

创建线程导致内存溢出异常 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码/**
* vm参数:-Xss2M
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}

public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
thread.start();
}
}

public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

以上是一段比较有风险的代码,可能会导致系统假死,运行结果如下:

image-20210324213320530

1.3、方法区和运行时常量池溢出

这里再提一下方法区和运行时常量池的变迁,JDK1.7以后字符串常量池移动到了堆中,JDK1.8在直接内存中划出一块区域元空间来实现方区域。

String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的 字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,永久代本身内存不限制可能会出现错误:

1
java复制代码java.lang.OutOfMemoryError: PermGen space

1.4、本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。

直接通过反射获取Unsafe实例,通过反射向操作系统申请分配内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* vm参数:-Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

运行结果:

image-20210324215114989

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况。

2、内存泄漏

内存回收,简单说就是应该被垃圾回收的对象没有被垃圾回收。

内存泄漏

在上图中:对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长,Y生命周期结束的时候,垃圾回收器不会回收对象Y。

我们来看几个内存泄漏的例子:

  • 静态集合类引起内存泄漏

静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。

1
2
3
4
5
6
7
8
9
java复制代码public class OOM {
static List list = new ArrayList();

public void oomTests(){
Object obj = new Object();

list.add(obj);
}
}
  • 单例模式:

和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。

  • 数据连接、IO、Socket等连接

创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码        try {
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url", "", "");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("....");
} catch (Exception e) {

}finally {
//不关闭连接
}
}
  • 变量不合理的作用域

一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。

1
2
3
4
5
6
7
8
9
java复制代码public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代码
//由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放
object = null;
}
}
  • 引用了外部类的非静态内部类

非静态内部类(或匿名类)的初始化总是需要依赖外部类的实例。默认情况下,每个非静态内部类都包含对其包含类的隐式引用,若在程序中使用这个内部类对象,那么即使在包含类对象超出范围之后,也不会被回收(内部类对象隐式地持有外部类对象的引用,使其成不能被回收)。

  • Hash 值发生改变

对象Hash值改变,使用HashMap、HashSet等容器中时候,由于对象修改之后的Hah值和存储进容器时的Hash值不同,会导致无法从容器中单独删除当前对象,造成内存泄露。

  • ThreadLocal 造成的内存泄漏

ThreadLocal 可以实现变量的线程隔离,但若使用不当,就可能会引入内存泄漏问题。


参考:

【1】:周志朋编著《深入理解Java虚拟机:JVM高级特性与最佳实践》

【2】:周志朋等翻译《Java虚拟机规范》

【3】:封亚飞编著《揭秘Java虚拟机 JVM设计原理与实现》

【4】:Java 中的内存溢出和内存泄露是什么?我给你举个有味道的例子

【5】:那个小白还没搞懂内存溢出,只能用案例说给他听了

【6】:Intellij IDEA 集成 JProfiler 性能分析神器

【7】:JVM系列(二) - JVM内存区域详解

【8】1篇文章搞清楚8种JVM内存溢出(OOM)的原因和解决方法

【9】:十种JVM内存溢出的情况,你碰到过几种?

【10】:JVM系列(二):JVM 内存泄漏与内存溢出及问题排查

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点认证协议 普及篇之ADFS , WS-Federat

发表于 2021-03-25

总文档 :文章目录

Github : github.com/black-ant

  • 纯约束型协议 : OAuth , SAML , OIDC , CAS
  • 服务器类协议 : RADIUS , Kerberos , ADFS
  • 认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)
  • 认证服务器(附带) : AD , LDAP , ADFS

这一篇的深度不会太深 , 主要是对概念的普及 !

一 . 前言

之所以这一篇把 ADFS 放上来主要是因为它和 SAML 协议比较接近, 所以通过它来完善一下 SAML 的认证体系 .

先说一下 ADFS 和 WS-Federation 是什么 .

WIF : .NET Framework 类

WIF (Windows Identity Foundation) : 这个.NET 库用于在NET应用程序和依赖方中驱动基于声明的身份验证。它还可以用作WS-Trust客户端和构建自定义STS

WS-Federation

WS-Federation(简称: WS-Fed ) 属于Web Services Security(简称: WS-Security、WSS: 针对web-service安全性方面扩展的协议标准集合) 的一部分,是由OASIS(www.oasis-open.org)发布的标准协议

作用 : WS-Federation的基本目标就是为了能够简化联合(所谓联合: Federation,是指一组相互之间存在安全共享资源关系的领域集合)服务的开发。

流程 : 由依赖方和STS用于协商安全令牌的协议。应用程序使用WS联合从STS请求安全令牌,STS(大多数时候)使用WS联合协议将SAML安全令牌返回给应用程序。这通常是通过HTTP(获取、post和重定向)实现的。

ADFS : Windows 基于 AD 引用实现了 Federation 的服务

AD FS,Active Directory Federation Services : Active Directory联合身份验证服务 , AD FS 使用基于Claims的访问控制验证模型来实现联合认证。它提供 Web 单一登录技术,这样只要在会话的有效期内,就可对一次性的对用户所访问的多个Web应用程序进行验证。

由Microsoft生产并构建在Windows Identity Foundation (WIF)上的现成安全令牌服务(STS) ,依赖AD进行身份验证。可以在活动(SOAP web服务)或被动(web站点)场景中使用,并支持SAML令牌、WS-Federation、WS-Trust和SAML- protocol

总结 : 这就是说 ADFS 是 Federation 协议的实现 , AD 是身份管理 , ADFS 是一个服务 , 并且实现了很多协议.

  • ADFS 1.0 - Windows Server 2003 R2 (additional download)
  • ADFS 1.1 - Windows Server 2008 and Windows Server 2008 R2.
  • ADFS 2.0 - Windows Server 2008 and Windows Server 2008 R2 (download from Microsoft.com)
  • ADFS 2.1 - Windows Server 2012.
  • ADFS 3.0 - Windows Server 2012 R2.

STS : 身份认证成员 , 安全令牌颁发者

STS是位于依赖方应用程序和用户之间的代理。STS是安全令牌的颁发者 , STS 有2种 角色 ,

  • 身份提供者(IdP) : 当它们验证用户时
  • 联合提供者(FP) : 当它们位于信任链的中间,充当其他IdP的“依赖方”时

我们来看一张 ADFS 的概念图 :

ADFS002.jpg

二 . 深入 WS-Federation

2.1 WS-Federation 成员解析

WS-Trust
WS-Trust定义了Security Token Service (STS)协议用于请求/发布安全令牌;
通过定义服务模型,安全令牌服务(STS)以及用于请求/发布这些安全令牌的协议(由WS-Security使用并由WS-SecurityPolicy描述)来提供联合的基础.
STS发出了一个由资源提供者信任的令牌,因此请求者必须通过STS验证自己才能获得令牌,然后向资源提供者请求资源以及声明/令牌.

ws-fedration.jpg

2.2 Metadata Model元数据模型

发起点: 请求者希望最终通信的服务。 给定目标服务的元数据端点引用(metadpoint reference,MEPR)允许请求者获取关于服务的所有需求元数据(例如联合元数据、通信策略、 WSDL 等)。

作用: 联合元数据描述有关如何在联合中使用服务以及服务如何参与联合的设置和信息。

身份: 联合元数据只是服务总体元数据的一个组成部分, 还有一个通信策略,描述发送到服务的 web 服务消息的需求,以及服务、端点和消息的组织结构的 WSDL 描述。

范围: : 联合元数据(如通信策略)的范围可以限定到服务、端点,甚至是消息。 此外,所描述的信息类型可能根据联合中的服务角色而有所不同(例如,目标服务、安全令牌服务…)。

三 . 深入 ADFS

3.1 ADFS 流程

为了更清楚 , 我这里画了一个图来说明
ADFSjpg.jpg

ADFS002.jpg

3.2 核心问题

身份令牌 : 有认证就会有令牌
ADFS 种会传递 security token 来表明用户 , 安全令牌包含关于用户的声明,如用户名、组成员、UPN、电子邮件地址、管理员详细信息和电话号码 . 应用程序会自行通过令牌判断该如何处理人员 , 所以 :

  • ADFS 做身份验证决策 , 应用做权限管理以及行为控制
  • 双方之间的联合信任通过证书进行管理 , 而AD FS服务器可以自签名安全令牌签名和加密证书
    • 发布者和消费应用程序或服务之间的HTTPS通信需要一个公钥基础设施(PKI)

IDP 和 SP
这涉及到 ADFS 是一个认证方还是非认证方 , 答案是 : ADFS 中都可以配置 , ADFS 即可以让应用认证 , 也可以去 SSO 中完成认证 , 只不过作为 IDP 时 , AD DS认证成功后,AD FS的STS (security token service)组件会发出安全令牌。

怎么理解联合

就像一个 SSO 集群 , 可以去对方的地方认证 , 也可以让对方到你这里认证 , 只是通过一定的协议颁发令牌即可

ADFS 认证的方式 :

  • 表单验证: 这种身份验证方法适用于发布在公司网络之外的资源,并且客户端可以通过internet访问这些资源。
  • Windows集成验证: 这是默认的认证方法,适用于在企业网络中发布的资源,并且只能从内网资源访问。

3.3 其他知识点

Account Store/Attribute store :
Active Directory联合身份验证服务使用术语“属性存储”来指组织用来存储其用户帐户及其关联的属性值的目录或数据库

claims provider (CP):
声明提供方,是联合身份验证服务,负责收集和验证用户,构建声明,并将声明打包为安全令牌(Security Token),ADFS本身就是典型的CP

claims provider trust (CPT) :
受ADFS信赖的其他CP,根据Claims Rules向ADFS发送声明,受信任的CP用户可以访问ADFS配置(relying party trust)中的relying party

relying party(RP) :
信赖方,即声明的消费方,需要依赖ADFS进行用户验证的应用程序,信赖方向Claim Provider请求并接收claims provider传过来的Claims(Claim需要根据Claim Rule进行转换/映射)

relying party trust :
可以理解为ADFS的“白名单”,受信任的RP才能向接收到ADFS发送的Token/Claims

claims rule :
声明的转换规则(通过 Claim Engine执行),规则即:如果服务器收到声明A,则颁发声明B,ADFS向外(relying party应用)发出的声明受claim rule约束,需要在claim rules事先约定(需要进行转换/映射)

声明引擎(Claims Engine) :
联合身份验证服务中的唯一实体,负责在您配置的所有联合信任关系中运行每个规则集(Claims Rule),并将输出结果移交给声明管道(Claims Pipeline)

信任的传递 :
为安全起见,ADFS服务器部署在内网,不直接对外网提供服务,而是通过部署在外围网络(屏蔽子网)的代理服务器进行转发信任(trust ) ,信任的方向(Trust Direction)是One-way trust 或者Two-way trust , Trust Transitivity(信任的传递)是可传递信任

四 . 知识点

4.1 WS联合会 和 SAML 的区别

目标

  • WS : 浏览器重定向(URL中的消息)-浏览器POST(HTML格式的消息)-SOAP(通过HTTP)
  • SAML : 浏览器重定向(URL中的消息)-浏览器POST(HTML格式的消息)-工件(引用断言+ SOAP调用)-SOAP(通过HTTP)- 反向SOAP(通过HTTP)

支持安全令牌

  • WS : SAML断言,X509证书,kerberos
  • SAML : SAML断言-其他任何令牌类型(通过SubjectConfirmation 元素嵌入在SAML断言中)

服务依赖关系

  • WS : WS-Trust ,WS-Policy,WS-SecurityPolicy。WS-Eventing (订阅单一注销消息)。WS-Transfer和WS-ResourceTransfe
  • SAML : 无

元数据

  • WS :
    • 联盟元数据格式的描述。
    • 说明此元数据的安全传输。
    • 可以保存有关多个联盟的信息
  • SAML :
    • SSO等的元数据描述。
    • 按角色进行组织(IdP,SP,属性请求者,PDP …)

登出

  • WS : 可以由SP或(主要)STS发起,后者将向所有RP发送注销消息
  • SAML : 一样

断言

  • WS : 基于参考令牌的使用(即,可以进行WS-Transfer GET检索实际令牌的EPR )
  • SAML :
    • 工件配置文件(完整的SAML响应)
    • URI绑定(仅获得SAML断言)
    • SAML还定义的机制来请求或查询现有断言(由受试者或声明类型)

认证等级

  • WS :

-WS-Trust定义参数(AuthenticationType )。WS-Fed指定预定义的值(例如Ssl,SslSndKey,智能卡)。

  • SAML : SAML 2.0提供了更广泛和可扩展的认证上下文

隐私

  • WS :
    • 请求者可以表达其请求的安全令牌的保护要求(protectData 要求和STS的确认)。
    • 隐私声明可以通过WS-Transfer检索。
  • SAML :
    • SAML提供一系列选项,以约束断言的使用&范围
    • 这些约束可能源自SP或IdP

联盟

  • WS :
    • 由提供身份映射及其管理的Pseudonym服务(可选…)执行。
    • 假名服务可以独立于IP / STS,并且可以存储与假名关联的令牌。
    • 映射可以由请求者(主要…)或资源(SP)的所有者创建。
    • 对化名的所有操作(获取,设置,创建或删除)都是通过WS-Transfer(及其扩展名WS-ResourceTransfer来过滤这些操作的范围)完成的。
  • SAML :
    • 身份映射是IdP的一部分。尽管灵活性较低(?),但它避免了假名服务与断言生成器(WS- *中的IP / STS)之间还需要另一个协议。
    • 映射是由IdP创建的,但可以由IdP或SP进行更改
    • SAML在其AuthNRequest中没有提供与ClientPseudonym相似的概念 。名称ID管理协议(和SPProviderID)不用于瞬时ID映射。

总结 :

感觉还是没讲清楚 , 这方面能查阅的资料比较少 ,而英文看到的又有误差 , 很多都没有解释清楚 ,

ADFS 是一个很重量级的认证方式 ,通常在一些大型企业中可以看到 , 说句心里话 , 碰到 Windows 的东西就怂了 , 太难查问题了 , 文档是多 ,但是一个概念居然有4,5个描述他的文档 , 还每个不同 , 就是折磨了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…698699700…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%