我们提供了一个服务只有合法的请求才能访问,如何证明请求的合法性就是认证的本意,只有证明了合法身份才能保证系统的安全性,才知道来者何人,后续才能进行身份授权和鉴权,可以看出认证的重要性,本文只聚焦于服务和服务之间的认证
IP白名单
流程图
通过IP白名单认证比较简洁,Server2(s2)维护合法的IP白名单列表,通过Client IP即可确认请求服务信息
实战应用
1. 创建库表
sql
CREATE TABLE `server_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`server_sign` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '服务标识',
`server_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '服务名称',
`server_desc` varchar(300) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '服务描述',
`is_enable` tinyint DEFAULT NULL COMMENT '是否启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
CREATE TABLE `server_ip` (
`id` bigint NOT NULL AUTO_INCREMENT,
`server_sign` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '服务标识',
`ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip地址',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
2. 维护服务信息
sql
INSERT INTO `server_info` VALUES (1, 's1', 'server1', 'test server 1', 1, now(), now());
INSERT INTO `server_ip` VALUES ('s1', '10.0.0.1', now(), now());
INSERT INTO `server_ip` VALUES ('s1', '192.168.31.229', now(), now());
3. 从DB获取数据
- store 从db中查询数据
go
type ServerStore struct {
DB *gorm.DB
}
func (s *ServerStore) SelectByIp(ip string) (result *model.ServerList, err error) {
err = s.DB.First(&result, "ip = ?", ip).Error
return
}
func (s *ServerStore) SelectBySign(sign string) (result *model.Server, err error) {
err = s.DB.First(&result, "server_sign = ?", sign).Error
return
}
- service 从store中获取server信息
go
type ServerSvc struct {
store *store.ServerStore
}
func (s *ServerSvc) GetServerByIp(ip string) (*model.Server, error) {
serverIp, err := s.store.SelectByIp(ip)
if err != nil {
return nil, err
}
if serverIp == nil {
return nil, nil
}
return s.store.SelectBySign(serverIp.Sign)
}
4. 在Middleware中完成认证
go
func Authentication(c *gin.Context) {
ip := c.ClientIP()
// 通过client IP获取服务信息
server, err := service.ServerSvcIns.GetServerByIp(ip)
if err != nil {
log.Println(err)
c.Next()
return
}
c.Set("server", server)
c.Next()
}
5. Gin中使用Middleware
go
g.Use(middleware.Authentication)
小结
在Middleware中配置认证模块,通过Client IP获取服务信息完成认证后存储在Context中将认证信息传递到下一层。IP白名单方式认证的是整个服务节点,容器环境借助于k8s的容器调度也可以使用这个认证方式:在pod创建时注册IP;销毁时注销IP。
完整代码:github.com/lidenger/wr...
Access Token
流程图
- Server2(s2)管理员维护服务基础信息,包含AKSK(AK:AccessKey,SK:SecretKey)
- Server1(s1)通过AK,SK获取AccessToken
- s2检测AK,SK生成AccessToken后入库并返回给s1
- s1请求功能接口时携带AccessToken
- s2验证AccessToken通过后将认证的服务信息配置到Context中传递到下一层
实战应用
1. 创建库表增加测试数据
sql
CREATE TABLE `server_info`
...
CREATE TABLE `aksk` (
`id` bigint NOT NULL AUTO_INCREMENT,
`server_sign` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`ak` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'access key',
`sk` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'secret key',
`is_enable` tinyint DEFAULT NULL COMMENT '是否启用',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
CREATE TABLE `access_token` (
`id` bigint NOT NULL AUTO_INCREMENT,
`server_sign` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '服务标识',
`access_token` varchar(300) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'token',
`is_valid` tinyint DEFAULT NULL COMMENT '是否有效,1是,0否',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
CREATE TABLE `order_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '订单编号',
`order_status` tinyint DEFAULT NULL COMMENT '订单状态',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
INSERT INTO `server_info` VALUES (1, 's1', 'server1', 'test server 1', 1, now(), now());
INSERT INTO `aksk` (`server_sign`, `ak`, `sk`, `is_enable`, `remark`, `create_time`, `update_time`) VALUES ('s1', 'b4a9bcc1825f11ef', 'c26c6f9c825f11efa5ae8c32', 1, 'secret1', now(), now());
INSERT INTO `aksk` (`server_sign`, `ak`, `sk`, `is_enable`, `remark`, `create_time`, `update_time`) VALUES ('s1', '7dcea8db82bd11ef', '81b377ed82bd11ef826b8c32', 1, 'secret2', now(), now());
INSERT INTO `order_info` (`order_no`, `order_status`, `create_time`, `update_time`) VALUES ('425c3b29832711ef8f0f8c32', 1, now(), now());
2. 创建和检测AccessToken
go
// 验证ak sk
func (s *AccessTokenSvc) verifyAkSk(serverSign, ak, sk string) (bool, error) {
if ak == "" || sk == "" {
return false, nil
}
aksk, err := s.akStore.SelectByAk(ak)
if err != nil {
return false, err
}
if aksk == nil {
return false, nil
}
if aksk.Sk == sk && aksk.Sign == serverSign {
return true, nil
}
return false, nil
}
// GenAccessToken 验证AK,SK生成AccessToken
func (s *AccessTokenSvc) GenAccessToken(serverSign, ak, sk string) (string, error) {
verify, err := s.verifyAkSk(serverSign, ak, sk)
if err != nil {
return "", err
}
if !verify {
return "", errors.New("无效的ak sk")
}
tokenM := &model.AccessToken{}
tokenM.Sign = serverSign
tokenM.Token = util.Generate32Str()
now := time.Now()
tokenM.CreateTime = now
tokenM.UpdateTime = now
tokenM.IsValid = 1
err = s.store.Insert(tokenM)
if err != nil {
return "", err
}
return tokenM.Token, nil
}
// VerifyAccessToken 验证AccessToken
func (s *AccessTokenSvc) VerifyAccessToken(token string) (string, error) {
if token == "" {
return "", nil
}
tokenM, err := s.store.SelectByToken(token)
if err != nil {
return "", err
}
if tokenM == nil {
return "", nil
}
// 已经无效,直接返回无效
if tokenM.IsValid == 0 {
return "", nil
}
// 有效,检测是否已失效
if tokenM.IsValid == 1 {
if tokenM.CreateTime.Add(TOKEN_VALID_HOUR).After(time.Now()) {
return tokenM.Sign, nil
}
// 已无效,更新状态
err = s.store.UpdateToExpire(token)
if err != nil {
return "", err
}
return "", nil
}
return "", errors.New(fmt.Sprintf("无效的Token状态: %d", tokenM.IsValid))
}
3. 在Middleware中增加认证
go
func Authentication(c *gin.Context) {
token := c.GetHeader("accessToken")
if token == "" {
c.Next()
return
}
sign, err := service.AccessTokenSvcIns.VerifyAccessToken(token)
if err != nil {
log.Printf("+%v", err)
c.Next()
return
}
server, err := service.ServerSvcIns.GetBySign(sign)
if err != nil {
log.Printf("+%v", err)
c.Next()
return
}
c.Set("server", server)
c.Next()
}
注:这里只是认证,本意是识别请求来源并非拦截所以只需要将认证信息传递到下一层即可
4. 在Middleware中增加鉴权
go
func Authority(c *gin.Context) {
server, isExists := c.Get("server")
if !isExists || !checkAuth(server) {
result.R(c, errors.New("无权访问"), "")
c.Abort()
return
}
log.Printf("%+v", server)
c.Next()
}
注:权限并非本文重点,这里模拟鉴权可以看出认证是权限的基础
5. 使用Middleware
go
g.Use(middleware.Authentication)
...
g.POST("/access_token", handler.GenAccessToken)
v1 := g.Group("/v1")
v1.Use(middleware.Authority)
{
order := v1.Group("/order")
order.GET(":orderNo", handler.FetchOrderByNo)
}
6. 模拟Server1获取订单信息
go
// 获取access token
func GetAccessToken() (string, error) {
url := "http://127.0.0.1/access_token"
param := &GenAccessTokenParam{
Sign: "s1",
Ak: "b4a9bcc1825f11ef",
Sk: "c26c6f9c825f11efa5ae8c32",
}
jsonBytes, err := json.Marshal(param)
if err != nil {
return "", err
}
resp, err := http.Post(url, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
return "", err
}
return analysisResp[string](resp)
}
// 从Server2获取订单信息
func GetOrderByNo(orderNo string) (Order, error) {
token, err := GetAccessToken()
if err != nil {
return Order{}, err
}
url := "http://127.0.0.1/v1/order/" + orderNo
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return Order{}, err
}
req.Header.Set("accessToken", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return Order{}, err
}
return analysisResp[Order](resp)
}
func analysisResp[T any](resp *http.Response) (T, error) {
...
}
...
func TestGetOrderByNo(t *testing.T) {
order, err := GetOrderByNo("425c3b29832711ef8f0f8c32")
if err != nil {
t.Fatal(err)
}
t.Log(order)
}
小结
模拟了Server1请求订单信息的过程,使用自己服务的AKSK得到AccessToken设置到Header中请求订单信息,Server2检测AccessToken合法性完成对Server1服务的认证,这个case中可以看出认证和权限的区别和关系。
完整代码:github.com/lidenger/wr...
对请求签名
流程图
- Server1(s1)管理员申请AKSK,Server2(s2)创建AKSK并入库,s1管理员在s1服务器上部署AKSK
- s1发起功能请求前对本次请求签名
- s2通过s1的AKSK和请求参数验证请求签名并完成认证
- s2完成鉴权,处理功能逻辑,返回结果
实战应用
1. 创建库表增加测试数据
sql
CREATE TABLE `server_info`
...
CREATE TABLE `aksk`
...
CREATE TABLE `order_info`
...
2. s1请求订单信息
go
func GetOrderByNo(orderNo string) (Order, error) {
domain := "http://127.0.0.1"
url := "/v1/order/" + orderNo
req, err := http.NewRequest("GET", domain+url, nil)
if err != nil {
return Order{}, err
}
req.Header.Set("Authorization", GenSignatureHeader(url))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return Order{}, err
}
return analysisResp[Order](resp)
}
// GenSignatureHeader 生成Authorization
func GenSignatureHeader(url string) string {
algorithm := sha256.New
ak := "b4a9bcc1825f11ef"
sk := "c26c6f9c825f11efa5ae8c32"
nonce := util.Generate16Str()
t := time.Now().Unix()
ts := strconv.Itoa(int(t))
sign := Signature(algorithm, ak, sk, nonce, ts, url)
return fmt.Sprintf("algorithm=%s,ak=%s,time=%s,nonce=%s,signature=%s", "HMAC-SHA256", ak, ts, nonce, sign)
}
// Signature 签名
func Signature(hash func() hash.Hash, ak, sk, nonce, time, url string) string {
dataBuilder := &strings.Builder{}
dataBuilder.WriteString(ak)
dataBuilder.WriteString(nonce)
dataBuilder.WriteString(time)
dataBuilder.WriteString(url)
data := dataBuilder.String()
h := hmac.New(hash, []byte(sk))
h.Write([]byte(data))
digested := h.Sum(nil)
return hex.EncodeToString(digested)
}
注: 请求前将本次请求签名设置Header的Authorization字段中,Authorization示例:
go
Authorization: algorithm=HMAC-SHA256,ak=b4a9bcc1825f11ef,time=1728305354,nonce=8e8b8b7584aa11ef,signature=72e65eac3cab1655f7164f87b9f9c4a3dbccae1f5f8a9479e273c76e64c17163
3. s2对请求验证,完成服务认证
- 在middelware中认证
go
// 在middelware中认证
g.Use(middleware.Authentication)
- 从header中获取auth数据完成认证
go
...
authorization := c.GetHeader("Authorization")
...
// 获取Server1的aksk
params := AnalysisHeaderSignature(authorization)
aksk, err := service.AkSkSvcIns.GetByAk(params["ak"])
// 校验有效期,可以约束一个auth的有效时间
time.Unix(int64(t), 0).Add(VALID_SECOND).Before(time.Now())
// 验签
err = VerifySignature(params, aksk.Sk, url)
// 验签通过完成认证,获取服务信息
server, err := service.ServerSvcIns.GetBySign(aksk.ServerSign)
小结
s1管理员在服务提供者管理平台获取自己服务的AKSK并部署在服务器上后续认证环节不需要传输降低了密钥泄露的风险,服务提供者s2根据指定的签名算法验签完成认证,这里除了AKSK以外并没有其它额外需要存储的数据
完整代码:github.com/lidenger/wr...
JWT-基础版
简介
JWT:JSON Web Token,可以简单理解为将数据以JSON形式封装再加一个签名,JWT = JSON + 签名,这里用到了密码学中的签名,签名就是为了防篡改,jwt.io/
流程图
- Server1(s1)管理员申请服务获取aksk并完成部署
- s1使用aksk申请Token
- Server2(s2)生成JWT并返回
- s1请求功能接口时携带JWT
- s2验证JWT完成认证,后续完成鉴权,返回结果
实战应用
1. 创建库表增加测试数据
sql
CREATE TABLE `server_info`
...
CREATE TABLE `order_info`
...
CREATE TABLE `aksk`
...
-- 签名使用的密钥
CREATE TABLE `sys_secret`
...
2. s1通过aksk获取jwt
go
func GetJwt() (string, error) {
url := Domain + "/genToken"
param := &AkSk{
Ak: AK,
Sk: SK,
}
jsonBytes, err := json.Marshal(param)
...
resp, err := http.Post(url, "application/json", bytes.NewReader(jsonBytes))
...
jwt, err := analysisResp[string](resp)
return jwt, err
}
3. s1获取订单信息时携带jwt
go
func GetOrderByNo(orderNo string) (*Order, error) {
url := "/v1/order/" + orderNo
req, err := http.NewRequest("GET", Domain+url, nil)
...
jwt, err := GetJwt()
...
req.Header.Set("Authorization", jwt)
resp, err := http.DefaultClient.Do(req)
...
return analysisResp[*Order](resp)
}
4. s2启动时加载所有签名使用的密钥
go
var akCache map[string]*model.AkSk
...
func (s *AkSkSvc) LoadAllAkSk() {
akCache = make(map[string]*model.AkSk)
ms, err := s.store.SelectAll()
...
for _, m := range ms {
akCache[m.Ak] = m
}
}
...
service.AkSkSvcIns.LoadAllAkSk()
5. s2生成jwt,header.payload.signature
go
func (s *JwtSvc) GenJwt(serverSign string) (string, error) {
// header
header := &model.JwtHeader{}
header.Algorithm = "HS256"
header.Issuer = "Server2"
// payload
payload := &model.JwtPayload{}
payload.Nonce = util.Generate16Str()
payload.ServerSign = serverSign
payload.Timestamp = time.Now().Unix()
secret := s.RandomSecret()
payload.SecretID = secret.ID
// signature
key := []byte(secret.Secret)
data, err := json.Marshal(payload)
...
sign, err := util.Signature(data, key, header.Algorithm)
...
jwt := &model.Jwt{
Header: header,
Payload: payload,
Signature: sign,
}
return util.JwtMarshal(jwt)
}
6. s2验证jwt,使用jwt中的密钥ID获取对应的密钥并对payload签名比对jwt中的签名
go
func VerifyJwt(jwtStr string) (string, error) {
jwt, err := util.JwtUnmarshal(jwtStr)
...
payload, err := json.Marshal(jwt.Payload)
...
secret := FetchSecretByID(jwt.Payload.SecretID)
key := []byte(secret.Secret)
header := jwt.Header
sign, err := util.Signature(payload, key, header.Algorithm)
...
isVerify := hex.EncodeToString(sign) == hex.EncodeToString(jwt.Signature)
if isVerify {
return jwt.Payload.ServerSign, nil
} else {
return "", errors.New("验签失败,非法JWT")
}
}
7. s2在middleware中启用验证jwt,认证通过后获取请求者服务信息并传递到下一层
go
func Authentication(c *gin.Context) {
...
jwt := c.GetHeader("Authorization")
...
serverSign, err := service.VerifyJwt(jwt)
if err != nil {
result.AuthErr(c, "无效的JWT")
c.Abort()
}
...
server, err := service.ServerSvcIns.GetBySign(serverSign)
...
c.Set("server", server)
}
小结
s1通过AKSK获取JWT并作为凭证请求订单信息,s2验证JWT完成认证,在这个流程中认证的信息包含在JWT中,基于密码学的签名方式JWT无法被篡改,所有签名使用的密钥都在s2系统内部,s2也不需要存储JWT信息,只需要验证通过即可
完整代码:
client: github.com/lidenger/wr...
server: github.com/lidenger/wr...
JWT-增强版
简述
在上小节中使用了JWT的方式认证,但常规的JWT有以下几点需要关注
- 颁发出去的JWT如何销毁:换个角度其实就是销毁的JWT验证失败即可,可以在JWT ID,AK,服务标识等多维度治理
- JWT中的信息是明文:这其实有利有弊,明文数据好排查问题,不好在于如果要让JWT中传递的数据也不被泄露,明文确实是一个问题,这个也好解决使用密码学中的加解密即可,client只需要知道这是自己身份的凭证即无需关心内部细节
- JWT数据很多时需要消耗很多流量和带宽:通过Protobuf序列化JWT减少体积
应用实战
在payload中增加JWT ID,结构如下:
go
type JwtHeader struct {
// 算法: HS256,HS512
Algorithm string `json:"alg"`
// 颁发者
Issuer string `json:"iss"`
}
type JwtPayload struct {
// JWT ID
JwtID string `json:"jwtID"`
// AK
AK string `json:"ak"`
// 服务标识
ServerSign string `json:"serverSign"`
// 密钥ID
SecretID int64 `json:"secretID"`
// Nonce
Nonce string `json:"nonce"`
// IP
IP string `json:"ip"`
// 颁发时间
Timestamp int64 `json:"timestamp"`
}
}
type Jwt struct {
Header *JwtHeader `json:"header"`
Payload *JwtPayload `json:"payload"`
Signature []byte `json:"signature"`
}
验证注销的JWT
1. 单个JWT维度
创建记录注销JWT的表,如果当前DB请求有压力可以将注销信息放入其它抗高压的DB中,例如:Redis
go
CREATE TABLE `jwt_deactive` (
`id` bigint NOT NULL AUTO_INCREMENT,
`jwt_id` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
系统启用时加载所有注销的JWT ID
go
...
service.JwtSvcIns.LoadAllDeactiveJwt()
...
验证JWT时增加是否被注销检测
go
func VerifyJwt(jwtStr string) (string, error) {
jwt, err := util.JwtUnmarshal(jwtStr)
...
if _, exists := deactiveCache[jwt.Payload.JwtID]; exists {
return "", errors.New("非法JWT")
}
...
2. AK维度,如果AKSK被泄露或有风险可以将AK对应的JWT全部注销
go
func VerifyJwt(jwtStr string) (string, error) {
jwt, err := util.JwtUnmarshal(jwtStr)
...
if AkSkSvcIns.IsDisable(jwt.Payload.AK) {
return "", errors.New("非法JWT")
}
...
3. 服务维度,通过禁用服务的形式将颁发给某个服务的JWT全部注销
go
func VerifyJwt(jwtStr string) (string, error) {
jwt, err := util.JwtUnmarshal(jwtStr)
...
if ServerSvcIns.IsDisable(jwt.Payload.ServerSign) {
return "", errors.New("非法JWT")
}
...
JWT加密
1. 生成JWT时加密
go
...
jwt := &model.Jwt{
Header: header,
Payload: payload,
Signature: sign,
}
...
jwtJson, err := util.JwtMarshal(jwt)
...
// Encrypt AES/CBC/PKCS#7
secret1 := FetchSecretByID(1)
cipher, err := util.Encrypt([]byte(secret1.Secret), []byte(jwtJson))
...
2. 验证JWT时解密
go
secret1 := FetchSecretByID(1)
jwtDecode, err := hex.DecodeString(jwtStr)
...
// Decrypt AES/CBC/PKCS#7
jwtData, err := util.Decrypt([]byte(secret1.Secret), jwtDecode)
jwt, err := util.JwtUnmarshal(string(jwtData))
...
JWT体积
使用protobuf序列化
- 定义IDL文件 jwt.proto
go
syntax = "proto3";
option go_package = "/protogen";
message JwtHeader {
string algorithm = 1;
string issuer = 2;
}
message JwtPayload {
string jwt_id = 1;
string ak = 2;
string server_sign = 3;
int64 secret_id = 4;
string nonce = 5;
string ip = 6;
int64 timestamp = 7;
}
message Jwt {
JwtHeader header = 1;
JwtPayload payload = 2;
bytes signature = 3;
}
- 执行命令生成go代码:jwt.pb.go
bash
protoc -I=protogen --go_out=./ protogen/jwt.proto
- 使用protobuf序列化jwt
go
...
jwt := &protogen.Jwt{
Header: header,
Payload: payload,
Signature: sign,
}
jwtData, err := proto.Marshal(jwt)
...
数据长度由原来的510降低到256大概减少了原来的50%
小结
本小节主要针对JWT中的注销,明文数据防泄漏,数据长度等方面提供了解决方案,可以发现经过本节操作后和常规的JWT有些不同了,其实我们应该把重心放到认证上,JWT只是我们完成认证的一种工具
完整代码:github.com/lidenger/wr...
mTLS
双向证书校验,我们通过https协议访问网站时浏览器已经替我们校验了网站的证书了,这个过程是单向的,也就是说服务端不关心是谁访问了它,双向就是不仅客户端要校验服务端的证书,相同的服务端也要校验客户端证书,了解原理后这种机制也适用于两个服务之间的认证
流程图
- s1管理员到s2申请证书并部署到s1系统中
- s2创建s2系统证书并入库
- s1和s2互相验证证书
- s2验证通过完成认证,执行功能逻辑返回结果
应用实战
1. 创建库表增加测试数据
sql
CREATE TABLE `server_info`
...
CREATE TABLE `order_info`
...
2. 生成client和server两个根证书(CA)
shell
# 创建client root ca证书
openssl genrsa -out clientroot.key 4096
openssl req -new -x509 -key clientroot.key -subj "/C=cn/ST=beijing/L=beijing/O=lidenger/OU=software/CN=clientroot" -addext "extendedKeyUsage=clientAuth,serverAuth" -addext "subjectAltName=DNS:*.lidenger.com" -days 3650 -out clientroot.crt
# 创建server root ca证书
openssl genrsa -out serverroot.key 4096
openssl req -new -x509 -key serverroot.key -subj "/C=cn/ST=beijing/L=beijing/O=lidenger/OU=software/CN=serverroot" -addext "extendedKeyUsage=clientAuth,serverAuth" -addext "subjectAltName=DNS:*.lidenger.com" -days 3650 -out serverroot.crt
3. 使用CA分别签发client和server的服务证书
shell
# 使用clientroot签发s1证书
openssl genrsa -out s1.key 4096
openssl req -new -x509 -key s1.key -subj "/C=cn/ST=beijing/L=beijing/O=lidenger/OU=software/CN=s1" -addext "extendedKeyUsage=clientAuth,serverAuth" -addext "subjectAltName=DNS:*.lidenger.com" -days 3650 -out s1.crt -CA clientroot.crt -CAkey clientroot.key
# 使用serverroot签发s2证书
openssl genrsa -out s2.key 4096
openssl req -new -x509 -key s2.key -subj "/C=cn/ST=beijing/L=beijing/O=lidenger/OU=software/CN=s2" -addext "extendedKeyUsage=clientAuth,serverAuth" -addext "subjectAltName=DNS:*.lidenger.com" -days 3650 -out s2.crt -CA serverroot.crt -CAkey serverroot.key
4. s2启动服务时配置CA证书和s2的服务证书和私钥
go
pool := x509.NewCertPool()
// 这里需要配置client ca证书,因为要验证client的服务证书
cert, err := os.ReadFile("./config/crt/clientroot.crt")
if err != nil {
panic(err)
}
pool.AppendCertsFromPEM(cert)
server := &http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", *port),
Handler: g,
TLSConfig: &tls.Config{
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
},
}
err = server.ListenAndServeTLS("./config/crt/s2.crt", "./config/crt/s2.key")
if err != nil {
log.Fatal(err)
}
5. s1请求功能接口时配置CA证书和server1的服务证书和私钥
go
domain := "https://server2.lidenger.com"
url := "/v1/order/" + orderNo
// 配置server ca证书
pool := x509.NewCertPool()
// 这里需要配置server ca证书,因为要验证server的服务证书
caCrt, err := os.ReadFile("serverroot.crt")
if err != nil {
return nil, err
}
pool.AppendCertsFromPEM(caCrt)
// 配置s1证书
s1Crt, err := tls.LoadX509KeyPair("s1.crt", "s1.key")
if err != nil {
return nil, err
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: pool,
Certificates: []tls.Certificate{s1Crt},
},
}
client := &http.Client{Transport: tr}
resp, err := client.Get(domain + url)
注:为了方便测试在本机设置了host:127.0.0.1 server2.lidenger.com
6. 在s2服务的middleware中完成认证获取服务信息保存在context中传递到一次层
go
// 获取client证书信息
tlsState := c.Request.TLS
if tlsState == nil {
c.Next()
return
}
clientCert := tlsState.PeerCertificates[0]
if clientCert == nil {
c.Next()
return
}
serverSign := clientCert.Subject.CommonName
// 获取服务信息
server, err := service.ServerSvcIns.GetBySign(serverSign)
if err != nil {
log.Println(err)
c.Next()
return
}
c.Set("server`", server)
c.Next()
小结
生成client和server的ca证书用于验证ca签发的服务证书,这样在代码层面只需要配置一套CA的证书即可完成服务证书的校验,还有个好处在于服务证书的更换,例如,s1的client服务证书私钥需要更换那么作为s2的服务证书和私钥就无需更换,只需要服务端使用client ca重新签发新的client证书即可
注意:上面代码为了演示将证书和私钥放到了代码中,在实际项目中对于这些敏感数据需要加密存储在DB或部署在root权限的主机中
完整代码:
client: github.com/lidenger/wr...
server: github.com/lidenger/wr...
摸鱼时刻
今天给大家介绍一种小众的淡水热带鱼-海龙
水质:偏碱性
适应温度 :20 - 30℃
饲养难度:较难
尺寸:成年15厘米左右