0.序文
このセクションでは、最初にログイン インターフェイスが完成し、次に jwt ベースの ID 認証実装がプロジェクトに追加されます。
1. ログインインターフェース
1.1bcrypt パッケージの概要
以前に完了した登録インターフェイスでは、ユーザーのパスワードがバックエンド データベースに平文で保存されており、これはユーザーのプライバシーの侵害でした。次に、bcrypt パッケージを使用してユーザー パスワードを暗号化し、データベースに保存する必要があります。
Bycrypt は公式 Go パッケージの 1 つであり、暗号化パッケージです。その暗号化は不可逆的です。つまり、復号結果からパスワードを推測することはできません。これは、ユーザー パスワードを暗号化するロジックと非常に一致しています。
具体的な使い方は以下の2つの方法をご覧ください。
//加密方法
//此方法传入用户密码,和一个“cost”(加盐),返回一个加密后的密码和错误信息。
func GenerateFromPassword(password []byte, cost int) ([]byte, error)
//比较方法
//此方法传入用户密码和数据库中保存的密码哈希,进行比较后,如果错误,会返回error,如果密码正确,会返回Nil的error.
func CompareHashAndPassword(hashedPassword, password []byte) error
1.2 コードの実装
ログイン インターフェイスを実装する前に、まず登録インターフェイスのロジックを変更し、元々プレーン テキストで保存されていたパスワードを暗号化ストレージに変更します。
hashedPassword,err:=bcrypt.GenerateFromPassword([]byte(password),bcrypt.DefaultCost)
if err!=nil{
c.JSON(500,gin.H{
"error":"there are something wrong when encrypting password!",
})
}
db.Create(&model.User{Name: name,Password: string(hashedPassword),Telephone: telephone})
ログイン ルーティングを router.go に追加します。
router.GET("/login",controller.Login)
ログインインターフェイスを実装します。
func Login(c *gin.Context) {
//获取参数
telephone:=c.Query("telephone")
password:=c.Query("password")
//查询是否存在
db:=common.GetDB()
var user model.User
db.Where("telephone=?",telephone).First(&user)
if len(telephone)!=11{
c.JSON(400,gin.H{
"error":"the telephone is not correct",
})
return
}
//不存在
if user.ID==0{
c.JSON(400,gin.H{
"error": "the telephone is not registered!",
})
return
}
//判断密码是否正确
//使用bcrypt包进行密码控制
err:=bcrypt.CompareHashAndPassword([]byte(user.Password),[]byte(password))
if err!=nil {
c.JSON(400,gin.H{
"error":"the password is wrong!",
})
return
}
//发放token
//这里的token直接设定的11,后面会设立真正的token
token:="11"
//返回登录成功
c.JSON(200,gin.H{
"msg":"login succeed",
"data":gin.H{
"token":token,
},
})
}
2. JWT の概要
この部分はブログ https://blog.wangjunfeng.com/post/golang-jwt/#3-%E7%AD%BE%E5%90%8D-signature を参照しています。
JWT (JSON Web Token) はオープン標準 ( RFC 7519 ) であり、JSON に基づいており、当事者間の情報の安全な送信を保証するために WEB 上で特定のトークンを宣言するために使用されます。
JWT はユーザー情報をトークンに暗号化するため、サーバーはユーザー情報を保存する必要がありません。サーバーは、保存されたキーを使用してトークンの正当性を検証するだけでよく、それが正しければ検証に合格します。
2.1 JWT 構成分析
JWS は実際には文字列であり、ヘッダー、ペイロード、署名の 3 つの部分が結合されて構成されています.
。ヘッダーとペイロードはデータを JSON 形式で保存しますが、エンコードされます。
2.1.1. ヘッダー
各 JWT にはヘッダー情報が含まれます。ヘッダー情報は、主に使用されるアルゴリズムを宣言します。アルゴリズムを宣言するためのフィールド名alg
とフィールドtyp
。デフォルトでJWT
十分です。次の例のアルゴリズムは HS256 です。
{
"alg": "HS256",
"typ": "JWT"
}
JWT は文字列であるため、上記のコンテンツを Base64 エンコードする必要もあります。エンコードされた文字列は次のとおりです。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.1.2. ペイロード
ペイロードはメッセージ本文であり、実際のコンテンツはここに格納されます。これがトークンのデータ要求 (Claim) です。このセクションのフィールドの一部は標準フィールドですが、必要に応じて必要なフィールドを追加することもできます。標準フィールドは次のとおりです。
iss
: トークン発行者。形式は大文字と小文字が区別される文字列または URI で、トークンを発行した当事者を一意に識別するために使用されます。sub
: トークンの主体、つまりその所有者。形式は、大文字と小文字が区別される文字列または URI です。aud
: トークンを受け取る側。形式は、大文字と小文字が区別される文字列または URI、あるいはその両方の配列です。exp
: トークンの有効期限 (タイムスタンプ形式)。nbf
: nbf time、つまりトークンが有効になる時刻より前にトークンを使用できないことをタイムスタンプの形式で指定します。iat
: トークンの発行時刻 (タイムスタンプ形式)。jti
: このトークンの一意の識別文字列を指します。主に一意性の保証とリプレイの防止を実現するために使用されます。
以下に例を示します。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
同じ Base64 エンコード後の文字列は次のようになります。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
2.1.3. 署名
署名はヘッダーとペイロードの内容に署名するもので、データの最初の 2 つの部分が改ざんされると、サーバーの暗号化に使用される鍵が漏洩しない限り、取得された署名は確実に以前の署名と不一致になります。
署名プロセス:
- Base64URL でヘッダーの json データをエンコードし、文字列 str1 を取得します。
- Base64URL はペイロードの json データをエンコードし、文字列 str2 を取得します
- 上記の 2 つの文字列を使用して
.
連結し、文字列 str3 を取得します。 - ヘッダーで宣言されたアルゴリズムとサーバーのキーを使用して、連結された文字列を暗号化し、署名を生成します。
疑似コードで表現すると、次のようになります (HS256 を例にします)。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
3 つの文字列セットを接続して.
完全なトークンを取得します。次に例を示します。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk
2.2 JWT ワークフロー
ユーザー認証の場合、これまでの従来の方法では、セッションがサーバーに保存され、Cookie がクライアントに返されます。jwt が認証に使用される場合、ユーザーがログインに成功するとトークンがユーザーに与えられ、フロントエンドはトークンをローカルに保存するだけで済みます (通常は localStorage が使用されますが、Cookie も使用できます)。
ユーザーが保護されたリソースにアクセスする必要がある場合、ヘッダーで Bearer モードの Authorization ヘッダーを使用する必要があります。その内容は次のようになります。
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk
2.3 jwtのメリットとデメリット
アドバンテージ:
- json はユニバーサルであり、言語を超えて使用できます。
- シンプルな構成、小さなバイト使用、送信しやすい
- サーバーはセッション情報を保存する必要がなく、水平拡張が容易です。
- 1 か所で生成し、複数の場所で使用することで、分散システムにおけるシングル サインオンの問題を解決できます。
- CSRF攻撃から保護します
欠点:
- ペイロード部分は単純にエンコードされているため、ロジックに必要な非機密情報を格納するためにのみ使用できます。
- 暗号化キーは保護する必要があり、一度漏洩すると悲惨な結果になります。
- トークンのハイジャックを避けるには、https プロトコルを使用するのが最善です
- 処理されたトークンは無効化できないため、データの有効期限の問題には対処できません。
3. jwtをプロジェクトに統合してユーザー情報を返すインターフェースを実装する
3.1 jwt ツールキットの作成
3.1.1 プロジェクト ディレクトリで次のコマンドを入力して jwt-go パッケージをインポートします。
go get github.com/dgrijalva/jwt-go
3.1.2 jwt ツールキットを記述するための共通フォルダーに jwt.go ファイルを作成します。
3.1.3 上記の紹介によると、jwt-go の使用は次の部分に分かれています。
1. 使用する暗号化キーを指定します。
2. トークンに含まれるペイロード情報を保存する Claims 構造体を設定します。これには jwt.StandardClaims が含まれている必要があることに注意してください。
3. トークンを生成する関数とトークンを解析する関数を作成します。
コードは以下のように表示されます。
package common
import (
"errors"
"gin_test/model"
"github.com/dgrijalva/jwt-go"
"time"
)
//设定加密秘钥
var jwtKey =[]byte("my key")
//定义声明结构体
type Claims struct {
jwt.StandardClaims
UserId uint
}
//生成token
func GenerateToken(user model.User)(string,error){
//过期时间设定为24小时
expiresationtime:=time.Now().Add(7*24*time.Hour)
claims:=Claims{
UserId: user.ID,
StandardClaims:jwt.StandardClaims{
ExpiresAt:expiresationtime.Unix(),
IssuedAt: time.Now().Unix(),
Issuer: "yzy",
Subject: "user token",
},
}
//创建新的声明
tokenClaims:=jwt.NewWithClaims(jwt.SigningMethodHS256,claims)
tokenstr,err:=tokenClaims.SignedString(jwtKey)
if err != nil {
return "",err
}
return tokenstr,nil
}
//解析token
func ParseToken(tokenstr string)(*Claims,error){
claims:=Claims{}
token,err:=jwt.ParseWithClaims(tokenstr,&claims,func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err!=nil {
return nil,err
}
//如果token无效
if !token.Valid{
return nil,errors.New("the token is invalid")
}
return &claims,nil
}
3.3 上記のログイン ロジックを完了する
上記のログイン ロジックでは、トークンが直接指定されていますが、次に、上で記述した jwt ツールキットを使用して、ログイン ファイルに正しいトークンを生成する必要があります。
トークンを発行するためのコードを変更するだけです。
//发放token
token,err:=common.GenerateToken(user)
if err != nil {
c.JSON(500,gin.H{
"error":"system wrong",
})
log.Printf("token generate error:%v",err)
}
//返回登录成功
c.JSON(200,gin.H{
"msg":"login succeed",
"data":gin.H{
"token":token,
},
})
3.4 ミドルウェアの作成
gin ミドルウェアの作成には、次の 2 つの手順だけが必要です。
1. ミドルウェア関数の作成(フォーマット仕様あり)
2. ルーティングにミドルウェア機能を使用します。
今回書いた認証ミドルウェアは以下の通りです。
func AuthMiddleware()gin.HandlerFunc {
return func(c *gin.Context) {
//获取参数
tokenstr:=c.GetHeader("Authorization")
if len(tokenstr)==0||!strings.HasPrefix(tokenstr,"Bearer "){
c.JSON(400,gin.H{
"error":"Insufficient permissions",
})
c.Abort()//丢弃请求
return
}
tokenstr=tokenstr[7:]
//解析token
claims,err:=common.ParseToken(tokenstr)
if err != nil {
c.JSON(400,gin.H{
"error":"Insufficient permissions",
})
log.Println(err)
c.Abort()//丢弃请求
return
}
//对用户进行鉴权
userid:=claims.UserId
var user model.User
db:=common.GetDB()
db.First(&user,userid)
if user.ID==0{
c.JSON(400,gin.H{
"error":"Insufficient permissions",
})
c.Abort()//丢弃请求
return
}
//如果用户存在,将用户信息传入上下文
c.Set("user",user)
// before request
c.Next()
// after request
log.Println("完成一次鉴权的使用")
}
}
ルーティングでミドルウェアを使用します。
router.GET("/info",middleware.AuthMiddleware(),controller.Info)
途中にミドルウェア機能が追加されており、最後のcontroller.infoは以下に実装されていることに注意してください。
3.5 ユーザー情報を返すためのロジックを作成する
ユーザー情報は機密情報であるため、このインターフェイスを呼び出すには認証が必要です。
func Info(c *gin.Context) {
//从上下文中获取信息
user,_:=c.Get("user")
c.JSON(200,gin.H{
"data":user,
})
}
5. まとめ
将来的にはバージョン管理に git を使用し、シリーズ全体をより理解しやすくするために各章が繰り返し更新される予定です。