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.
- 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

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

@ -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"`
Phone string `json:"phone"`
Password string `json:"password"`
Salt string `json:"salt"`
Status int `json:"status"`
Avatar string `json:"avatar"`
Balance int64 `json:"balance"`

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

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

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

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

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

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

@ -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 {
m := md5.New()
m.Write([]byte(value))
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 '昵称',
`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 '用户余额(分)',

@ -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, -- 用户余额(分)

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

Loading…
Cancel
Save