完成部分接口

dev
truthhun 2 years ago
parent cef49c086c
commit f8f21003b6

@ -38,9 +38,15 @@ message User {
}
message RegisterAndLoginRequest {
string username = 1 [(gogoproto.moretags) = "validate:\"min=4,max=32,alphanum\""];
string password = 2 [(gogoproto.moretags) = "validate:\"min=6\""];
string username = 1
[ (gogoproto.moretags) = "validate:\"min=4,max=32,alphanum\"" ];
string password = 2 [ (gogoproto.moretags) = "validate:\"min=6\"" ];
string captcha = 3;
string captcha_id = 4;
}
message GetUserCaptchaRequest {
string type = 1; // registerlogincommentfind_passwordupload
}
message LoginReply { string token = 1; }
@ -67,6 +73,13 @@ message ListUserReply {
repeated User user = 2;
}
message GetUserCaptchaReply {
bool enable = 1;
string id = 2;
string captcha = 3;
string type = 4;
}
//
message UpdateUserPasswordRequest {
int64 id = 1;
@ -122,6 +135,13 @@ service UserAPI {
};
}
// GetUserCaptcha
rpc GetUserCaptcha(GetUserCaptchaRequest) returns (GetUserCaptchaReply) {
option (google.api.http) = {
get : '/api/v1/user/captcha',
};
}
//
// rpc ListUserFans(ListUserFansRequest) returns (ListUserReply) {
// option (google.api.http) = {

@ -7,6 +7,9 @@ import (
"moredoc/model"
"moredoc/util/validate"
"moredoc/util/captcha"
"github.com/alexandrevicenzi/unchained"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@ -28,15 +31,31 @@ func (s *UserAPIService) getValidFieldMap() map[string]string {
}
// Register 用户注册
// TODO: 1. 判断系统是否启用了注册
// TODO: 2. 如果系统启用了注册,判断是否需要管理员审核
// TODO: 3. 如果启用了验证码功能,则需要判断验证码是否正确
// TODO: 1. 如果系统启用了注册,判断是否需要管理员审核
func (s *UserAPIService) Register(ctx context.Context, req *pb.RegisterAndLoginRequest) (*emptypb.Empty, error) {
err := validate.ValidateStruct(req, s.getValidFieldMap())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
cfg := s.dbModel.GetConfigOfSecurity(
model.ConfigSecurityEnableCaptchaRegister,
model.ConfigSecurityEnableRegister,
model.ConfigSecurityIsClose,
)
if !cfg.EnableRegister {
return nil, status.Errorf(codes.InvalidArgument, "系统未开放注册")
}
if !cfg.IsClose {
return nil, status.Errorf(codes.InvalidArgument, "网站已关闭,占时不允许注册")
}
if cfg.EnableCaptchaRegister && !captcha.VerifyCaptcha(req.CaptchaId, req.Captcha) {
return nil, status.Errorf(codes.InvalidArgument, "验证码错误")
}
exist, _ := s.dbModel.GetUserByUsername(req.Username, "id")
if exist.Id > 0 {
return nil, status.Errorf(codes.AlreadyExists, "用户名已存在")
@ -51,13 +70,34 @@ func (s *UserAPIService) Register(ctx context.Context, req *pb.RegisterAndLoginR
return &emptypb.Empty{}, nil
}
// Login 用户登录
// TODO: 1. 判断是否启用了验证码,如果启用了验证码,则需要进行验证码验证
func (s *UserAPIService) Login(ctx context.Context, req *pb.RegisterAndLoginRequest) (*pb.LoginReply, error) {
errValidate := validate.ValidateStruct(req, s.getValidFieldMap())
if errValidate != nil {
return nil, status.Errorf(codes.InvalidArgument, errValidate.Error())
}
return &pb.LoginReply{}, nil
// 如果启用了验证码,则需要进行验证码验证
cfg := s.dbModel.GetConfigOfSecurity(model.ConfigSecurityEnableCaptchaLogin)
if cfg.EnableCaptchaLogin && !captcha.VerifyCaptcha(req.CaptchaId, req.Captcha) {
return nil, status.Errorf(codes.InvalidArgument, "验证码错误")
}
user, err := s.dbModel.GetUserByUsername(req.Username, "id", "password")
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
if ok, err := unchained.CheckPassword(req.Password, user.Password); !ok || err != nil {
return nil, status.Errorf(codes.InvalidArgument, "用户名或密码错误")
}
token, err := s.dbModel.CreateUserJWTToken(user.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
return &pb.LoginReply{Token: token}, nil
}
func (s *UserAPIService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
@ -75,3 +115,36 @@ func (s *UserAPIService) DeleteUser(ctx context.Context, req *pb.DeleteUserReque
func (s *UserAPIService) ListUser(ctx context.Context, req *pb.ListUserRequest) (*pb.ListUserReply, error) {
return &pb.ListUserReply{}, nil
}
// GetUserCaptcha 获取用户验证码
func (s *UserAPIService) GetUserCaptcha(ctx context.Context, req *pb.GetUserCaptchaRequest) (res *pb.GetUserCaptchaReply, err error) {
cfgCaptcha := s.dbModel.GetConfigOfCaptcha()
cfgSecurity := s.dbModel.GetConfigOfSecurity()
res = &pb.GetUserCaptchaReply{
Enable: false,
Type: cfgCaptcha.Type,
}
switch req.Type {
case "register":
res.Enable = cfgSecurity.EnableCaptchaRegister
case "login":
res.Enable = cfgSecurity.EnableCaptchaLogin
case "upload":
res.Enable = cfgSecurity.EnableCaptchaUpload
case "find_password":
res.Enable = cfgSecurity.EnableCaptchaFindPassword
case "comment":
res.Enable = cfgSecurity.EnableCaptchaComment
default:
return nil, status.Errorf(codes.InvalidArgument, "不支持的验证码类型")
}
if res.Enable {
res.Id, res.Captcha, err = captcha.GenerateCaptcha(cfgCaptcha.Type)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
}
return res, nil
}

@ -23,6 +23,8 @@ require (
)
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7 // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
)
@ -37,6 +39,8 @@ require (
github.com/go-playground/validator v9.31.0+incompatible
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/gofrs/uuid v4.3.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
@ -47,6 +51,7 @@ require (
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.5
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/afero v1.6.0 // indirect

@ -2,13 +2,32 @@ package model
import (
"fmt"
"strconv"
"strings"
"time"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"gorm.io/gorm"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
const (
// ConfigCategorySystem 系统配置系统名称、logo、版权信息、是否闭站等
ConfigCategorySystem = "system"
// ConfigCategoryUser 用户配置:是否启用注册、是否需要审核等
ConfigCategoryUser = "user"
// ConfigCategoryEmail 邮箱配置smtp服务器、端口、用户名、密码、发件人
ConfigCategoryEmail = "email"
// ConfigCategoryCaptcha 验证码配置:是否启用验证码、验证码有效期、验证码长度、验证码类型等
ConfigCategoryCaptcha = "captcha"
// ConfigCategoryJWT JWT配置JWT有效期、JWT加密密钥等
ConfigCategoryJWT = "jwt"
// ConfigCategorySecurity 安全配置项
ConfigCategorySecurity = "security"
)
type Config struct {
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"`
Label string `form:"label" json:"label,omitempty" gorm:"column:label;type:varchar(64);size:64;default:;comment:标签名称;"`
@ -202,3 +221,166 @@ func (m *DBModel) DeleteConfig(ids []interface{}) (err error) {
}
return
}
type ConfigJWT struct {
Duration int `json:"duration"` // JWT有效期
Secret string `json:"secret"` // JWT加密密钥
}
// GetConfigJWT 获取JWT配置
func (m *DBModel) GetConfigOfJWT() (config ConfigJWT) {
var configs []Config
err := m.db.Where("category = ?", ConfigCategoryJWT).Find(&configs).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigJWT", zap.Error(err))
}
var data = make(map[string]interface{})
for _, cfg := range configs {
switch cfg.Name {
case "secret":
value := cfg.Value
if value == "" {
value = "moredoc"
}
data[cfg.Name] = value
case "duration":
value, _ := strconv.Atoi(cfg.Value)
if value <= 0 {
value = 3600 * 24 * 7
}
data[cfg.Name] = value
}
}
bytes, _ := json.Marshal(data)
json.Unmarshal(bytes, &config)
return
}
type ConfigCaptcha struct {
Length int `json:"length"` // 验证码长度
Width int `json:"width"`
Height int `json:"height"`
Type string `json:"type"` // 验证码类型
}
// GetConfigOfCaptcha 获取验证码配置
func (m *DBModel) GetConfigOfCaptcha() (config ConfigCaptcha) {
var configs []Config
err := m.db.Where("category = ?", ConfigCategoryCaptcha).Find(&configs).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfCaptcha", zap.Error(err))
}
for _, cfg := range configs {
switch cfg.Name {
case "length":
config.Length, _ = strconv.Atoi(cfg.Value)
if config.Length <= 0 {
config.Length = 6
}
case "width":
config.Width, _ = strconv.Atoi(cfg.Value)
if config.Width <= 0 {
config.Width = 240
}
case "height":
config.Height, _ = strconv.Atoi(cfg.Value)
if config.Height <= 0 {
config.Height = 60
}
case "type":
// 验证码类型
config.Type = cfg.Value
}
}
return
}
type ConfigSystem struct {
Domain string `json:"domain"` // 站点域名,不带 HTTPS:// 和 HTTP://
Title string `json:"title"` // 系统名称
Keywords string `json:"keywords"` // 系统关键字
Description string `json:"description"` // 系统描述
Logo string `json:"logo"` // logo
Favicon string `json:"favicon"` // logo
Theme string `json:"theme"` // 网站主题
Copyright string `json:"copyright"` // 版权信息
ICP string `json:"icp"` // 网站备案
Analytics string `json:"analytics"` // 统计代码
}
// GetConfigOfSystem 获取系统配置
func (m *DBModel) GetConfigOfSystem() (config ConfigSystem) {
var confgis []Config
err := m.db.Where("category = ?", ConfigCategorySystem).Find(&confgis).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfSystem", zap.Error(err))
}
var data = make(map[string]interface{})
for _, cfg := range confgis {
switch cfg.Name {
// 字符串类型的配置项
case "title", "description", "keywords", "logo", "favicon", "icp", "domain", "analytics", "theme", "copyright":
data[cfg.Name] = cfg.Value
}
}
bytes, _ := json.Marshal(data)
json.Unmarshal(bytes, &config)
return
}
var (
ConfigSecurityIsClose = "is_close" // 是否关闭注册
ConfigSecurityEnableRegister = "enable_register" // 是否允许注册
ConfigSecurityEnableCaptchaLogin = "enable_captcha_login" // 是否开启登录验证码
ConfigSecurityEnableCaptchaRegister = "enable_captcha_register" // 是否开启注册验证码
ConfigSecurityEnableCaptchaComment = "enable_captcha_comment" // 是否开启注册验证码
ConfigSecurityEnableCaptchaFindPassword = "enable_captcha_find_password" // 是否开启注册验证码
ConfigSecurityEnableCaptchaUpload = "enable_captcha_upload" // 是否开启注册验证码
)
type ConfigSecurity struct {
IsClose bool `json:"is_close"` // 是否闭站
EnableRegister bool `json:"enable_register"` // 是否启用注册
EnableCaptchaLogin bool `json:"enable_captcha_login"` // 是否启用登录验证码
EnableCaptchaRegister bool `json:"enable_captcha_register"` // 是否启用注册验证码
EnableCaptchaComment bool `json:"enable_captcha_comment"` // 是否启用评论验证码
EnableCaptchaFindPassword bool `json:"enable_captcha_find_password"` // 找回密码是否需要验证码
EnableCaptchaUpload bool `json:"enable_captcha_upload"` // 上传文档是否需要验证码
}
// GetConfigOfSecurity 获取安全配置
func (m *DBModel) GetConfigOfSecurity(name ...string) (config ConfigSecurity) {
var configs []Config
db := m.db.Where("category = ?", ConfigCategorySecurity)
if len(name) > 0 {
db = db.Where("name in (?)", name)
}
err := db.Find(&configs).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfSecurity", zap.Error(err))
}
var data = make(map[string]interface{})
for _, cfg := range configs {
switch cfg.Name {
case "is_close", "enable_register", "enable_captcha_login", "enable_captcha_register", "enable_captcha_comment", "enable_captcha_find_password", "enable_captcha_upload":
value, _ := strconv.ParseBool(cfg.Value)
data[cfg.Name] = value
}
}
bytes, _ := json.Marshal(data)
json.Unmarshal(bytes, &config)
return
}

@ -6,6 +6,8 @@ import (
"time"
"github.com/alexandrevicenzi/unchained"
"github.com/gofrs/uuid"
"github.com/golang-jwt/jwt"
"go.uber.org/zap"
"gorm.io/gorm"
)
@ -235,3 +237,45 @@ func (m *DBModel) DeleteUser(ids []interface{}) (err error) {
}
return
}
type UserClaims struct {
UserId int64
UUID string
jwt.StandardClaims
}
// CreateUserJWTToken 生成用户JWT Token
func (m *DBModel) CreateUserJWTToken(userId int64) (string, error) {
jwtCfg := m.GetConfigOfJWT()
expireTime := time.Now().Add(time.Duration(jwtCfg.Duration) * time.Second).Unix()
claims := UserClaims{
UserId: userId,
UUID: uuid.Must(uuid.NewV4()).String(),
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime,
Issuer: "moredoc",
IssuedAt: time.Now().Unix(),
},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(jwtCfg.Secret))
return token, err
}
// CheckUserJWTToken 验证用户JWT token
func (m *DBModel) CheckUserJWTToken(token string) (*UserClaims, error) {
jwtCfg := m.GetConfigOfJWT()
tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtCfg.Secret), nil
})
if err != nil {
return nil, err
}
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}

@ -0,0 +1,71 @@
package captcha
import (
"strings"
"github.com/mojocn/base64Captcha"
)
var (
store = base64Captcha.DefaultMemStore
width = 240
height = 60
sourceChinese = strings.Join(strings.Split("欢迎使用由深圳市摩枫网络科技有限公司基于阿帕奇开源协议的魔刀文库系统", ""), ",")
sourceString = "1234567890qwertyuioplkjhgfdsazxcvbnm"
)
const (
CaptchaTypeString = "string" // 字符串
CaptchaTypeDigit = "digit" // 数字
CaptchaTypeMath = "math" // 数学公式
CaptchaTypeChinese = "chinese" // 中文字符
CaptchaTypeAudio = "audio" // 音频
)
// GenerateCaptcha 生成验证码
func GenerateCaptcha(captchaType string) (id, b64s string, err error) {
var driver base64Captcha.Driver
switch captchaType {
case "audio":
driver = &base64Captcha.DriverAudio{
Length: 6,
Language: "zh",
}
case "string":
driver = &base64Captcha.DriverString{
Height: height,
Width: width,
Source: sourceString,
ShowLineOptions: base64Captcha.OptionShowHollowLine | base64Captcha.OptionShowSlimeLine | base64Captcha.OptionShowSineLine,
Length: 6,
}
case "math":
driver = &base64Captcha.DriverMath{
Height: height,
Width: width,
NoiseCount: 0,
}
case "chinese":
driver = &base64Captcha.DriverChinese{
Height: height,
Width: width,
Source: sourceChinese,
Length: 4, // 4个字符
Fonts: []string{"wqy-microhei.ttc"},
}
default:
driver = &base64Captcha.DriverDigit{
Height: height,
Width: width,
DotCount: 80,
MaxSkew: 1,
Length: 6,
}
}
return base64Captcha.NewCaptcha(driver, store).Generate()
}
// VerifyCaptcha 验证验证码
func VerifyCaptcha(id string, captchaValue string) (ok bool) {
return store.Verify(id, captchaValue, true)
}
Loading…
Cancel
Save