一、背景
在实现了静态的加解密工具后,感觉不够灵活,想设计一个动态生成 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