登录功能完成

This commit is contained in:
2022-10-04 00:36:01 +08:00
parent 26278707bb
commit 968eab9b06
31 changed files with 1022 additions and 36 deletions

7
pkg/common/errcode.go Normal file
View File

@@ -0,0 +1,7 @@
package common
const (
LoginErrorCode = 2001
NoPermission = 2002
LoginExpired = 2003
)

85
pkg/conf/conf.go Normal file
View File

@@ -0,0 +1,85 @@
package conf
import (
"encoding/json"
"github.com/BurntSushi/toml"
"github.com/mcuadros/go-defaults"
"mc-client-updater-server/pkg/log"
"mc-client-updater-server/pkg/util"
"os"
)
type Config struct {
Common Common `toml:"common"`
Database Database `toml:"database"`
Security Security `toml:"security"`
}
type Common struct {
Host string `toml:"host" default:"0.0.0.0"`
Port uint16 `toml:"port" default:"8080"`
}
type Database struct {
Host string `toml:"host" default:"localhost"`
Port uint16 `toml:"port" default:"5432"`
User string `toml:"user" default:"user"`
Password string `toml:"password" default:"password"`
DB string `toml:"db" default:"mc_client_updater"`
}
type Security struct {
PasswordEncoder string `toml:"password_encoder" default:"argon2id"`
JWTSecret string `toml:"jwt_secret"`
}
var (
Conf *Config
)
// 释放默认配置文件
func releaseConfig(file string, config *Config) {
f, err := util.CreateNestedFile(file)
if err != nil {
log.Logger.Fatalf("创建默认配置文件失败: %s", err.Error())
}
if config.Security.JWTSecret == "" {
config.Security.JWTSecret = util.RandStr(32)
}
encoder := toml.NewEncoder(f)
err = encoder.Encode(config)
if err != nil {
log.Logger.Fatalf("创建默认配置文件失败: %s", err.Error())
}
}
func InitConfig(file string) {
Conf = &Config{}
defaults.SetDefaults(Conf)
if util.NotExists(file) {
// 默认配置文件不存在则创建
if file == "config.toml" {
releaseConfig(file, Conf)
log.Logger.Infof("已创建默认配置文件: %s请修改配置文件内容后重新启动", file)
os.Exit(0)
} else {
log.Logger.Fatalf("配置文件不存在: %s", file)
}
}
// 解析配置文件
_, err := toml.DecodeFile(file, &Conf)
if err != nil {
log.Logger.Fatalf("配置文件解析出错: %s", err.Error())
}
// 配置预检查
jsonConfig, err := json.Marshal(Conf)
if err == nil {
log.Logger.Debugf("配置文件已加载: %s", string(jsonConfig))
}
}

View File

@@ -1 +1,76 @@
package dao
import (
"fmt"
"github.com/sirupsen/logrus"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"mc-client-updater-server/pkg/conf"
"mc-client-updater-server/pkg/dao/entity"
"mc-client-updater-server/pkg/log"
"sync"
"time"
)
type LogrusWriter struct {
log *logrus.Logger
}
func (w *LogrusWriter) Printf(format string, v ...interface{}) {
logStr := fmt.Sprintf(format, v...)
w.log.WithField("method", "GORM").Warn(logStr)
}
func NewLogger() *LogrusWriter {
return &LogrusWriter{log: log.Logger}
}
var (
once sync.Once
db *gorm.DB
)
func DB() *gorm.DB {
once.Do(NewDBConn)
return db
}
func NewDBConn() {
var err error
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
conf.Conf.Database.Host,
conf.Conf.Database.User,
conf.Conf.Database.Password,
conf.Conf.Database.DB,
conf.Conf.Database.Port,
)
// >= 1s SQL慢查询
slowLogger := logger.New(NewLogger(), logger.Config{
SlowThreshold: time.Second * 1,
LogLevel: logger.Warn,
})
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: slowLogger,
})
if err != nil {
log.Logger.Fatal("创建数据库连接失败:", err)
}
migrate()
}
func migrate() {
var err error
err = db.AutoMigrate(&entity.User{})
err = db.AutoMigrate(&entity.Instance{})
err = db.AutoMigrate(&entity.Update{})
err = db.AutoMigrate(&entity.Grant{})
err = db.AutoMigrate(&entity.Token{})
if err != nil {
log.Logger.Fatal("关联数据表失败:", err)
}
}

13
pkg/dao/entity/grant.go Normal file
View File

@@ -0,0 +1,13 @@
package entity
import (
"gorm.io/gorm"
)
// Grant Grant是授权给实例的给予实例访问权限
type Grant struct {
gorm.Model `json:"model"`
Token string `gorm:"unique;not null" json:"token,omitempty"`
TTL int `gorm:"not null;default:0" json:"ttl,omitempty"`
GrantTo uint `gorm:"not null;default:0;comment:instances(id) 授权给实例0表示无指定所有" json:"grant_to,omitempty"`
}

View File

@@ -0,0 +1,11 @@
package entity
import (
"gorm.io/gorm"
)
type Instance struct {
gorm.Model `json:"model"`
Name string `gorm:"unique;not null" json:"name,omitempty"`
UpdateURL string `gorm:"column:update_url;not null;default:'';comment:更新URL未指定使用默认" json:"update_url,omitempty"`
}

11
pkg/dao/entity/token.go Normal file
View File

@@ -0,0 +1,11 @@
package entity
import "gorm.io/gorm"
// Token Token是授权给用户的给予用户登录权限
type Token struct {
gorm.Model `json:"model"`
Token string `gorm:"unique;not null" json:"token,omitempty"`
GrantTo string `gorm:"index;not null;default:''" json:"grant_to,omitempty"`
TTL int `gorm:"not null;default:0" json:"ttl,omitempty"`
}

10
pkg/dao/entity/update.go Normal file
View File

@@ -0,0 +1,10 @@
package entity
import "gorm.io/gorm"
type Update struct {
gorm.Model `json:"model"`
HashID string `gorm:"index;not null" json:"hash_id,omitempty"`
Comment string `gorm:"not null;default:'';comment:更新内容或注释" json:"comment,omitempty"`
Changes string `gorm:"not null;comment:更改的文件列表逗号分隔引用files(hash_id)" json:"changes,omitempty"`
}

View File

@@ -3,9 +3,8 @@ package entity
import "gorm.io/gorm"
type User struct {
gorm.Model
Username string `gorm:"unique;not null"`
Password string `gorm:"not null"`
Instance string `gorm:"not null;default:''"`
Role string `gorm:"not null;default:0"`
gorm.Model `json:"model"`
Username string `gorm:"unique;not null" json:"username,omitempty"`
Password string `gorm:"not null" json:"password,omitempty"`
Roles string `gorm:"not null;default:''" json:"roles,omitempty"`
}

18
pkg/dto/user.go Normal file
View File

@@ -0,0 +1,18 @@
package dto
import (
"mc-client-updater-server/pkg/dao/entity"
"strings"
)
type User struct {
Username string
Roles []string
}
func UserEntity2DTO(user *entity.User) *User {
return &User{
Username: user.Username,
Roles: strings.Split(user.Roles, ","),
}
}

28
pkg/log/logger.go Normal file
View File

@@ -0,0 +1,28 @@
package log
import (
nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
)
var Logger *logrus.Logger
func InitLogger(debug bool) {
log := logrus.New()
log.SetFormatter(&nested.Formatter{
FieldsOrder: []string{"method", "url", "statusCode", "spendTime"},
HideKeys: true,
NoFieldsColors: true,
TimestampFormat: "2006-01-02 15:04:05.000",
})
var lvl logrus.Level
switch debug {
case true:
lvl = logrus.DebugLevel
case false:
lvl = logrus.InfoLevel
}
log.SetLevel(lvl)
Logger = log
}

10
pkg/param/authorize.go Normal file
View File

@@ -0,0 +1,10 @@
package param
type AuthorizeQueryParam struct {
ClientId uint `form:"client_id" binding:"required"`
ResponseType string `form:"response_type" binding:"required"`
State string `form:"state" binding:"required"`
Scope string `form:"scope" binding:"required"`
CodeChallenge string `form:"code_challenge" binding:"required"`
CodeChallengeMethod string `form:"code_challenge_method" binding:"required"`
}

6
pkg/param/login.go Normal file
View File

@@ -0,0 +1,6 @@
package param
type LoginParam struct {
Username string `json:"username"`
Password string `json:"password"`
}

16
pkg/password/argon2id.go Normal file
View File

@@ -0,0 +1,16 @@
package password
import "github.com/alexedwards/argon2id"
type Argon2id struct {
}
func (a *Argon2id) Create(password string) (hash string, err error) {
hash, err = argon2id.CreateHash(password, argon2id.DefaultParams)
return
}
func (a *Argon2id) Verify(password, hash string) (match bool, err error) {
match, err = argon2id.ComparePasswordAndHash(password, hash)
return
}

29
pkg/password/password.go Normal file
View File

@@ -0,0 +1,29 @@
package password
import (
"mc-client-updater-server/pkg/conf"
"mc-client-updater-server/pkg/log"
"sync"
)
var (
once sync.Once
encoder Encoder
)
func Password() Encoder {
once.Do(func() {
switch conf.Conf.Security.PasswordEncoder {
case "argon2id":
encoder = &Argon2id{}
default:
log.Logger.Fatal("非法的PasswordEncoder类型", conf.Conf.Security.PasswordEncoder)
}
})
return encoder
}
type Encoder interface {
Create(string) (string, error)
Verify(password, hash string) (bool, error)
}

74
pkg/result/base.go Normal file
View File

@@ -0,0 +1,74 @@
package result
import (
"github.com/gin-gonic/gin"
"mc-client-updater-server/pkg/common"
"net/http"
)
type Result struct {
ctx *gin.Context
}
type Root struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
func NewResult(c *gin.Context) *Result {
return &Result{ctx: c}
}
func (r *Result) Success(data interface{}) {
if data == nil {
data = gin.H{}
}
res := Root{
Code: http.StatusOK,
Msg: "",
Data: data,
}
r.ctx.JSON(http.StatusOK, res)
r.ctx.Next()
}
func (r *Result) Fail(code int, msg string) {
res := Root{
Code: code,
Msg: msg,
Data: gin.H{},
}
r.ctx.JSON(http.StatusOK, res)
r.ctx.Abort()
}
//func (r *Result) Redirect302(location string) {
// r.ctx.Header("Location", location)
// r.ctx.Status(http.StatusFound)
// r.ctx.Next()
//}
func (r *Result) InternalServerError(msg string) {
r.Fail(http.StatusInternalServerError, msg)
}
func (r *Result) BadRequest() {
r.Fail(http.StatusBadRequest, "请求参数错误")
}
func (r *Result) LoginError() {
r.Fail(common.LoginErrorCode, "账号或密码错误")
}
func (r *Result) Unauthorized() {
r.Fail(http.StatusUnauthorized, "未登录")
}
func (r *Result) NoPermission() {
r.Fail(common.NoPermission, "权限不足")
}
func (r *Result) LoginExpired() {
r.Fail(common.LoginExpired, "登录过期")
}

25
pkg/token/jwt.go Normal file
View File

@@ -0,0 +1,25 @@
package token
import (
"github.com/golang-jwt/jwt/v4"
"mc-client-updater-server/pkg/conf"
"mc-client-updater-server/pkg/log"
"time"
)
func NewToken(aud string) string {
auds := make([]string, 1)
auds = append(auds, aud)
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"aud": auds,
"iat": now,
"nbf": now,
})
tokenStr, err := token.SignedString([]byte(conf.Conf.Security.JWTSecret))
if err != nil {
log.Logger.Error("生成Token失败", err)
return ""
}
return tokenStr
}

30
pkg/util/file.go Normal file
View File

@@ -0,0 +1,30 @@
package util
import (
"os"
"path/filepath"
)
func Exists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func NotExists(name string) bool {
return !Exists(name)
}
func CreateNestedFile(path string) (*os.File, error) {
basePath := filepath.Dir(path)
if !Exists(basePath) {
err := os.MkdirAll(basePath, 0700)
if err != nil {
return nil, err
}
}
return os.Create(path)
}

36
pkg/util/rand.go Normal file
View File

@@ -0,0 +1,36 @@
package util
import (
"math/rand"
"time"
"unsafe"
)
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
var src = rand.NewSource(time.Now().UnixNano())
const (
// 6 bits to represent a letter index
letterIdBits = 6
// All 1-bits as many as letterIdBits
letterIdMask = 1<<letterIdBits - 1
letterIdMax = 63 / letterIdBits
)
func RandStr(n int) string {
b := make([]byte, n)
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdMax
}
if idx := int(cache & letterIdMask); idx < len(letters) {
b[i] = letters[idx]
i--
}
cache >>= letterIdBits
remain--
}
return *(*string)(unsafe.Pointer(&b))
}

21
pkg/util/slice.go Normal file
View File

@@ -0,0 +1,21 @@
package util
func InStringSlice(sl []string, ele string) bool {
slMap := convertStrSlice2Map(sl)
return inMap(slMap, ele)
}
// ConvertStrSlice2Map 将字符串 slice 转为 map[string]struct{}。
func convertStrSlice2Map(sl []string) map[string]struct{} {
set := make(map[string]struct{}, len(sl))
for _, v := range sl {
set[v] = struct{}{}
}
return set
}
// InMap 判断字符串是否在 map 中。
func inMap(m map[string]struct{}, s string) bool {
_, ok := m[s]
return ok
}

5
pkg/util/token.go Normal file
View File

@@ -0,0 +1,5 @@
package util
func GenSessionID() string {
return RandStr(32)
}