add bcrypt as authentication method

x/bcrypt
Michael Li 2 years ago
parent 859da55cba
commit 2631db9186
No known key found for this signature in database

@ -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. - frontend: add tweets filter support use tag for home page and make it as default behavior.
- add pin topic support. - add pin topic support.
- support upload webp format image as picture when send tweet. - 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 ## 0.5.2
### Change ### Change

@ -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 package conf
import ( import (

@ -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
}

@ -20,7 +20,6 @@ type User struct {
Username string `json:"username"` Username string `json:"username"`
Phone string `json:"phone"` Phone string `json:"phone"`
Password string `json:"password"` Password string `json:"password"`
Salt string `json:"salt"`
Status int `json:"status"` Status int `json:"status"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Balance int64 `json:"balance"` Balance int64 `json:"balance"`

@ -40,7 +40,7 @@ func JWT() gin.HandlerFunc {
// 加载用户信息 // 加载用户信息
if user, err := ums.GetUserByID(claims.UID); err == nil { 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("USER", user)
c.Set("UID", claims.UID) c.Set("UID", claims.UID)
c.Set("USERNAME", claims.Username) c.Set("USERNAME", claims.Username)
@ -132,7 +132,7 @@ func JwtLoose() gin.HandlerFunc {
if claims, err := app.ParseToken(token); err == nil { if claims, err := app.ParseToken(token); err == nil {
// 加载用户信息 // 加载用户信息
user, err := ums.GetUserByID(claims.UID) 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("UID", claims.UID)
c.Set("USERNAME", claims.Username) c.Set("USERNAME", claims.Username)
c.Set("USER", user) c.Set("USER", user)

@ -21,6 +21,7 @@ import (
"github.com/rocboss/paopao-ce/internal/model/web" "github.com/rocboss/paopao-ce/internal/model/web"
"github.com/rocboss/paopao-ce/internal/servants/base" "github.com/rocboss/paopao-ce/internal/servants/base"
"github.com/rocboss/paopao-ce/internal/servants/chain" "github.com/rocboss/paopao-ce/internal/servants/chain"
"github.com/rocboss/paopao-ce/pkg/auth"
"github.com/rocboss/paopao-ce/pkg/xerror" "github.com/rocboss/paopao-ce/pkg/xerror"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -40,6 +41,7 @@ type coreSrv struct {
*base.DaoServant *base.DaoServant
oss core.ObjectStorageService oss core.ObjectStorageService
wc core.WebCache wc core.WebCache
passwordProvider auth.PasswordProvider
messagesExpire int64 messagesExpire int64
prefixMessages string prefixMessages string
} }
@ -305,12 +307,17 @@ func (s *coreSrv) ChangePassword(req *web.ChangePasswordReq) mir.Error {
} }
// 旧密码校验 // 旧密码校验
user := req.User 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 return web.ErrErrorOldPassword
} }
// 更新入库 // 更新入库
user.Password, user.Salt = encryptPasswordAndSalt(req.Password) user.Password, err = s.passwordProvider.Generate(req.Password)
if err := s.Ds.UpdateUser(user); err != nil { 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) logrus.Errorf("Ds.UpdateUser err: %s", err)
return xerror.ServerError return xerror.ServerError
} }
@ -418,7 +425,7 @@ func (s *coreSrv) messagesFromCache(req *web.GetMessagesReq, limit int, offset i
return 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 cs := conf.CacheSetting
return &coreSrv{ return &coreSrv{
DaoServant: s, DaoServant: s,
@ -426,5 +433,6 @@ func newCoreSrv(s *base.DaoServant, oss core.ObjectStorageService, wc core.WebCa
wc: wc, wc: wc,
messagesExpire: cs.MessagesExpire, messagesExpire: cs.MessagesExpire,
prefixMessages: conf.PrefixMessages, prefixMessages: conf.PrefixMessages,
passwordProvider: provider,
} }
} }

@ -22,6 +22,7 @@ import (
"github.com/rocboss/paopao-ce/internal/servants/base" "github.com/rocboss/paopao-ce/internal/servants/base"
"github.com/rocboss/paopao-ce/internal/servants/web/assets" "github.com/rocboss/paopao-ce/internal/servants/web/assets"
"github.com/rocboss/paopao-ce/pkg/app" "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/utils"
"github.com/rocboss/paopao-ce/pkg/version" "github.com/rocboss/paopao-ce/pkg/version"
"github.com/rocboss/paopao-ce/pkg/xerror" "github.com/rocboss/paopao-ce/pkg/xerror"
@ -40,6 +41,8 @@ const (
type pubSrv struct { type pubSrv struct {
api.UnimplementedPubServant api.UnimplementedPubServant
*base.DaoServant *base.DaoServant
passwordProvider auth.PasswordProvider
} }
func (s *pubSrv) SendCaptcha(req *web.SendCaptchaReq) mir.Error { 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) logrus.Errorf("scheckPassword err: %v", err)
return nil, web.ErrUserRegisterFailed 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{ user := &ms.User{
Nickname: req.Username, Nickname: req.Username,
Username: req.Username, Username: req.Username,
Password: password, Password: password,
Avatar: getRandomAvatar(), Avatar: getRandomAvatar(),
Salt: salt,
Status: ms.UserStatusNormal, Status: ms.UserStatusNormal,
} }
user, err := s.Ds.CreateUser(user) user, err = s.Ds.CreateUser(user)
if err != nil { if err != nil {
logrus.Errorf("Ds.CreateUser err: %s", err) logrus.Errorf("Ds.CreateUser err: %s", err)
return nil, web.ErrUserRegisterFailed return nil, web.ErrUserRegisterFailed
@ -137,7 +143,7 @@ func (s *pubSrv) Login(req *web.LoginReq) (*web.LoginResp, mir.Error) {
return nil, web.ErrTooManyLoginError 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 { if user.Status == ms.UserStatusClosed {
return nil, web.ErrUserHasBeenBanned return nil, web.ErrUserHasBeenBanned
} }
@ -187,8 +193,9 @@ func (s *pubSrv) validUsername(username string) mir.Error {
return nil return nil
} }
func newPubSrv(s *base.DaoServant) api.Pub { func newPubSrv(s *base.DaoServant, provider auth.PasswordProvider) api.Pub {
return &pubSrv{ return &pubSrv{
DaoServant: s, DaoServant: s,
passwordProvider: provider,
} }
} }

@ -12,11 +12,9 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/alimy/mir/v4" "github.com/alimy/mir/v4"
"github.com/gofrs/uuid/v5"
"github.com/rocboss/paopao-ce/internal/core" "github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/ms" "github.com/rocboss/paopao-ce/internal/core/ms"
"github.com/rocboss/paopao-ce/internal/model/web" "github.com/rocboss/paopao-ce/internal/model/web"
"github.com/rocboss/paopao-ce/pkg/utils"
"github.com/rocboss/paopao-ce/pkg/xerror" "github.com/rocboss/paopao-ce/pkg/xerror"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -88,18 +86,6 @@ func checkPassword(password string) mir.Error {
return nil 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 删除推文的媒体内容, 宽松处理错误(就是不处理), 后续完善 // deleteOssObjects 删除推文的媒体内容, 宽松处理错误(就是不处理), 后续完善
func deleteOssObjects(oss core.ObjectStorageService, mediaContents []string) { func deleteOssObjects(oss core.ObjectStorageService, mediaContents []string) {
mediaContentsSize := len(mediaContents) mediaContentsSize := len(mediaContents)

@ -31,13 +31,14 @@ var (
func RouteWeb(e *gin.Engine) { func RouteWeb(e *gin.Engine) {
lazyInitial() lazyInitial()
ds := base.NewDaoServant() ds := base.NewDaoServant()
provider := conf.NewPasswordProvider()
// aways register servants // aways register servants
api.RegisterAdminServant(e, newAdminSrv(ds, _wc)) 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.RegisterRelaxServant(e, newRelaxSrv(ds, _wc), newRelaxChain())
api.RegisterLooseServant(e, newLooseSrv(ds, _ac)) api.RegisterLooseServant(e, newLooseSrv(ds, _ac))
api.RegisterPrivServant(e, newPrivSrv(ds, _oss), newPrivChain()) api.RegisterPrivServant(e, newPrivSrv(ds, _oss), newPrivChain())
api.RegisterPubServant(e, newPubSrv(ds)) api.RegisterPubServant(e, newPubSrv(ds, provider))
api.RegisterTrendsServant(e, newTrendsSrv(ds)) api.RegisterTrendsServant(e, newTrendsSrv(ds))
api.RegisterFollowshipServant(e, newFollowshipSrv(ds)) api.RegisterFollowshipServant(e, newFollowshipSrv(ds))
api.RegisterFriendshipServant(e, newFriendshipSrv(ds)) api.RegisterFriendshipServant(e, newFriendshipSrv(ds))

@ -7,6 +7,7 @@ package app
import ( import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"strconv"
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@ -31,7 +32,7 @@ func GenerateToken(user *ms.User) (string, error) {
Username: user.Username, Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expireTime), 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 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)) contents := make([]byte, 0, len(conf.JWTSetting.Issuer)+len(data))
copy(contents, []byte(conf.JWTSetting.Issuer)) copy(contents, []byte(conf.JWTSetting.Issuer))
contents = append(contents, []byte(data)...) contents = append(contents, []byte(data)...)

@ -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{}
}

@ -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)
}

@ -12,6 +12,5 @@ import (
func EncodeMD5(value string) string { func EncodeMD5(value string) string {
m := md5.New() m := md5.New()
m.Write([]byte(value)) m.Write([]byte(value))
return hex.EncodeToString(m.Sum(nil)) return hex.EncodeToString(m.Sum(nil))
} }

@ -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 '密码';

@ -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`;

@ -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 '';

@ -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;

@ -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;

@ -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;

@ -345,8 +345,7 @@ CREATE TABLE `p_user` (
`nickname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称', `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 '用户名', `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 '手机号', `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密码', `password` varchar(255) NOT NULL DEFAULT '' COMMENT '密码',
`salt` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '盐值',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态1正常2停用', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态1正常2停用',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像', `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像',
`balance` BIGINT NOT NULL COMMENT '用户余额(分)', `balance` BIGINT NOT NULL COMMENT '用户余额(分)',

@ -283,8 +283,7 @@ CREATE TABLE p_user (
nickname VARCHAR(32) NOT NULL DEFAULT '', nickname VARCHAR(32) NOT NULL DEFAULT '',
username VARCHAR(32) NOT NULL DEFAULT '', username VARCHAR(32) NOT NULL DEFAULT '',
phone VARCHAR(16) NOT NULL DEFAULT '', -- 手机号 phone VARCHAR(16) NOT NULL DEFAULT '', -- 手机号
password VARCHAR(32) NOT NULL DEFAULT '', -- MD5密码 password VARCHAR(255) NOT NULL DEFAULT '', -- 密码
salt VARCHAR(16) NOT NULL DEFAULT '', -- 盐值
status SMALLINT NOT NULL DEFAULT 1, -- 状态1正常2停用 status SMALLINT NOT NULL DEFAULT 1, -- 状态1正常2停用
avatar VARCHAR(255) NOT NULL DEFAULT '', avatar VARCHAR(255) NOT NULL DEFAULT '',
balance BIGINT NOT NULL, -- 用户余额(分) balance BIGINT NOT NULL, -- 用户余额(分)

@ -360,8 +360,7 @@ CREATE TABLE "p_user" (
"nickname" text(32) NOT NULL, "nickname" text(32) NOT NULL,
"username" text(32) NOT NULL, "username" text(32) NOT NULL,
"phone" text(16) NOT NULL, "phone" text(16) NOT NULL,
"password" text(32) NOT NULL, "password" text(255) NOT NULL,
"salt" text(16) NOT NULL,
"status" integer NOT NULL, "status" integer NOT NULL,
"avatar" text(255) NOT NULL, "avatar" text(255) NOT NULL,
"balance" integer NOT NULL, "balance" integer NOT NULL,

Loading…
Cancel
Save