1. Background
After implementing the static encryption and decryption tool, I felt that it was not flexible enough. I wanted to design an intermediate system that dynamically generates RSA KeyPair, temporarily called Dynamic RSA System, to achieve the effect of automatically maintaining the high security wall.
2. Architecture V1
Killing a chicken with a weak knife ? Let’s take this opportunity to review the forgotten technology again. Let’s briefly introduce the digital labeling process:
1. The service layer starts for the first time, generates Server-UUID, RSA Key Pair, and sends initialization information data to DRS.
2. Nginx load balancing distributes multiple server requests to different DRS-Node.
3. DRS-Node saves the initialization information data to Mysql Master, Redis Master
The orange part is the log system, which has always recorded and collected data. For the construction of the ELK log system, you can refer to the blog written before.
3. Related technology stacks
Go1.18.4, Gin framework, Gorm framework, Redis 7.0.5, Mysql 8.0, MongoDB 6.0.2, Nginx 1.22.1, RabbitMQ 3.11.3
4. 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
}
5. Gorm connects to 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)
}
}
6. Connecting to Redis and operating encapsulation
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
}
7. Connect to 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
}
8. System global static variables
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"
9. Database design
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
;
10. Gin MVC framework construction
Architecture changes
- Added JWT protection API interface
- Added email sending event report
- Added Rabbit MQ cache queue
- Rule constraints
middleware
1. Cross-domain middleware
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 middleware
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. Log middleware
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
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)
}
Mail tool package
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 environment construction and packaging
Refer to previous blog