文章目录
- Logrus日志管理
- Logrus基本用法
- 实现日志切割和过期删除
- token身份验证
- Hash消息认证签名实现token
- RSA签名实现token
- 椭圆曲线数字签名算法(ECDSA)方式生成token
Logrus日志管理
Logrus是一个结构化的Go日志框架,功能强大,具有高度的灵活性,它提供了自定义插件的功能,有TEXT与JSON两种可选的日志输出格式。Logrus还支持Field机制和可扩展的HooK机制,它鼓励用户通过Field机制进行精细化的、结构化的日志记录,允许用户通过hook的方式将日志分到任意地方。
许多著名开源项目,如docker、prometheus等都是使用Logrus来记录日志。
Logrus基本用法
添加依赖:go get github.com/sirupsen/logrus
,代码如下:
//router
r.GET("/logrusDemo1", controllers.LogrusDemo1)
//controllers
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
"os"
)
var log = logrus.New() // 创建一个log示例
func initLogrus() error {
log.Formatter = &logrus.JSONFormatter{} // 设置为json格式的日志
//os.O_CREATE: 如果文件不存在则创建
//os.O_WRONLY: 以读写的方式打开
//os.O_APPEND: 追加写入
//0644: 文件权限
file, err := os.OpenFile("./logs/http_api.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) // 创建一个log日志文件
if err != nil {
fmt.Println("文件打开或创建失败")
return err
}
log.Out = file // 设置log的默认文件输出
gin.SetMode(gin.ReleaseMode) // 发布版本
gin.DefaultWriter = log.Out // 如果设置此项,gin框架自己记录的日志也会写入到文件
log.Level = logrus.InfoLevel // 设置日志级别
return nil
}
// 文档地址:https://github.com/sirupsen/logrus
func LogrusDemo1(c *gin.Context) {
err := initLogrus()
if err != nil {
fmt.Println(err)
return
}
//log日志信息的写入
log.WithFields(logrus.Fields{
"url": c.Request.RequestURI, //自定义显示的字段,下同
"method": c.Request.Method,
"params": c.Query("name"),
"IP": c.ClientIP(),
}).Info()
resData := struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}{http.StatusOK, "响应成功", "OK"}
c.JSON(http.StatusOK, resData)
}
运行后,可以看到指定目录下已经生成了JSON格式的日志:
实现日志切割和过期删除
如果想要将日志保存为每一天一个文件,并且设置超过一段时间(比如7天)之前的日志自动删除,可以使用下面两个依赖实现:
- 使用
go get github.com/lestrrat-go/file-rotatelogs
实现日志文件切割 - 使用
go get github.com/rifflock/lfshook
实现日志文件的hook机制
具体实现代码如下:
//router
r.Use(controllers.LogMiddleware())
r.GET("/LogrusDemo2", controllers.LogrusDemo2)
//controllers
import (
"fmt"
"github.com/gin-gonic/gin"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"net/http"
"os"
"path"
"time"
)
// 日志文件自动切割以及过期删除
func LogMiddleware() gin.HandlerFunc {
var (
logFilePath = "./logs" //文件存储路径
logFileName = "system.log"
)
// 日志文件
fileName := path.Join(logFilePath, logFileName)
// 写入文件
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println(err)
}
// 实例化
logger := logrus.New()
//设置日志级别
logger.SetLevel(logrus.DebugLevel)
//设置输出
logger.Out = file
// 设置 rotatelogs,实现文件分割
logWriter, err := rotatelogs.New(
// 分割后的文件名称
fileName+".%Y%m%d.log",
// 生成软链,指向最新日志文件
rotatelogs.WithLinkName(fileName),
// 设置最大保存时间(7天)
rotatelogs.WithMaxAge(7*24*time.Hour), //以hour为单位的整数
// 设置日志切割时间间隔(1天)
rotatelogs.WithRotationTime(1*time.Hour),
)
//hook机制的设置
writerMap := lfshook.WriterMap{
logrus.InfoLevel: logWriter,
logrus.FatalLevel: logWriter,
logrus.DebugLevel: logWriter,
logrus.WarnLevel: logWriter,
logrus.ErrorLevel: logWriter,
logrus.PanicLevel: logWriter,
}
//给logrus添加hook
logger.AddHook(lfshook.NewHook(writerMap, &logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
}))
return func(c *gin.Context) {
c.Next()
//请求方式
method := c.Request.Method
//请求路由
reqUrl := c.Request.RequestURI
//状态码
statusCode := c.Writer.Status()
//请求ip
clientIP := c.ClientIP()
logger.WithFields(logrus.Fields{
"status_code": statusCode,
"client_ip": clientIP,
"req_method": method,
"req_uri": reqUrl,
}).Info()
}
}
func LogrusDemo2(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "响应成功",
"data": "OK",
})
}
运行后,可以看到指定目录下已经生成了按照日期存储的日志文件:
token身份验证
token是一种验证客户端请求参数是否合法的方式,最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名)。
token和session的区别
- token和session都是用来校验客户端请求的数据是否合法,session一般翻译为会话,而token更多的时候是翻译为令牌;
- session可以保存在服务器的缓存、文件、数据库等,session和token都可以设置过期时间。
- 其实token与session在时间与空间的侧重点不同,session是空间换时间,而token是时间换
空间。两者的选择要看具体情况而定。 - 对于token,服务端不需要记录任何状态,每次客户端的请求都是无状态的,每次解密验证然后判断请求是否合法。
token验证方式
The HMAC signing method(HS256,HS384,HS512)//Hash消息认证签名
The RSA signing method(RS256,RS384,RS512)//RSA非对称加密签名
The ECDSA signing method(ES256,ES384,ES512)//椭圆曲线数字签名
文档地址:https://github.com/dgrijalva/jwt-go
Hash消息认证签名实现token
通过一个自定义的字符串加密,使用jwt-go
来生成和校验token是否有效。添加依赖go get github.com/dgrijalva/jwt-go
代码如下:
//router
r3.POST("hmac/createToken", token.HmacCreateToken)
//controllers
package token
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
// Hash消息认证签名实现token
type HmacUser struct {
UserId string `json:"user_id"` //根据用户id生成token
UserName string `json:"user_name"`
}
type MyClaims struct {
UserId string
jwt.StandardClaims
}
var jwtKey = []byte("secret_renxing_csdn_gin") //证书签名秘钥,用来签发证书
func HmacCreateToken(c *gin.Context) {
var user HmacUser
c.Bind(&user) //绑定客户端传来的参数
fmt.Println("user", user)
if user.UserId == "" {
c.JSON(http.StatusBadRequest, "user_id不能为空")
return
}
token, err := _createToken(user)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "token创建成功",
"data": gin.H{
"user_id": user.UserId,
"token": token,
},
})
}
// 创建token
func _createToken(user HmacUser) (string, error) {
expirationTime := time.Now().Add(7 * 24 * time.Hour) //token有效期截止时间,从当前时间往后7天
claims := &MyClaims{
UserId: user.UserId,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(), //过期时间
IssuedAt: time.Now().Unix(), //发布时间
Subject: "create hmac token demo", //主题
Issuer: "renxing", //发布者
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) //生成token
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
通过上面的方法可以生成一个token,然后还需要实现校验token是否有效,代码如下:
//router
r3.POST("hmac/checkToken", token.HmacAuthMiddleware(), func(c *gin.Context) {
c.String(http.StatusOK, "token验证通过")
})
//controllers
// 校验token
func HmacAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
auth := "renxing"
// 获取authorization header
Authorization := c.GetHeader("Authorization") //客户端传递的header信息,header的key是Authorization
fmt.Println("header:Authorization", Authorization)
if Authorization == "" || !strings.HasPrefix(Authorization, auth+":") { //验证token不为空,并且以 renxing: 为前缀
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "解析token前缀错误"})
c.Abort()
return
}
index := strings.Index(Authorization, auth+":") //找到token前缀对应的位置
//真实token的值
tokenString := Authorization[index+len(auth)+1:] //截取真实的token(开始位置为:索引开始的位置+关键字符的长度+1(:的长度为1))
fmt.Println("tokenString", tokenString)
//解析token
token, claims, err := _hamcParseToke(tokenString)
fmt.Println("token", token)
fmt.Println("claims", claims)
if err != nil || !token.Valid { //解析错误或者过期等
fmt.Println("err", err)
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "证书无效"})
c.Abort()
return
}
//校验比对解析后的token中的user_id是否和客户端传来的user_id匹配
var user HmacUser
c.Bind(&user)
if user.UserId != claims.UserId {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "用户不存在"})
c.Abort()
return
}
c.Next()
}
}
// 解析token
func _hamcParseToke(tokenString string) (*jwt.Token, *MyClaims, error) {
claims := &MyClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
return jwtKey, nil
})
return token, claims, err
}
控制台输出
RSA签名实现token
RSA加密是一种非对称加密,可以在不直接传递密钥的情况下完成解密,这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。RSA是由一对密钥来进行加解密的过程,分别称为公钥和私钥。RSA加密算法的原理就是对一极大整数做因数分解的困难性来保证安全性,通常个人保存私钥,公钥是公开的(可能同时多人持有)。
在 http://www.metools.info/code/c80.html
这个网站可以生成公钥私钥对:
接下来,在Gin框架中实现RSA加密生成token。首先将上面生成的公钥和私钥保存到项目中的两个文件,命名为.pem
结尾。
代码如下:
//router
r3.POST("rsa/createToken", token.RsaCreateToken)
r3.POST("rsa/checkToken", token.RsaAuthMiddleware(), func(c *gin.Context) {
c.String(http.StatusOK, "RSA token验证通过")
})
//controllers
package token
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"strings"
"time"
)
// RSA签名实现token
// RAS密钥生成工具链接:http://www.metools.info/code/c80.html
type RsaUser struct {
UserId string `json:"user_id"` //根据用户id生成token
UserName string `json:"user_name"`
}
type RasClaims struct {
UserId string `json:"user_id"`
jwt.StandardClaims
}
var (
resPrivateKey []byte //读取私钥内容
resPublicKey []byte //读取公钥内容
err1, err2 error
)
// 初始化读取秘钥文件内容
func init() {
resPrivateKey, err1 = ioutil.ReadFile("/home/rx/go/gin-demo/token/pem/privateKey.pem") //路径从GOPATH开始
resPublicKey, err2 = ioutil.ReadFile("/home/rx/go/gin-demo/token/pem/publicKey.pem")
if err1 != nil || err2 != nil {
panic(fmt.Sprintf("读取秘钥文件内容出错:%s,%s", err1, err2))
return
}
}
func RsaCreateToken(c *gin.Context) {
var user RsaUser
c.Bind(&user) //绑定客户端传来的参数
fmt.Println("user", user)
if user.UserId == "" {
c.JSON(http.StatusBadRequest, "user_id不能为空")
return
}
token, err := _createRsaToken(user)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "token创建成功",
"data": gin.H{
"user_id": user.UserId,
"token": token,
},
})
}
// 创建token
func _createRsaToken(user RsaUser) (interface{}, error) {
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(resPrivateKey)
if err != nil {
return nil, err
}
claims := &RasClaims{
UserId: user.UserId,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Unix(), //token有效期截止时间,从当前时间往后7天
Issuer: "renxing", //发布者
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedString, err := token.SignedString(privateKey)
return signedString, err
}
// 校验token
func RsaAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
auth := "renxing"
tokenString := c.GetHeader("Authorization")
if tokenString == "" || !strings.HasPrefix(tokenString, auth+":") {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "无效的token"})
c.Abort()
return
}
index := strings.Index(tokenString, auth+":")
tokenString = tokenString[index+len(auth)+1:]
claims, err := _rsaParseToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "证书无效"})
c.Abort()
return
}
claimsValue := claims.(jwt.MapClaims) //断言
if claimsValue["user_id"] == nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "用户不存在"})
c.Abort()
return
}
u := RsaUser{}
c.Bind(&u)
id := claimsValue["user_id"].(string)
if u.UserId != id {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "用户Id不匹配"})
c.Abort()
return
}
c.Next()
}
}
// 解析token
func _rsaParseToken(tokenString string) (interface{}, error) {
pem, err := jwt.ParseRSAPublicKeyFromPEM(resPublicKey)
if err != nil {
return nil, err
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
if _, OK := token.Method.(*jwt.SigningMethodRSA); !OK {
return nil, fmt.Errorf("解析的方法错误")
}
return pem, err
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, err
}
运行效果:
椭圆曲线数字签名算法(ECDSA)方式生成token
椭圆曲线加密算法,是基于椭圆曲线数学理论实现的一种非对称加密算法。相比RSA,ECDSA优势是可以使用更短的密钥,来实现与RSA相当或更高的安全,RSA加密算法也是一种非对称加密算法,在公开密钥加密和电子商业中RSA被广泛使用。更多信息请参考 这里。
下面是实现的代码:
//router
r3.POST("ecdsa/createToken", token.EcdsaCreateToken)
r3.POST("ecdsa/checkToken", token.EcdsaAuthMiddleware, func(c *gin.Context) {
c.String(http.StatusOK, "ECDSA token验证通过")
})
//controllers
package token
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"time"
)
type EcdsaUser struct {
ID string `json:"id"`
Name string `json:"name"`
}
type EcdsaClaims struct {
UserId string `json:"user_id"`
jwt.StandardClaims
}
var (
err3 error
eccPrivateKey *ecdsa.PrivateKey
eccPublicKey *ecdsa.PublicKey
)
func init() {
eccPrivateKey, eccPublicKey, err3 = getEcdsaKey(2)
if err3 != nil {
panic(err3)
return
}
}
// ecdsa秘钥生成
func getEcdsaKey(keyType int) (*ecdsa.PrivateKey, *ecdsa.PublicKey, error) {
var err error
var pri_key *ecdsa.PrivateKey
var pub_key *ecdsa.PublicKey
var curve elliptic.Curve //椭圆曲线
switch keyType {
case 1:
curve = elliptic.P224()
case 2:
curve = elliptic.P256()
case 3:
curve = elliptic.P384()
case 4:
curve = elliptic.P521()
default:
err = errors.New("输入的签名key类型错误!key取值:\n 1:椭圆曲线224\n 2:椭圆曲线2256\n 3:椭圆曲线384\n 4:椭圆曲线521\n")
return nil, nil, err
}
pri_key, err = ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, err
}
pub_key = &pri_key.PublicKey
return pri_key, pub_key, err
}
func EcdsaCreateToken(c *gin.Context) {
u := EcdsaUser{}
err := c.Bind(&u)
if err != nil {
c.JSON(http.StatusBadRequest, "参数错误")
return
}
token, err := _ecdsaCreateToken(u)
if err != nil {
c.JSON(http.StatusBadRequest, "生成token错误")
return
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "授权成功",
"data": token,
})
}
// 生成token
func _ecdsaCreateToken(u EcdsaUser) (interface{}, error) {
claims := &EcdsaClaims{
UserId: u.ID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Unix(),
Issuer: "renxing",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
signedString, err := token.SignedString(eccPrivateKey)
return signedString, err
}
// token认证中间件
func EcdsaAuthMiddleware(c *gin.Context) {
auth := "renxing"
tokenString := c.GetHeader("Authorization")
if tokenString == "" || !strings.HasPrefix(tokenString, auth+":") {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "无效的token"})
c.Abort()
return
}
index := strings.Index(tokenString, auth+":")
tokenString = tokenString[index+len(auth)+1:]
claims, err := _ecdsaJwtTokenRead(tokenString)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, err)
return
}
claimsValue := claims.(jwt.MapClaims)
if claimsValue["user_id"] == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, "id不存在")
return
}
u := EcdsaUser{}
c.Bind(&u)
if u.ID != claimsValue["user_id"] {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "用户不存在"})
c.Abort()
return
}
c.Next()
}
// token解析
func _ecdsaJwtTokenRead(tokenString string) (interface{}, error) {
myToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
return nil, fmt.Errorf("无效的签名方法: %v", token.Method)
}
return eccPublicKey, nil
})
if claims, ok := myToken.Claims.(jwt.MapClaims); ok && myToken.Valid {
return claims, nil
}
return nil, err
}
运行效果:
源代码:https://gitee.com/rxbook/gin-demo