セキュリティ
この章では、Go-Sail におけるセキュリティについて説明します。
はじめに
アプリケーションやシステムの種類に関係なく、セキュリティは常に最優先で考慮すべき事項です。
セキュリティ対策はあらゆる側面を含み、すべてのケースを網羅することはできませんが、ここでは Web アプリケーションにおける一般的なセキュリティ保護ソリューションを紹介します。
認証
前章のログインインターフェースを例に、データベーステーブルに指定ユーザーが存在するかを確認し、入力されたパスワードがデータベースに保存されたパスワードと一致するかどうかを比較する方法を実装しました。
しかし、ここには問題があります。パスワードが平文のままデータベースに保存されており、これは非常に危険でプロフェッショナルとは言えません。一般的には、ユーザーのパスワードはデータベースに保存する前に暗号化すべきであり、万が一情報漏洩が起きた場合でも大きな被害を防ぐことができます。
暗号化
現在、さまざまな暗号化・復号化アルゴリズムが存在します。ここでは、一般的によく使われる RSA 暗号化・復号化方式を利用した例を紹介します。
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
)
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
... (省略)
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
... (省略)
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない
if len(loginRequest.Username) == 0 {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
// パスワード不一致
if loginRequest.Password != user.Password {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, afterFunc).Launch()
}
RSAアルゴリズムに必要な公開鍵・秘密鍵は openssl コマンドを使用して生成できます。openssl は多くの MacOS、Linux、Windows システムで利用できます。
以下のコマンドで PKCS8 形式の公開鍵・秘密鍵を生成できます。
openssl genrsa -out keypair.pem 2048 && \
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \
-in keypair.pem \
-out pkcs8.key && \
openssl rsa -in pkcs8.key \
-pubout -out pkcs8.pem && \
rm -f keypair.pem
ここで pkcs8.key が秘密鍵、pkcs8.pem が公開鍵です。
復号化
暗号化の逆の処理が復号化です。暗号文を復号化する必要がある場合、Go-Sail が提供する復号化関数を利用します。例えば上記の例のように、ユーザーが入力したパスワードとデータベースに保存されているパスワードが一致するかを比較する際に利用できます。
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
)
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
... (省略)
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
... (省略)
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない
if len(loginRequest.Username) == 0 {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
passwordDecrypted, _ := sail.Utils().RSA().Decrypt(user.Password, privateKey)
// パスワード不一致
if loginRequest.Password != passwordDecrypted {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, afterFunc).Launch()
}
認可(オーソライゼーション)
認可とは、アプリケーションが現在のセッションでユーザーの身元およびユーザーの取得したアクセス権限を判断し、アクセスの可否等の更なる措置を取れるようにすることです。
上記のコードでは、ユーザー認証が成功した後、ユーザーにアクセストークンを発行し、それがユーザーの身元情報を表しています。ミドルウェア 章で紹介したように、ミドルウェアを用いて簡単にユーザー認証を実現できます。
ユーザーの権限情報をもとに追加の認可判定を行う方法はいくつかあります。たとえば、ユーザー本人の識別情報から DB で権限を取得する、または認証段階で権限情報も一緒に発行(トークンに権限情報をパックする)といったやり方です。
ここでは、業界でよく利用されている JWT(JSON Web Token)ソリューションを利用します。
有効化・設定
Go-Sail の JWTコンポーネントを利用するには、利用前に対応する設定が必要です。
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/jwt"
)
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
... (省略)
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
... (省略)
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
JwtConf: &jwt.Conf{
Enable: true,
PublicKey: publicKey,
PrivateKey: privateKey,
Algorithm: "RS256",
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない
if len(loginRequest.Username) == 0 {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
passwordDecrypted, _ := sail.Utils().RSA().Decrypt(user.Password, privateKey)
// パスワード不一致
if loginRequest.Password != passwordDecrypted {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, afterFunc).Launch()
}
同時に、JWT コンポーネントを有効化すると、Go-Sail に組み込まれている JWT コントロールプレーンから、暗号化・復号化方式を直接利用できます。
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/jwt"
)
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
... (省略)
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
... (省略)
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
JwtConf: &jwt.Conf{
Enable: true,
PublicKey: publicKey,
PrivateKey: privateKey,
Algorithm: "RS256",
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない
if len(loginRequest.Username) == 0 {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
passwordDecrypted, _ := sail.JWT().Decrypt(user.Password, privateKey)
// パスワード不一致
if loginRequest.Password != passwordDecrypted {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.JWT().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, afterFunc).Launch()
}
トークン発行
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/jwt"
)
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
... (省略)
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
... (省略)
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
JwtConf: &jwt.Conf{
Enable: true,
PublicKey: publicKey,
PrivateKey: privateKey,
Algorithm: "RS256",
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない
if len(loginRequest.Username) == 0 {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
passwordDecrypted, _ := sail.Utils().RSA().Decrypt(user.Password, privateKey)
// パスワード不一致
if loginRequest.Password != passwordDecrypted {
sail.Response(c).Failure("login failed, username or password not match!")
return
}
fields := map[string]any{
"scopes": []string{
"/user/info",
"/user/balance",
"/user/orders",
},
}
exp := time.Now().Add(time.Hour*24).Unix()
token, _ := sail.JWT().MakeToken("go-sail", exp, fields)
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
}
)
func main() {
sail.WakeupHttp("go-sail", conf).Hook(registerRoutes, nil, afterFunc).Launch()
}
この例では、ユーザーにアクセストークンを発行する際、3つのリソースへのアクセス権を持たせています:
- /user/info
- /user/balance
- /user/orders
認可の完了
また、認証用ルーティングミドルウェアの検証コードもあわせて修正が必要です。
package main
import (
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/constants"
"github.com/keepchen/go-sail/v3/sail"
)
...
// ValidateToken ユーザーのトークンを検証する
func ValidateToken() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Request.Header.Get("Authorization")
ok, claims, err := sail.JWT().ValidToken(token)
if !ok || err != nil {
sail.Response(c).Wrap(constants.ErrAuthorizationTokenInvalid, nil).Send()
return
}
scopes, ok := claims.["scopes"].([]string)
if !ok {
sail.Response(c).Wrap(constants.ErrAuthorizationTokenInvalid, nil).Send()
return
}
parsedPath := c.Request.URL.Path
var hit bool
for _, scope := range scopes {
if scope == parsedPath {
hit = true
break
}
}
if !hit {
// 権限なし
sail.Response(c).Wrap(constants.ErrAuthorizationTokenInvalid, nil).Send()
return
}
c.Next()
}
}
...
JSON Web Token
Go-Sail の組み込み JWT コンポーネントは golang-jwt/jwt ライブラリを元に実装されており、以下のような複数のアルゴリズムをサポートします。
- RS256
- RS512
- HS512
- EdDSA
- ES256
- ES384
- ES512
ここでは各アルゴリズム用のキー生成コマンド(いずれも openssl コマンドが必要)を説明します。
rsa (pkcs8)
openssl genrsa -out keypair.pem 2048 && \
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \
-in keypair.pem \
-out pkcs8.key && \
openssl rsa -in pkcs8.key \
-pubout -out pkcs8.pem && \
rm -f keypair.pem
hmac シークレット
openssl rand -base64 32
ed25519
openssl genpkey -algorithm ED25519 -out ed25519_private.pem && \
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem
ecdsa (256)
openssl ecparam -name prime256v1 -genkey -noout | \
openssl pkcs8 -topk8 -nocrypt -out ecdsa_p256_private.pem
openssl pkey -in ecdsa_p256_private.pem -pubout -out ecdsa_p256_public.pem
ecdsa (384)
openssl ecparam -name secp384r1 -genkey -noout | \
openssl pkcs8 -topk8 -nocrypt -out ecdsa_p384_private.pem
openssl pkey -in ecdsa_p384_private.pem -pubout -out ecdsa_p384_public.pem
ecdsa (521)
openssl ecparam -name secp521r1 -genkey -noout | \
openssl pkcs8 -topk8 -nocrypt -out ecdsa_p521_private.pem
openssl pkey -in ecdsa_p521_private.pem -pubout -out ecdsa_p521_public.pem
HS512 アルゴリズム以外は、生成した鍵ペアをそれぞれ公開鍵/秘密鍵設定箇所に利用します。HS512 アルゴリズムを利用する場合は、hmac secret で出力された値を HmacSecret 設定項目へ登録してください。