Dynamic RSA System 设计与实现

一、背景

在实现了静态的加解密工具后,感觉不够灵活,想设计一个动态生成 RSA KeyPair 的中间系统,暂且称为 Dynamic RSA System,以达到自动化维护信安高墙的效果。

加解密和签名校验工具_余衫马的博客-CSDN博客_校验和工具RSA加密、解密、签名、验证 golang 封装https://blog.csdn.net/weixin_47560078/article/details/126042242

二、架构 V1

杀鸡蔫用牛刀?也是借此机会再把遗忘的技术再回顾一下吧。简单介绍一下数字标的流程:

1、服务层首次启动,生成 Server-UUID,RSA Key Pair,发送初始化信息数据给 DRS 
2、Nginx 负载均衡,将多个 Server 请求分发给不同 DRS-Node
3、DRS-Node 将初始化信息数据保存到 Mysql Master、Redis Master

橙色部分为日志系统,一直都有记录收集数据,ELK 日志系统的搭建可以参考之前写的博客。

Win10搭建ELK8.0.0环境_余衫马的博客-CSDN博客_xpack.enrollment下载参考官方下载地址。Elasticsearch配置配置文件路径 .\elasticsearch-8.0.0\config\elasticsearch.yml# 主机 IPnetwork.host: 127.0.0.1# 端口http.port: 9200# 禁止下载 Geoipingest.geoip.downloader.enabled: false# 配置跨域http.cors.enabled: truehttp.cors.allow-origin: "https://blog.csdn.net/weixin_47560078/article/details/123201491

三、相关技术栈

Go1.18.4,Gin 框架,Gorm 框架,Redis 7.0.5,Mysql 8.0,MongoDB 6.0.2,Nginx 1.22.1,RabbitMQ 3.11.3

四、Utils

1、UUID

import uuid "github.com/satori/go.uuid"

/* 生成全局唯一 uuid */

func GenerateUUID() string {
	return uuid.NewV4().String()
}

func GenerateServiceUUID() string {
	return os.Getenv("SERVICE") + GenerateUUID()
}

2、RSA

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"log"
	"os"
)


// 生成RSA私钥和公钥,保存到文件中

func GenerateRSAKey(bits int, privateFilePath string) []byte {
	// GenerateKey函数使用随机数据生成器random生成一对具有指定字位数的RSA密钥
	// Reader是一个全局、共享的密码用强随机数生成器
	privateKey, err := rsa.GenerateKey(rand.Reader, bits)
	if err != nil {
		log.Fatalln(err)
	}
	// 保存私钥
	// 通过x509标准将得到的ras私钥序列化为ASN.1 的 DER编码字符串
	X509PrivateKey := x509.MarshalPKCS1PrivateKey(privateKey)
	// 使用pem格式对x509输出的内容进行编码
	// 创建文件保存私钥
	//privateFile, err := os.Create("./files/private.pem")
	privateFile, err := os.Create(privateFilePath)
	if err != nil {
		log.Fatalln(err)
	}
	defer privateFile.Close()
	// 构建一个pem.Block结构体对象
	privateBlock := pem.Block{Type: "RSA Private Key", Bytes: X509PrivateKey}
	// 将数据保存到文件
	pem.Encode(privateFile, &privateBlock)

	// 保存公钥
	// 获取公钥的数据
	publicKey := privateKey.PublicKey
	// X509对公钥编码
	X509PublicKey, err := x509.MarshalPKIXPublicKey(&publicKey)
	if err != nil {
		log.Fatalln(err)
	}
	// pem格式编码

	// 创建一个pem.Block结构体对象
	publicBlock := pem.Block{Type: "RSA Public Key", Bytes: X509PublicKey}
	// 保存到文件
	return pem.EncodeToMemory(&publicBlock)
}

// RSA 公钥加密

func EncryptByPublicKey(plainText []byte, publicKeyPem []byte) []byte {
	// pem解码
	block, _ := pem.Decode(publicKeyPem)
	// x509解码
	publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		log.Fatalln(err)
	}
	// 类型断言
	publicKey := publicKeyInterface.(*rsa.PublicKey)
	// 对明文进行加密
	cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, plainText)
	if err != nil {
		log.Fatalln(err)
	}
	// 返回密文
	return Base64Encode(cipherText)
}

// RSA 私钥解密

func DecryptByPrivateKey(cipherText []byte, privateKeyPath string) []byte {
	//打开文件
	file, err := os.Open(privateKeyPath)
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
	//获取文件内容
	info, _ := file.Stat()
	buf := make([]byte, info.Size())
	file.Read(buf)
	//pem解码
	block, _ := pem.Decode(buf)
	//X509解码
	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		log.Fatalln(err)
	}
	//对密文进行解密
	decode, err := Base64Decode(cipherText)
	if err != nil {
		log.Fatalln(err)
	}
	plainText, _ := rsa.DecryptPKCS1v15(rand.Reader, privateKey, decode)
	//返回明文
	return plainText
}

3、Base64

import "encoding/base64"

func Base64Encode(str []byte) []byte {
	return []byte(base64.StdEncoding.EncodeToString(str))
}

func Base64Decode(str []byte) ([]byte, error) {
	return base64.StdEncoding.DecodeString(string(str))
}

4、Http

import (
	"DynamicRSASystem/server/structs"
	"bytes"
	"encoding/json"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func DoPostRequest(url string, data interface{}) structs.ResponseResult {

	var request *http.Request
	var err error

	// 序列化参数
	marshal, err := json.Marshal(data)
	if err != nil {
		log.Fatalln(err.Error())
	}

	// 请求超时时间
	client := http.Client{Timeout: 1 * time.Second}

	// 传参
	if data == nil {
		request, err = http.NewRequest(http.MethodPost, url, nil)
	} else {
		request, err = http.NewRequest(http.MethodPost, url, bytes.NewBuffer(marshal))
	}

	if err != nil {
		log.Fatalln(err.Error())
	}

	// header
	request.Header.Set("Content-Type", "application/json")

	// 执行请求
	response, err := client.Do(request)
	if err != nil {
		log.Fatalln(err.Error())
	}
	// 请求结果
	all, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Fatalln(err.Error())
	}
	defer response.Body.Close()
	// 响应结果序列化
	var res structs.ResponseResult
	err = json.Unmarshal(all, &res)
	if err != nil {
		log.Fatalln(err.Error())
	}
	return res
}

五、Gorm 连接 Msql

package database

import (
	"demo/sys"
	"fmt"
	"gorm.io/driver/mysql" // mysql 数据库驱动
	"gorm.io/gorm"         // 使用 gorm ,操作数据库的 orm 框架
	"gorm.io/gorm/logger"
	"log"
	"os"
)

//go 访问权限:
//变量名、函数名、常量名首字母大写,则可以被其他包访问,
//如果首字母小写,则只能在本包中使用。
//首字母大写为共有,首字母小写为私有。

var Db *gorm.DB

//数据库初始化
//init() 表示包初始化的时候执行的函数, 如果函数名写成 main() , 会在操作数据的时候报错。
//参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情。

func init() {

	// ----------------------- 日志设置 -----------------------
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             sys.SlowThreshold,             // Slow SQL threshold
			LogLevel:                  sys.LogLevel,                  // Log level
			IgnoreRecordNotFoundError: sys.IgnoreRecordNotFoundError, // Ignore ErrRecordNotFound error for logger
			Colorful:                  sys.Colorful,                  // Disable color
		},
	)
	// ----------------------- 连接数据库 -----------------------
	var err error
	Db, err = gorm.Open(mysql.New(mysql.Config{
		DSN:                       sys.DSN,                       // DSN data source name
		DefaultStringSize:         sys.DefaultStringSize,         // string 类型字段的默认长度
		DisableDatetimePrecision:  sys.DisableDatetimePrecision,  // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
		DontSupportRenameIndex:    sys.DontSupportRenameIndex,    // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
		DontSupportRenameColumn:   sys.DontSupportRenameColumn,   // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
		SkipInitializeWithVersion: sys.SkipInitializeWithVersion, // 根据当前 MySQL 版本自动配置
	}), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		fmt.Printf(sys.MySQLConnectErr, err)
	}
	if Db.Error != nil {
		fmt.Printf(sys.DataBaseErr, Db.Error)
	}

	// ----------------------- 连接池设置 -----------------------
	sqlDB, err := Db.DB()

	// SetMaxIdleConns 设置空闲连接池中连接的最大数量
	sqlDB.SetMaxIdleConns(sys.MaxIdleConns)

	// SetMaxOpenConns 设置打开数据库连接的最大数量。
	sqlDB.SetMaxOpenConns(sys.MaxOpenConns)

	// SetConnMaxLifetime 设置了连接可复用的最大时间。
	sqlDB.SetConnMaxLifetime(sys.ConnMaxLifetime)

	fmt.Println(sys.ConnectedMySQL)
}

// 分页封装

func Paginate(pageNum int,pageSize int) func(db *gorm.DB) *gorm.DB {
	return func (db *gorm.DB) *gorm.DB {
		if pageNum == 0 {
			pageNum = 1
		}
		switch {
		case pageSize > 100:
			pageSize = 100
		case pageSize <= 0:
			pageSize = 10
		}
		offset := (pageNum - 1) * pageSize
		return db.Offset(offset).Limit(pageSize)
	}
}

六、连接 Redis 以及操作封装

package redis

import (
	"demo/sys"
	"fmt"
	"github.com/go-redis/redis"
	"log"
	"time"
)

var client *redis.Client

/*初始化连接*/
func init() {
	ConnectRedis()
}

/* 连接 redis 服务端 */
func ConnectRedis() {
	client = redis.NewClient(&redis.Options{
		Addr:     sys.RedisAddr,     // 默认连接地址为 localhost:6379
		DB:       sys.RedisDbIndex,  // redis 默认有 0-15 共 16 个数据库,这里设置操作索引为 index 的数据库
		Password: sys.RedisPassword, // 默认无密码
	})

	pong, err := client.Ping().Result()

	if err != nil {
		log.Fatal(err)
	}

	if pong != "PONG" {
		log.Fatal(sys.FailToConnectRedis)
	} else {
		fmt.Println(sys.RedisConnectionIsSuccessful)
	}
}



/* === string类型数据操作 === */
/* redis命令:set key val */
func Set(key, val string) string {
	//有效期为0表示不设置有效期,非0表示经过该时间后键值对失效
	result, err := client.Set(key, val, 0).Result()

	if err != nil {
		log.Fatal(err)
	}
	return result
}

/* redis命令:get key */
func Get(key string) string {
	val, err := client.Get(key).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(val)
	return val
}

/* redis命令:mset key1 val1 key2 val2 key3 val3 ... */
func Mset(key1, val1, key2, val2, key3, val3 string) string {
	//以下三种方式都可以
	//result,err := client.MSet(key1,val1,key2,val2,key3,val3).Result()
	//result,err := client.MSet([]string{key1,val1,key2,val2,key3,val3}).Result()
	result, err := client.MSet(map[string]interface{}{key1: val1, key2: val2, key3: val3}).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result)
	return result
}

/* redis命令:mget key1 key2 key3 ... */
func Mget(key1, key2, key3 string) []interface{} {
	vals, err := client.MGet(key1, key2, key3).Result()

	if err != nil {
		log.Fatal(err)
	}

	for k, v := range vals {
		fmt.Printf("k = %v v = %s\n", k, v)
	}

	return vals
}

/* redis命令:del key1 key2 key3 ... */
func Del(key1 ...string) int64 {
	result, err := client.Del(key1...).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result)
	return result
}

/* redis命令:getrange key start end */
func Getrange(key string, start, end int64) string {
	val, err := client.GetRange(key, start, end).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(val)
	return val
}

/* redis命令:strlen key */
func Strlen(key string) int64 {
	len, err := client.StrLen(key).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len)
	return len
}

/* redis命令:setex key time val */
func Setex(key, val string, expire int) string {
	//time.Duration其实也是int64,不过是int64的别名罢了,但这里如果expire使用int64也无法与time.Second运算,
	//因为int64和Duration虽然本质一样,但表面上属于不同类型,go语言中不同类型是无法做运算的
	result, err := client.Set(key, val, time.Duration(expire)*time.Second).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result)
	return result
}

/* redis命令:append key val */
func Append(key, val string) int64 {
	//将val插入key对应值的末尾,并返回新串长度
	len, err := client.Append(key, val).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len)
	return len
}

/* redis命令:exists key */
func Exists(key string) int64 {
	// 返回1表示存在,0表示不存在
	isExists, err := client.Exists(key).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(isExists)
	return isExists
}

/* === hash类型数据操作 === */
/* redis命令:hset hashTable key val */
func Hset(hashTable, key, val string) bool {
	isSetSuccessful, err := client.HSet(hashTable, key, val).Result()

	if err != nil {
		log.Fatal(err)
	}
	//如果键存在这返回false,如果键不存在则返回true
	fmt.Println(isSetSuccessful)
	return isSetSuccessful
}

/* redis命令:hget hashTable key */
func Hget(hashTable, key string) string {
	val, err := client.HGet(hashTable, key).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(val)
	return val
}

/* redis命令:hmset hashTable map[string]interface{} */
func Hmset(hashTable string, fields map[string]interface{}) string {
	res, err := client.HMSet(hashTable, fields).Result()

	if err != nil {
		log.Fatal(err)
	}
	return res
}

/* redis命令:hmget hashTable key1 key2 key3 ... */
func Hmget(hashTable, key1, key2, key3 string) []interface{} {
	vals, err := client.HMGet(hashTable, key1, key2, key3).Result()

	if err != nil {
		log.Fatal(err)
	}

	for k, v := range vals {
		fmt.Printf("k = %v v = %s\n", k, v)
	}
	return vals
}

/* redis命令:hdel hashTable key1 key2 key3 ... */
func Hdel(hashTable, key1 string) int64 {
	//返回1表示删除成功,返回0表示删除失败
	//只要至少有一个被删除则返回1(不存在的键不管),一个都没删除则返回0(不存在的则也算没删除)
	res, err := client.Del(hashTable, key1).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res)
	return res
}

/* redis命令:hgetall hashTable */
func Hgetall(hashTable string) map[string]string {
	vals, err := client.HGetAll(hashTable).Result()

	if err != nil {
		log.Fatal(err)
	}

	for k, v := range vals {
		fmt.Printf("k = %v v = %s\n", k, v)
	}

	return vals
}

/* redis命令:hexists hashTable key */
func Hexists(hashTable, key string) bool {
	isExists, err := client.HExists(hashTable, key).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(isExists)
	return isExists
}

/* redis命令:hlen hashTable */
func Hlen(hashTable string) int64 {
	len, err := client.HLen(hashTable).Result()

	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len)
	return len
}

/* redis命令:hkeys hashTable */
func Hkeys(hashTable string) []string {
	keys, err := client.HKeys(hashTable).Result()

	if err != nil {
		log.Fatal(err)
	}

	for k, v := range keys {
		fmt.Printf("k = %v v = %s\n", k, v)
	}
	return keys
}

/* redis命令:hvals hashTable */
func Hvals(hashTable string) []string {
	vals, err := client.HVals(hashTable).Result()

	if err != nil {
		log.Fatal(err)
	}

	for k, v := range vals {
		fmt.Printf("k = %v v = %s\n", k, v)
	}

	return vals
}

七、连接 MongoDB

package mongodb

import (
	"context"
	"demo/sys"
	"fmt"
	"go.mongodb.org/mongo-driver/mongo/writeconcern"
	"log"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

var MongoDB *mongo.Client

func init() {
	ConnectMongoDB()
}

func ConnectMongoDB() {

	//在连接时指定单节点连接而不是集群,否则会报错
	//server selection error: server selection timeout, current topology: { Type: Unknown, Servers: [{ Addr: localhost:27017, Type: Unknown, Last error: connection() error occured during connection handshake: dial tcp [::1]:27017: connect: connection refused }, ] }
	//connect=direct
	//clientOpts := options.Client().ApplyURI("mongodb://user:password@ip:port/?connect=direct&authSource=admin&authMechanism=SCRAM-SHA-1")

	// 设置客户端连接配置,指定 writeConcern 为 {w: 0} 可以达到最高吞吐量
	clientOptions := options.Client().ApplyURI(sys.MongoDBUrl).SetWriteConcern(writeconcern.New(writeconcern.W(0)))

	// 连接到MongoDB
	client, err := mongo.Connect(context.TODO(), clientOptions)
	if err != nil {
		log.Fatal(err)
	}

	// 检查连接
	err = client.Ping(context.TODO(), nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(sys.ConnectedMongoDB)
	MongoDB = client
}

八、系统全局静态变量

package sys

import (
	"gorm.io/gorm/logger"
	"time"
)

// mongoDb 封装
const MongoDBUrl string = "mongodb://user:password@ip:port/?connect=direct&authSource=admin&authMechanism=SCRAM-SHA-1"

const ConnectedMongoDB string = "Connected to MongoDB"

// redis 封装

const RedisAddr string = "127.0.0.1:6379" // 内网 redis-server

const RedisDbIndex int = 0 // 不同数据服务节点应使用不同的数据库

const RedisPassword string = ""

const RedisHashTableName string = "" // 不同数据服务节点应使用不同的 hashTable

const FailToConnectRedis string = "fail to connect redis server"

const RedisConnectionIsSuccessful = "The client has successfully connected to the Redis server"

// mysql 封装

//----------------------- 日志设置 -----------------------

const SlowThreshold = time.Second
const LogLevel = logger.Silent
const IgnoreRecordNotFoundError = true
const Colorful = false

// ----------------------- 连接数据库 -----------------------

const ConnectedMySQL string = "Connected to ConnectedMySQL"

// 考虑到网络信号问题,timeout 不应设置太短时间

const DSN string = "root:root@tcp(127.0.0.1:3306)/db?charset=utf8&parseTime=True&loc=Local&timeout=1000ms"

const DefaultStringSize = 256

const DisableDatetimePrecision = true
const DontSupportRenameIndex = true
const DontSupportRenameColumn = true
const SkipInitializeWithVersion = true

const MaxIdleConns = 10

const MaxOpenConns = 100

const ConnMaxLifetime = time.Hour

const MySQLConnectErr string = "mysql connect error %v"

const DataBaseErr string = "database error %v"

九、数据库设计

CREATE TABLE `DrsServiceInitInfos` (
	`id` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_0900_ai_ci',
	`serviceId` VARCHAR(50) NOT NULL COMMENT '服务id' COLLATE 'utf8mb4_0900_ai_ci',
	`publicKey` VARCHAR(256) NOT NULL COMMENT '公钥' COLLATE 'utf8mb4_0900_ai_ci',
	`expiredTime` VARCHAR(20) NOT NULL COMMENT '过期时间' COLLATE 'utf8mb4_0900_ai_ci',
	`currentStatus` VARCHAR(10) NOT NULL DEFAULT 'normal' COMMENT '当前状态' COLLATE 'utf8mb4_0900_ai_ci',
	`createdAt` VARCHAR(20) NOT NULL COMMENT '创建时间' COLLATE 'utf8mb4_0900_ai_ci',
	`updatedAt` VARCHAR(20) NULL DEFAULT NULL COMMENT '更新时间' COLLATE 'utf8mb4_0900_ai_ci',
	`createdUser` VARCHAR(20) NOT NULL COLLATE 'utf8mb4_0900_ai_ci',
	`updatedUser` VARCHAR(20) NULL DEFAULT NULL COMMENT '操作人id' COLLATE 'utf8mb4_0900_ai_ci',
	PRIMARY KEY (`id`) USING BTREE
)
COMMENT='初始化信息'
COLLATE='utf8mb4_0900_ai_ci'
ENGINE=InnoDB
;

十、Gin MVC 框架搭建

简单搭建 Gin + GORM MVC 框架_余衫马的博客-CSDN博客_gin mvc框架参考GORM 指南目录结构controller:负责请求转发,接受页面过来的参数,传给 Model 处理,接到返回值,再传给页面。database:通过 gorm 连接数据库。models:对应数据表的增删查改。router:处理路由。templates:HTML 模板。定义路由package routerimport (. "gin-mvc/controller""github.com/gin-gonic/gin")/*InitRouter 路.https://blog.csdn.net/weixin_47560078/article/details/119245691?spm=1001.2014.3001.5501参考这篇文章。

架构变更

  • 新增 JWT 保护 API 接口
  • 新增 Email 发送事件报告
  • 新增 Rabbit MQ 缓存队列
  • 规则约束

中间件

1、跨域中间件

package middleware

/**
 * 跨域中间件
 *
 * @author yushanma
 */


import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"time"
)

// 跨域请求

func Cors() gin.HandlerFunc {
	handlerFunc := cors.New(cors.Config{
		AllowMethods:     []string{"*"},
		AllowHeaders:     []string{"content-type", "token", "fileType", "size", "digest"}, //此处设置非默认之外的请求头(自定义请求头),否则会出现跨域问题
		AllowAllOrigins:  true,
		AllowCredentials: true,
		MaxAge:           24 * time.Hour,
		ExposeHeaders:    []string{"*"},
	})
	return handlerFunc
}

// gin 上下文配置 cors

func CORSMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, HEAD, OPTIONS")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}

		c.Next()
	}
}

//nginx 跨域

//server {
//#以上省略
//add_header 'Access-Control-Allow-Origin' '*';
//add_header 'Access-Control-Allow-Headers' 'X-Pagination-Current-Page,Content-Type';
//add_header 'Access-Control-Allow-Methods' 'PUT,GET,POST,HEAD,DELETE';
//add_header 'Access-Control-Expose-Headers' 'X-Pagination-Current-Page,Content-Type';
//#以下省略
//}
//Allow-Headers "Accept","Accept-Encoding","Host","Origin","Referer","User-Agent",

2、JWT中间件

package middleware

import (
	"DynamicRSASystem/sys"
	"DynamicRSASystem/utils"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

/**
 * jwt
 *
 * @author yushanma
 * @since 2022/11/9 11:48
 */

// 定义一个JWTAuth的中间件,用于接口鉴权
// 1、获取 header 中的 token
// 2、校验解析 token
// 3、负载信息写入上下文 gin.Context
// 4、调用后续处理函数,如日志中间件

func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 通过 http header 中的 token 解析来认证
		token := c.Request.Header.Get("token")
		if token == "" {
			c.JSON(http.StatusOK, gin.H{
				"code": -1,
				"msg":  sys.NoneTokenErr,
				"data": nil,
			})
			c.Abort()
			return
		}

		log.Println("token length", len(token))

		// 初始化一个JWT对象实例,并根据结构体方法来解析token
		j := utils.NewJWT()
		// 解析token中包含的相关信息(有效载荷)
		claims, err := j.ParserToken(token)

		if err != nil {
			// token过期
			if err.Error() == sys.TokenExpired {
				c.JSON(http.StatusOK, gin.H{
					"code": -1,
					"msg":  sys.ExpiredTokenErr,
					"data": nil,
				})
				c.Abort()
				return
			}
			// 其他错误
			c.JSON(http.StatusOK, gin.H{
				"code": -1,
				"msg":  err.Error(),
				"data": nil,
			})
			c.Abort()
			return
		}

		// 将解析后的有效载荷 claims 重新写入 gin.Context 引用对象中
		c.Set("claims", claims)
		// 调用后续处理函数
		c.Next()
	}
}

3、日志中间件

package middleware

import (
	"DynamicRSASystem/mongo"
	"DynamicRSASystem/utils"
	"github.com/gin-gonic/gin"
)

/**
 * log 记录
 *
 * @author yushanma
 * @since 2022/11/9 11:49
 */

// 日志中间件,记录用户访问信息
// 1、检查并解析 jwt 信息
// 2、获取请求 header 信息
// 3、写入数据库

func SystemLogRecord() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 检查并解析 jwt 信息
		value, exists := c.Get("claims")
		var claims *utils.CustomClaims
		if exists {
			claims = value.(*utils.CustomClaims)
		} else {
			claims = &utils.CustomClaims{
				UserCode: "null",
				UserRole: "visitors",
			}
		}
		// 获取请求 header 信息
		hostIp := c.RemoteIP()

		// 写入数据库
		mongo.InsertServerlog(hostIp, claims.UserCode, claims.UserRole, c.FullPath(), c.Request.Header.Get("Referer"), c.Request.Header.Get("User-Agent"))
	}
}

JWT Util 封装

package utils

import (
	"github.com/dgrijalva/jwt-go"
	"DynamicRSASystem/sys"
	"fmt"
)

/**
 * jwt 鉴权
 *
 * @author yushanma
 * @since 2022/11/9 14:04
 */

// 定义一个jwt对象

type JWT struct {
	// 声明签名信息
	SigningKey []byte
}

// 初始化jwt对象

func NewJWT() *JWT {
	return &JWT{
		[]byte(sys.SigningKey),
	}
}

// 自定义有效载荷(这里采用自定义的 UserCode 和 UserRole 作为有效载荷的一部分)

type CustomClaims struct {
	UserCode string `json:"userCode"`
	UserRole string `json:"userRole"`
	// StandardClaims结构体实现了Claims接口(Valid()函数)
	jwt.StandardClaims
}

// 调用jwt-go库生成token
// 指定编码的算法为jwt.SigningMethodHS512

func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
	// https://gowalker.org/github.com/dgrijalva/jwt-go#Token
	// 返回一个token的结构体指针
	token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
	return token.SignedString(j.SigningKey)
}

// token解码

func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
	// https://gowalker.org/github.com/dgrijalva/jwt-go#ParseWithClaims
	// 输入用户自定义的Claims结构体对象,token,以及自定义函数来解析token字符串为jwt的Token结构体指针
	// Keyfunc是匿名函数类型: type Keyfunc func(*Token) (interface{}, error)
	// func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {}
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})

	if err != nil {
		// https://gowalker.org/github.com/dgrijalva/jwt-go#ValidationError
		// jwt.ValidationError 是一个无效token的错误结构
		if ve, ok := err.(*jwt.ValidationError); ok {
			// ValidationErrorMalformed是一个uint常量,表示token不可用
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, fmt.Errorf(sys.TokenDisabled)
				// ValidationErrorExpired表示Token过期
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				return nil, fmt.Errorf(sys.TokenExpired)
				// ValidationErrorNotValidYet表示无效token
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, fmt.Errorf(sys.TokenInvalid)
			} else {
				return nil, fmt.Errorf(sys.TokenDisabled)
			}

		}
	}

	// 将token中的claims信息解析出来并断言成用户自定义的有效载荷结构
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}

	return nil, fmt.Errorf(sys.TokenInvalid)

}

邮件工具封装

package utils

import (
	"DynamicRSASystem/sys"
	"DynamicRSASystem/types"
	"crypto/tls"
	"regexp"

	"github.com/go-gomail/gomail"
)

/**
 * email 代理
 *
 * @author yushanma
 * @since 2022/11/9 14:35
 */

func VerifyEmailFormat(email string) bool {
	pattern := `\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*` //匹配电子邮箱
	reg := regexp.MustCompile(pattern)
	return reg.MatchString(email)
}

//发送邮件,content 为发送内容,to 为收件人

func SendEmail(msg types.EmailMsgStruct) {
	m := gomail.NewMessage()
	m.SetHeader("From", sys.EmailName) // 发件人
	m.SetHeader("To", msg.To)              // 收件人
	m.SetHeader("Subject", msg.Subject)    // 邮件标题
	m.SetBody("text/html", msg.Content)    // 邮件内容
	//m.SetAddressHeader("Cc", sys.EmailName, sys.EmailUserName) // 抄送

	d := gomail.NewDialer(sys.EmailDialer, sys.EmailTLlsPort, sys.EmailName, sys.EmailToken) // 代理,端口,用户,授权码
	d.TLSConfig = &tls.Config{InsecureSkipVerify: true}                                      // TLS 配置
	// 发送
	if err := d.DialAndSend(m); err != nil {
		panic(err)
	}
}
package utils

import (
	"math/rand"
	"time"
)

/**
 * 验证码
 *
 * @author yushanma
 * @since 2022/11/9 14:43
 */


func GetVerifyEmailCode() string {
	var verifyEmailCode []byte
	// 验证码组成库
	charArray := []byte{
		'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
		'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
		'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
		'u', 'v', 'w', 'x', 'y', 'z',
		'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
		'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
		'U', 'V', 'W', 'X', 'Y', 'Z',
	}
	// 时间戳作为随机种子
	rand.Seed(time.Now().Unix())
	// 生成6为验证码
	for i := 0; i < 6; i++ {
		verifyEmailCode = append(verifyEmailCode, charArray[rand.Intn(len(charArray))])
	}
	return string(verifyEmailCode)
}

RabbitMQ 环境搭建与封装

参考之前的博客

Docker 镜像拉取docker 之 pull image 篇https://blog.csdn.net/weixin_47560078/article/details/126189386?spm=1001.2014.3001.5501或者

从0到1手写分布式对象存储系统-02分布式架构初建_余衫马的博客-CSDN博客从0到1手写分布式对象存储系统https://blog.csdn.net/weixin_47560078/article/details/120829103?spm=1001.2014.3001.5501

猜你喜欢

转载自blog.csdn.net/weixin_47560078/article/details/127576152
RSA