diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b2f9eed6..7b64389fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ All notable changes to paopao-ce are documented in this file. - frontend: add tweets filter support use tag for home page and make it as default behavior. - add pin topic support. - support upload webp format image as picture when send tweet. +- support use bcrypt or md5 as authentication method. Use md5 as authentication default if not custom add `BcryptAuthMethod` or `Md5AuthMethod` to `conf.yaml` 's `Features` section. + add `BcryptAuthMethod` or `Md5AuthMethod` to `conf.yaml` 's `Features` section to enable this feature like below: + ```yaml + # file config.yaml + ... + Features: + Default: ["Postgres", "Meili", "LocalOSS", "LoggerOpenObserve", "BcryptAuthMethod", "web"] + ... + ``` + + ## 0.5.2 ### Change diff --git a/internal/conf/alipay.go b/internal/conf/alipay.go index 8d1b8d0f1..8d0b226ca 100644 --- a/internal/conf/alipay.go +++ b/internal/conf/alipay.go @@ -1,3 +1,7 @@ +// Copyright 2023 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + package conf import ( diff --git a/internal/conf/auth.go b/internal/conf/auth.go new file mode 100644 index 000000000..7a672a4ac --- /dev/null +++ b/internal/conf/auth.go @@ -0,0 +1,27 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package conf + +import ( + "github.com/alimy/tryst/cfg" + "github.com/rocboss/paopao-ce/pkg/auth" + "golang.org/x/crypto/bcrypt" +) + +func NewPasswordProvider() (provider auth.PasswordProvider) { + cfg.On(cfg.Actions{ + "Md5AuthMethod": func() { + provider = auth.NewMd5PasswordProvider() + }, + "BcryptAuthMethod": func() { + provider = auth.NewBcryptPasswordProvider(bcrypt.DefaultCost) + }, + }, + func() { + provider = auth.NewMd5PasswordProvider() + }, + ) + return +} diff --git a/internal/dao/jinzhu/dbr/user.go b/internal/dao/jinzhu/dbr/user.go index 4d7f7f6d3..8ab042245 100644 --- a/internal/dao/jinzhu/dbr/user.go +++ b/internal/dao/jinzhu/dbr/user.go @@ -20,7 +20,6 @@ type User struct { Username string `json:"username"` Phone string `json:"phone"` Password string `json:"password"` - Salt string `json:"salt"` Status int `json:"status"` Avatar string `json:"avatar"` Balance int64 `json:"balance"` diff --git a/internal/servants/chain/jwt.go b/internal/servants/chain/jwt.go index 1400808d7..73534d2bf 100644 --- a/internal/servants/chain/jwt.go +++ b/internal/servants/chain/jwt.go @@ -40,7 +40,7 @@ func JWT() gin.HandlerFunc { // 加载用户信息 if user, err := ums.GetUserByID(claims.UID); err == nil { // 强制下线机制 - if app.IssuerFrom(user.Salt) == claims.Issuer { + if app.IssuerFrom(user.CreatedOn) == claims.Issuer { c.Set("USER", user) c.Set("UID", claims.UID) c.Set("USERNAME", claims.Username) @@ -132,7 +132,7 @@ func JwtLoose() gin.HandlerFunc { if claims, err := app.ParseToken(token); err == nil { // 加载用户信息 user, err := ums.GetUserByID(claims.UID) - if err == nil && app.IssuerFrom(user.Salt) == claims.Issuer { + if err == nil && app.IssuerFrom(user.CreatedOn) == claims.Issuer { c.Set("UID", claims.UID) c.Set("USERNAME", claims.Username) c.Set("USER", user) diff --git a/internal/servants/web/core.go b/internal/servants/web/core.go index 4430f6ee7..b7148b9ec 100644 --- a/internal/servants/web/core.go +++ b/internal/servants/web/core.go @@ -21,6 +21,7 @@ import ( "github.com/rocboss/paopao-ce/internal/model/web" "github.com/rocboss/paopao-ce/internal/servants/base" "github.com/rocboss/paopao-ce/internal/servants/chain" + "github.com/rocboss/paopao-ce/pkg/auth" "github.com/rocboss/paopao-ce/pkg/xerror" "github.com/sirupsen/logrus" ) @@ -38,10 +39,11 @@ var ( type coreSrv struct { api.UnimplementedCoreServant *base.DaoServant - oss core.ObjectStorageService - wc core.WebCache - messagesExpire int64 - prefixMessages string + oss core.ObjectStorageService + wc core.WebCache + passwordProvider auth.PasswordProvider + messagesExpire int64 + prefixMessages string } func (s *coreSrv) Chain() gin.HandlersChain { @@ -305,12 +307,17 @@ func (s *coreSrv) ChangePassword(req *web.ChangePasswordReq) mir.Error { } // 旧密码校验 user := req.User - if !validPassword(user.Password, req.OldPassword, req.User.Salt) { + err := s.passwordProvider.Compare(user.Password, req.OldPassword) + if err != nil { return web.ErrErrorOldPassword } // 更新入库 - user.Password, user.Salt = encryptPasswordAndSalt(req.Password) - if err := s.Ds.UpdateUser(user); err != nil { + user.Password, err = s.passwordProvider.Generate(req.Password) + if err != nil { + logrus.Errorf("generate hashed password err: %s", err) + return xerror.ServerError + } + if err = s.Ds.UpdateUser(user); err != nil { logrus.Errorf("Ds.UpdateUser err: %s", err) return xerror.ServerError } @@ -418,13 +425,14 @@ func (s *coreSrv) messagesFromCache(req *web.GetMessagesReq, limit int, offset i return } -func newCoreSrv(s *base.DaoServant, oss core.ObjectStorageService, wc core.WebCache) api.Core { +func newCoreSrv(s *base.DaoServant, oss core.ObjectStorageService, wc core.WebCache, provider auth.PasswordProvider) api.Core { cs := conf.CacheSetting return &coreSrv{ - DaoServant: s, - oss: oss, - wc: wc, - messagesExpire: cs.MessagesExpire, - prefixMessages: conf.PrefixMessages, + DaoServant: s, + oss: oss, + wc: wc, + messagesExpire: cs.MessagesExpire, + prefixMessages: conf.PrefixMessages, + passwordProvider: provider, } } diff --git a/internal/servants/web/pub.go b/internal/servants/web/pub.go index a16110349..b81961bf2 100644 --- a/internal/servants/web/pub.go +++ b/internal/servants/web/pub.go @@ -22,6 +22,7 @@ import ( "github.com/rocboss/paopao-ce/internal/servants/base" "github.com/rocboss/paopao-ce/internal/servants/web/assets" "github.com/rocboss/paopao-ce/pkg/app" + "github.com/rocboss/paopao-ce/pkg/auth" "github.com/rocboss/paopao-ce/pkg/utils" "github.com/rocboss/paopao-ce/pkg/version" "github.com/rocboss/paopao-ce/pkg/xerror" @@ -40,6 +41,8 @@ const ( type pubSrv struct { api.UnimplementedPubServant *base.DaoServant + + passwordProvider auth.PasswordProvider } func (s *pubSrv) SendCaptcha(req *web.SendCaptchaReq) mir.Error { @@ -104,16 +107,19 @@ func (s *pubSrv) Register(req *web.RegisterReq) (*web.RegisterResp, mir.Error) { logrus.Errorf("scheckPassword err: %v", err) return nil, web.ErrUserRegisterFailed } - password, salt := encryptPasswordAndSalt(req.Password) + password, err := s.passwordProvider.Generate(req.Password) + if err != nil { + logrus.Errorf("generate hashed password err: %v", err) + return nil, web.ErrUserRegisterFailed + } user := &ms.User{ Nickname: req.Username, Username: req.Username, Password: password, Avatar: getRandomAvatar(), - Salt: salt, Status: ms.UserStatusNormal, } - user, err := s.Ds.CreateUser(user) + user, err = s.Ds.CreateUser(user) if err != nil { logrus.Errorf("Ds.CreateUser err: %s", err) return nil, web.ErrUserRegisterFailed @@ -137,7 +143,7 @@ func (s *pubSrv) Login(req *web.LoginReq) (*web.LoginResp, mir.Error) { return nil, web.ErrTooManyLoginError } // 对比密码是否正确 - if validPassword(user.Password, req.Password, user.Salt) { + if err := s.passwordProvider.Compare(user.Password, req.Password); err == nil { if user.Status == ms.UserStatusClosed { return nil, web.ErrUserHasBeenBanned } @@ -187,8 +193,9 @@ func (s *pubSrv) validUsername(username string) mir.Error { return nil } -func newPubSrv(s *base.DaoServant) api.Pub { +func newPubSrv(s *base.DaoServant, provider auth.PasswordProvider) api.Pub { return &pubSrv{ - DaoServant: s, + DaoServant: s, + passwordProvider: provider, } } diff --git a/internal/servants/web/utils.go b/internal/servants/web/utils.go index 0e2696e31..0f61c6f2c 100644 --- a/internal/servants/web/utils.go +++ b/internal/servants/web/utils.go @@ -12,11 +12,9 @@ import ( "unicode/utf8" "github.com/alimy/mir/v4" - "github.com/gofrs/uuid/v5" "github.com/rocboss/paopao-ce/internal/core" "github.com/rocboss/paopao-ce/internal/core/ms" "github.com/rocboss/paopao-ce/internal/model/web" - "github.com/rocboss/paopao-ce/pkg/utils" "github.com/rocboss/paopao-ce/pkg/xerror" "github.com/sirupsen/logrus" ) @@ -88,18 +86,6 @@ func checkPassword(password string) mir.Error { return nil } -// ValidPassword 检查密码是否一致 -func validPassword(dbPassword, password, salt string) bool { - return strings.Compare(dbPassword, utils.EncodeMD5(utils.EncodeMD5(password)+salt)) == 0 -} - -// encryptPasswordAndSalt 密码加密&生成salt -func encryptPasswordAndSalt(password string) (string, string) { - salt := uuid.Must(uuid.NewV4()).String()[:8] - password = utils.EncodeMD5(utils.EncodeMD5(password) + salt) - return password, salt -} - // deleteOssObjects 删除推文的媒体内容, 宽松处理错误(就是不处理), 后续完善 func deleteOssObjects(oss core.ObjectStorageService, mediaContents []string) { mediaContentsSize := len(mediaContents) diff --git a/internal/servants/web/web.go b/internal/servants/web/web.go index 7f95eee2e..3f2280052 100644 --- a/internal/servants/web/web.go +++ b/internal/servants/web/web.go @@ -31,13 +31,14 @@ var ( func RouteWeb(e *gin.Engine) { lazyInitial() ds := base.NewDaoServant() + provider := conf.NewPasswordProvider() // aways register servants api.RegisterAdminServant(e, newAdminSrv(ds, _wc)) - api.RegisterCoreServant(e, newCoreSrv(ds, _oss, _wc)) + api.RegisterCoreServant(e, newCoreSrv(ds, _oss, _wc, provider)) api.RegisterRelaxServant(e, newRelaxSrv(ds, _wc), newRelaxChain()) api.RegisterLooseServant(e, newLooseSrv(ds, _ac)) api.RegisterPrivServant(e, newPrivSrv(ds, _oss), newPrivChain()) - api.RegisterPubServant(e, newPubSrv(ds)) + api.RegisterPubServant(e, newPubSrv(ds, provider)) api.RegisterTrendsServant(e, newTrendsSrv(ds)) api.RegisterFollowshipServant(e, newFollowshipSrv(ds)) api.RegisterFriendshipServant(e, newFriendshipSrv(ds)) diff --git a/pkg/app/jwt.go b/pkg/app/jwt.go index 75a43f7ad..3ac106a9f 100644 --- a/pkg/app/jwt.go +++ b/pkg/app/jwt.go @@ -7,6 +7,7 @@ package app import ( "crypto/md5" "encoding/hex" + "strconv" "time" "github.com/golang-jwt/jwt/v5" @@ -31,7 +32,7 @@ func GenerateToken(user *ms.User) (string, error) { Username: user.Username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expireTime), - Issuer: IssuerFrom(user.Salt), + Issuer: IssuerFrom(user.CreatedOn), }, } @@ -53,7 +54,8 @@ func ParseToken(token string) (res *Claims, err error) { return } -func IssuerFrom(data string) string { +func IssuerFrom(num int64) string { + data := strconv.FormatInt(num, 10) contents := make([]byte, 0, len(conf.JWTSetting.Issuer)+len(data)) copy(contents, []byte(conf.JWTSetting.Issuer)) contents = append(contents, []byte(data)...) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 000000000..5878e8b57 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,60 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package auth + +import ( + "errors" + "strings" + + "github.com/gofrs/uuid/v5" + "github.com/rocboss/paopao-ce/pkg/utils" + "golang.org/x/crypto/bcrypt" +) + +type PasswordProvider interface { + Generate(password string) (string, error) + Compare(hashedPassword, password string) error +} + +type bcryptPasswordProvider struct { + cost int +} + +type md5PasswordProvider struct{} + +func (p *bcryptPasswordProvider) Generate(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), p.cost) + return utils.String(hashedPassword), err +} + +func (p *bcryptPasswordProvider) Compare(hashedPassword, password string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + +func (p md5PasswordProvider) Generate(password string) (string, error) { + salt := uuid.Must(uuid.NewV4()).String()[:8] + return utils.EncodeMD5(utils.EncodeMD5(password)+salt) + ":" + salt, nil +} + +func (p md5PasswordProvider) Compare(hashedPassword, password string) error { + passwordSalt := strings.Split(string(hashedPassword), ":") + if len(passwordSalt) != 2 { + return errors.New("invalid hashed password") + } + if strings.Compare(passwordSalt[0], utils.EncodeMD5(utils.EncodeMD5(password)+passwordSalt[1])) != 0 { + return errors.New("invalid password") + } + return nil +} + +func NewBcryptPasswordProvider(cost int) PasswordProvider { + return &bcryptPasswordProvider{ + cost: cost, + } +} + +func NewMd5PasswordProvider() PasswordProvider { + return md5PasswordProvider{} +} diff --git a/pkg/types/password.go b/pkg/types/password.go deleted file mode 100644 index 1caf7aaf4..000000000 --- a/pkg/types/password.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 ROC. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package types - -import ( - "golang.org/x/crypto/bcrypt" -) - -type PasswordProvider interface { - Generate(password []byte) ([]byte, error) - Compare(hashedPassword, password []byte) error -} - -func NewBcryptPasswordProvider(cost int) PasswordProvider { - return &bcryptPasswordProvider{ - cost: cost, - } -} - -type bcryptPasswordProvider struct { - cost int -} - -func (p *bcryptPasswordProvider) Generate(password []byte) ([]byte, error) { - return bcrypt.GenerateFromPassword(password, p.cost) -} - -func (p *bcryptPasswordProvider) Compare(hashedPassword, password []byte) error { - return bcrypt.CompareHashAndPassword(hashedPassword, password) -} diff --git a/pkg/utils/md5.go b/pkg/utils/md5.go index d9b1e9084..a44c89eb7 100644 --- a/pkg/utils/md5.go +++ b/pkg/utils/md5.go @@ -12,6 +12,5 @@ import ( func EncodeMD5(value string) string { m := md5.New() m.Write([]byte(value)) - return hex.EncodeToString(m.Sum(nil)) } diff --git a/scripts/migration/mysql/0016_password_use_bcrypt.down.sql b/scripts/migration/mysql/0016_password_use_bcrypt.down.sql new file mode 100644 index 000000000..43ef97225 --- /dev/null +++ b/scripts/migration/mysql/0016_password_use_bcrypt.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE `p_user` ADD COLUMN `salt` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '盐值'; + +UPDATE + `p_user` +SET + `salt` = SUBSTRING_INDEX(`password`, ':', -1), + `password` = SUBSTRING_INDEX(`password`, ':', 1); + +ALTER TABLE `p_user` MODIFY COLUMN `password` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '密码'; diff --git a/scripts/migration/mysql/0016_password_use_bcrypt.up.sql b/scripts/migration/mysql/0016_password_use_bcrypt.up.sql new file mode 100644 index 000000000..0ff38b725 --- /dev/null +++ b/scripts/migration/mysql/0016_password_use_bcrypt.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE `p_user` MODIFY COLUMN `password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '密码'; + +UPDATE + p_user +SET + password = CONCAT_WS(':', password, salt); + +ALTER TABLE `p_user` DROP COLUMN `salt`; diff --git a/scripts/migration/postgres/0015_password_use_bcrypt.down.sql b/scripts/migration/postgres/0015_password_use_bcrypt.down.sql new file mode 100644 index 000000000..2d348b1a6 --- /dev/null +++ b/scripts/migration/postgres/0015_password_use_bcrypt.down.sql @@ -0,0 +1,12 @@ +ALTER TABLE p_user ADD COLUMN salt VARCHAR(32) NOT NULL DEFAULT ''; + +UPDATE + p_user +SET + salt = split_part(password, ':', -1), + password = split_part(password, ':', 1); + +ALTER TABLE p_user +ALTER COLUMN password TYPE VARCHAR(64), +ALTER COLUMN password SET NOT NULL, +ALTER COLUMN password SET DEFAULT ''; diff --git a/scripts/migration/postgres/0015_password_use_bcrypt.up.sql b/scripts/migration/postgres/0015_password_use_bcrypt.up.sql new file mode 100644 index 000000000..c75f31cd8 --- /dev/null +++ b/scripts/migration/postgres/0015_password_use_bcrypt.up.sql @@ -0,0 +1,11 @@ +ALTER TABLE p_user +ALTER COLUMN password TYPE VARCHAR(255) +ALTER COLUMN password SET NOT NULL +ALTER COLUMN password SET DEFAULT ''; + +UPDATE + p_user +SET + password = concat_ws(':', password, salt); + +ALTER TABLE p_user DROP COLUMN salt; diff --git a/scripts/migration/sqlite3/0016_password_use_bcrypt.down.sql b/scripts/migration/sqlite3/0016_password_use_bcrypt.down.sql new file mode 100644 index 000000000..aecd59855 --- /dev/null +++ b/scripts/migration/sqlite3/0016_password_use_bcrypt.down.sql @@ -0,0 +1,11 @@ +ALTER TABLE p_user ADD COLUMN salt text(32) NOT NULL DEFAULT ''; +ALTER TABLE p_user ADD COLUMN password_copy text(64) NOT NULL DEFAULT ''; + +UPDATE + p_user +SET + salt = substr(password, instr(password, ':')+1), + password_copy = substr(password, 1, instr(password, ':')-1); + +ALTER TABLE p_user DROP COLUMN password; +ALTER TABLE p_user RERENAME COLUMN password_copy TO password; diff --git a/scripts/migration/sqlite3/0016_password_use_bcrypt.up.sql b/scripts/migration/sqlite3/0016_password_use_bcrypt.up.sql new file mode 100644 index 000000000..a9d342205 --- /dev/null +++ b/scripts/migration/sqlite3/0016_password_use_bcrypt.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE p_user ADD COLUMN password_copy text(255) NOT NULL DEFAULT ''; +UPDATE p_user SET password_copy = concat_ws(':', password, salt); +ALTER TABLE p_user DROP COLUMN password; +ALTER TABLE p_user RERENAME COLUMN password_copy TO password; +ALTER TABLE p_user DROP COLUMN salt; diff --git a/scripts/paopao-mysql.sql b/scripts/paopao-mysql.sql index 3e47c691c..2325e133e 100644 --- a/scripts/paopao-mysql.sql +++ b/scripts/paopao-mysql.sql @@ -345,8 +345,7 @@ CREATE TABLE `p_user` ( `nickname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称', `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', `phone` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号', - `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'MD5密码', - `salt` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '盐值', + `password` varchar(255) NOT NULL DEFAULT '' COMMENT '密码', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态,1正常,2停用', `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像', `balance` BIGINT NOT NULL COMMENT '用户余额(分)', diff --git a/scripts/paopao-postgres.sql b/scripts/paopao-postgres.sql index 274c1fe1f..ef4e967ef 100644 --- a/scripts/paopao-postgres.sql +++ b/scripts/paopao-postgres.sql @@ -283,8 +283,7 @@ CREATE TABLE p_user ( nickname VARCHAR(32) NOT NULL DEFAULT '', username VARCHAR(32) NOT NULL DEFAULT '', phone VARCHAR(16) NOT NULL DEFAULT '', -- 手机号 - password VARCHAR(32) NOT NULL DEFAULT '', -- MD5密码 - salt VARCHAR(16) NOT NULL DEFAULT '', -- 盐值 + password VARCHAR(255) NOT NULL DEFAULT '', -- 密码 status SMALLINT NOT NULL DEFAULT 1, -- 状态,1正常,2停用 avatar VARCHAR(255) NOT NULL DEFAULT '', balance BIGINT NOT NULL, -- 用户余额(分) diff --git a/scripts/paopao-sqlite3.sql b/scripts/paopao-sqlite3.sql index 43ab63df8..784a64960 100644 --- a/scripts/paopao-sqlite3.sql +++ b/scripts/paopao-sqlite3.sql @@ -360,8 +360,7 @@ CREATE TABLE "p_user" ( "nickname" text(32) NOT NULL, "username" text(32) NOT NULL, "phone" text(16) NOT NULL, - "password" text(32) NOT NULL, - "salt" text(16) NOT NULL, + "password" text(255) NOT NULL, "status" integer NOT NULL, "avatar" text(255) NOT NULL, "balance" integer NOT NULL,