🎨 complete SMS login function

This commit is contained in:
landaiqing
2024-08-14 00:11:32 +08:00
parent 48c5aeb0f4
commit 368adadf52
38 changed files with 687 additions and 232 deletions

51
.air.toml Normal file
View File

@@ -0,0 +1,51 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "tmp\\main.exe"
cmd = "go build -o ./tmp/main.exe ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", ".idea", ".git"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@
*.so
*.dylib
.air.toml
test
# Test binary, built with `go test -c`
*.test

View File

@@ -1,14 +1,14 @@
package api
import (
"schisandra-cloud-album/api/auth_api"
"schisandra-cloud-album/api/captcha_api"
"schisandra-cloud-album/api/sms_api"
"schisandra-cloud-album/api/user_api"
)
// Apis 统一导出的api
type Apis struct {
AuthApi auth_api.AuthAPI
UserApi user_api.UserAPI
CaptchaApi captcha_api.CaptchaAPI
SmsApi sms_api.SmsAPI
}

View File

@@ -1,3 +0,0 @@
package auth_api
type AuthAPI struct{}

View File

@@ -1,159 +0,0 @@
package auth_api
import (
ginI18n "github.com/gin-contrib/i18n"
"github.com/gin-gonic/gin"
"reflect"
"schisandra-cloud-album/common/result"
"schisandra-cloud-album/model"
"schisandra-cloud-album/service"
"schisandra-cloud-album/utils"
)
var authService = service.Service.AuthService
// GetUserList
// @Summary 获取所有用户列表
// @Tags 鉴权模块
// @Success 200 {string} json
// @Router /api/auth/user/List [get]
func (AuthAPI) GetUserList(c *gin.Context) {
userList := authService.GetUserList()
result.OkWithData(userList, c)
}
// QueryUserByUsername
// @Summary 根据用户名查询用户
// @Tags 鉴权模块
// @Param username query string true "用户名"
// @Success 200 {string} json
// @Router /api/auth/user/query_by_username [get]
func (AuthAPI) QueryUserByUsername(c *gin.Context) {
username := c.Query("username")
user := authService.QueryUserByUsername(username)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "NotFoundUser"), c)
return
}
result.OkWithData(user, c)
}
// QueryUserByUuid
// @Summary 根据uuid查询用户
// @Tags 鉴权模块
// @Param uuid query string true "用户uuid"
// @Success 200 {string} json
// @Router /api/auth/user/query_by_uuid [get]
func (AuthAPI) QueryUserByUuid(c *gin.Context) {
uuid := c.Query("uuid")
user := authService.QueryUserByUuid(uuid)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "NotFoundUser"), c)
return
}
result.OkWithData(user, c)
}
// DeleteUser 删除用户
// @Summary 删除用户
// @Tags 鉴权模块
// @Param uuid query string true "用户uuid"
// @Success 200 {string} json
// @Router /api/auth/user/delete [delete]
func (AuthAPI) DeleteUser(c *gin.Context) {
uuid := c.Query("uuid")
err := authService.DeleteUser(uuid)
if err != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "DeletedFailed"), c)
return
}
result.OkWithMessage(ginI18n.MustGetMessage(c, "DeletedSuccess"), c)
}
// QueryUserByPhone 根据手机号查询用户
// @Summary 根据手机号查询用户
// @Tags 鉴权模块
// @Param phone query string true "手机号"
// @Success 200 {string} json
// @Router /api/auth/user/query_by_phone [get]
func (AuthAPI) QueryUserByPhone(c *gin.Context) {
phone := c.Query("phone")
user := authService.QueryUserByPhone(phone)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "NotFoundUser"), c)
return
}
result.OkWithData(user, c)
}
// AccountLogin 账号登录
// @Summary 账号登录
// @Tags 鉴权模块
// @Param account query string true "账号"
// @Param password query string true "密码"
// @Success 200 {string} json
// @Router /api/auth/user/login [post]
func (AuthAPI) AccountLogin(c *gin.Context) {
account := c.PostForm("account")
password := c.PostForm("password")
isPhone := utils.IsPhone(account)
if isPhone {
user := authService.QueryUserByPhone(account)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotRegister"), c)
} else {
verify := utils.Verify(password, *user.Password)
if verify {
result.OkWithData(user, c)
} else {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PasswordError"), c)
}
}
}
isEmail := utils.IsEmail(account)
if isEmail {
user := authService.QueryUserByEmail(account)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "EmailNotRegister"), c)
} else {
verify := utils.Verify(password, *user.Password)
if verify {
result.OkWithData(user, c)
} else {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PasswordError"), c)
}
}
}
isUsername := utils.IsUsername(account)
if isUsername {
user := authService.QueryUserByUsername(account)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "UsernameNotRegister"), c)
} else {
verify := utils.Verify(password, *user.Password)
if verify {
result.OkWithData(user, c)
} else {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PasswordError"), c)
}
}
}
}
// Register 用户注册
// @Summary 用户注册
// @Tags 鉴权模块
// @Param user body model.ScaAuthUser true "用户信息"
// @Success 200 {string} json
// @Router /api/auth/user/register [post]
func (AuthAPI) Register(c *gin.Context) {
var user model.ScaAuthUser
_ = c.ShouldBindJSON(&user)
err := authService.AddUser(user)
if err != nil {
result.FailWithMessage("用户注册失败!", c)
return
}
result.OkWithMessage("用户注册成功!", c)
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/wenlng/go-captcha/v2/rotate"
"github.com/wenlng/go-captcha/v2/slide"
"log"
"schisandra-cloud-album/api/captcha_api/model"
"schisandra-cloud-album/api/captcha_api/dto"
"schisandra-cloud-album/common/redis"
"schisandra-cloud-album/common/result"
"schisandra-cloud-album/global"
@@ -44,7 +44,7 @@ func (CaptchaAPI) GenerateRotateCaptcha(c *gin.Context) {
return
}
key := helper.StringToMD5(string(dotsByte))
err = redis.Set(key, dotsByte, time.Minute).Err()
err = redis.Set("user:login:client:"+key, dotsByte, time.Minute).Err()
if err != nil {
result.FailWithNull(c)
return
@@ -66,7 +66,7 @@ func (CaptchaAPI) GenerateRotateCaptcha(c *gin.Context) {
// @Success 200 {string} json
// @Router /api/captcha/rotate/check [post]
func (CaptchaAPI) CheckRotateData(c *gin.Context) {
rotateRequest := model.RotateCaptchaRequest{}
rotateRequest := dto.RotateCaptchaRequest{}
err := c.ShouldBindJSON(&rotateRequest)
angle := rotateRequest.Angle
key := rotateRequest.Key
@@ -74,7 +74,7 @@ func (CaptchaAPI) CheckRotateData(c *gin.Context) {
result.FailWithNull(c)
return
}
cacheDataByte, err := redis.Get(key).Bytes()
cacheDataByte, err := redis.Get("user:login:client:" + key).Bytes()
if len(cacheDataByte) == 0 || err != nil {
result.FailWithCodeAndMessage(1011, ginI18n.MustGetMessage(c, "CaptchaExpired"), c)
return
@@ -126,7 +126,7 @@ func (CaptchaAPI) GenerateBasicTextCaptcha(c *gin.Context) {
return
}
key := helper.StringToMD5(string(dotsByte))
err = redis.Set(key, dotsByte, time.Minute).Err()
err = redis.Set("user:login:client:"+key, dotsByte, time.Minute).Err()
if err != nil {
result.FailWithNull(c)
return
@@ -154,7 +154,7 @@ func (CaptchaAPI) CheckClickData(c *gin.Context) {
result.FailWithNull(c)
return
}
cacheDataByte, err := redis.Get(key).Bytes()
cacheDataByte, err := redis.Get("user:login:client:" + key).Bytes()
if len(cacheDataByte) == 0 || err != nil {
result.FailWithNull(c)
return

View File

@@ -1,4 +1,4 @@
package model
package dto
type RotateCaptchaRequest struct {
Angle int `json:"angle"`

3
api/role_api/role.go Normal file
View File

@@ -0,0 +1,3 @@
package role_api
type RoleAPI struct{}

1
api/role_api/role_api.go Normal file
View File

@@ -0,0 +1 @@
package role_api

View File

@@ -7,9 +7,11 @@ import (
"github.com/pkg6/go-sms/gateways"
"github.com/pkg6/go-sms/gateways/aliyun"
"github.com/pkg6/go-sms/gateways/smsbao"
"schisandra-cloud-album/common/redis"
"schisandra-cloud-album/common/result"
"schisandra-cloud-album/global"
"schisandra-cloud-album/utils"
"time"
)
// SendMessageByAli 发送短信验证码
@@ -46,6 +48,8 @@ func (SmsAPI) SendMessageByAli(c *gin.Context) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c)
return
}
result.OkWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendSuccess"), c)
}
// SendMessageBySmsBao 短信宝发送短信验证码
@@ -78,3 +82,32 @@ func (SmsAPI) SendMessageBySmsBao(c *gin.Context) {
}
result.OkWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendSuccess"), c)
}
// SendMessageTest 发送测试短信验证码
// @Summary 发送测试短信验证码
// @Description 发送测试短信验证码
// @Tags 短信验证码
// @Produce json
// @Param phone query string true "手机号"
// @Router /api/sms/test/send [get]
func (SmsAPI) SendMessageTest(c *gin.Context) {
phone := c.Query("phone")
if phone == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c)
return
}
isPhone := utils.IsPhone(phone)
if !isPhone {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneError"), c)
return
}
code := utils.GenValidateCode(6)
err := redis.Set("user:login:sms:"+phone, code, time.Minute).Err()
if err != nil {
global.LOG.Error(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c)
return
}
result.OkWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendSuccess"), c)
}

View File

@@ -0,0 +1,30 @@
package dto
import "encoding/json"
// RefreshTokenRequest 刷新token请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token"`
}
// PhoneLoginRequest 手机号登录请求
type PhoneLoginRequest struct {
Phone string `json:"phone"`
Captcha string `json:"captcha"`
}
// ResponseData 返回数据
type ResponseData struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
UID *string `json:"uid"`
}
func (res ResponseData) MarshalBinary() ([]byte, error) {
return json.Marshal(res)
}
func (res ResponseData) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, &res)
}

3
api/user_api/user.go Normal file
View File

@@ -0,0 +1,3 @@
package user_api
type UserAPI struct{}

307
api/user_api/user_api.go Normal file
View File

@@ -0,0 +1,307 @@
package user_api
import (
ginI18n "github.com/gin-contrib/i18n"
"github.com/gin-gonic/gin"
"github.com/yitter/idgenerator-go/idgen"
"reflect"
"schisandra-cloud-album/api/user_api/dto"
"schisandra-cloud-album/common/enum"
"schisandra-cloud-album/common/redis"
"schisandra-cloud-album/common/result"
"schisandra-cloud-album/global"
"schisandra-cloud-album/model"
"schisandra-cloud-album/service"
"schisandra-cloud-album/utils"
"strconv"
"time"
)
var userService = service.Service.UserService
var userRoleService = service.Service.UserRoleService
// GetUserList
// @Summary 获取所有用户列表
// @Tags 鉴权模块
// @Success 200 {string} json
// @Router /api/auth/user/List [get]
func (UserAPI) GetUserList(c *gin.Context) {
userList := userService.GetUserList()
result.OkWithData(userList, c)
}
// QueryUserByUsername
// @Summary 根据用户名查询用户
// @Tags 鉴权模块
// @Param username query string true "用户名"
// @Success 200 {string} json
// @Router /api/auth/user/query_by_username [get]
func (UserAPI) QueryUserByUsername(c *gin.Context) {
username := c.Query("username")
user := userService.QueryUserByUsername(username)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "NotFoundUser"), c)
return
}
result.OkWithData(user, c)
}
// QueryUserByUuid
// @Summary 根据uuid查询用户
// @Tags 鉴权模块
// @Param uuid query string true "用户uuid"
// @Success 200 {string} json
// @Router /api/auth/user/query_by_uuid [get]
func (UserAPI) QueryUserByUuid(c *gin.Context) {
uuid := c.Query("uuid")
user := userService.QueryUserByUuid(uuid)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "NotFoundUser"), c)
return
}
result.OkWithData(user, c)
}
// DeleteUser 删除用户
// @Summary 删除用户
// @Tags 鉴权模块
// @Param uuid query string true "用户uuid"
// @Success 200 {string} json
// @Router /api/auth/user/delete [delete]
func (UserAPI) DeleteUser(c *gin.Context) {
uuid := c.Query("uuid")
err := userService.DeleteUser(uuid)
if err != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "DeletedFailed"), c)
return
}
result.OkWithMessage(ginI18n.MustGetMessage(c, "DeletedSuccess"), c)
}
// QueryUserByPhone 根据手机号查询用户
// @Summary 根据手机号查询用户
// @Tags 鉴权模块
// @Param phone query string true "手机号"
// @Success 200 {string} json
// @Router /api/auth/user/query_by_phone [get]
func (UserAPI) QueryUserByPhone(c *gin.Context) {
phone := c.Query("phone")
user := userService.QueryUserByPhone(phone)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "NotFoundUser"), c)
return
}
result.OkWithData(user, c)
}
// AccountLogin 账号登录
// @Summary 账号登录
// @Tags 鉴权模块
// @Param account query string true "账号"
// @Param password query string true "密码"
// @Success 200 {string} json
// @Router /api/user/login [post]
func (UserAPI) AccountLogin(c *gin.Context) {
account := c.PostForm("account")
password := c.PostForm("password")
isPhone := utils.IsPhone(account)
if isPhone {
user := userService.QueryUserByPhone(account)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotRegister"), c)
return
} else {
verify := utils.Verify(password, *user.Password)
if verify {
result.OkWithData(user, c)
return
} else {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PasswordError"), c)
return
}
}
}
isEmail := utils.IsEmail(account)
if isEmail {
user := userService.QueryUserByEmail(account)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "EmailNotRegister"), c)
return
} else {
verify := utils.Verify(password, *user.Password)
if verify {
result.OkWithData(user, c)
return
} else {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PasswordError"), c)
return
}
}
}
isUsername := utils.IsUsername(account)
if isUsername {
user := userService.QueryUserByUsername(account)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "UsernameNotRegister"), c)
return
} else {
verify := utils.Verify(password, *user.Password)
if verify {
result.OkWithData(user, c)
return
} else {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PasswordError"), c)
return
}
}
}
}
// PhoneLogin 手机号登录/注册
// @Summary 手机号登录/注册
// @Tags 鉴权模块
// @Param phone query string true "手机号"
// @Param captcha query string true "验证码"
// @Success 200 {string} json
// @Router /api/user/phone_login [post]
func (UserAPI) PhoneLogin(c *gin.Context) {
request := dto.PhoneLoginRequest{}
err := c.ShouldBindJSON(&request)
if err != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
phone := request.Phone
captcha := request.Captcha
if phone == "" || captcha == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneAndCaptchaNotEmpty"), c)
return
}
isPhone := utils.IsPhone(phone)
if !isPhone {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c)
return
}
user := userService.QueryUserByPhone(phone)
if reflect.DeepEqual(user, model.ScaAuthUser{}) {
// 未注册
code := redis.Get("user:login:sms:" + phone)
if code == nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaExpired"), c)
return
} else {
uid := idgen.NextId()
uidStr := strconv.FormatInt(uid, 10)
createUser := model.ScaAuthUser{
UID: &uidStr,
Phone: &phone,
}
addUser, err := userService.AddUser(createUser)
if err != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "RegisterUserError"), c)
return
}
userRole := model.ScaAuthUserRole{
UserID: addUser.ID,
RoleID: enum.User,
}
e := userRoleService.AddUserRole(userRole)
if e != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "LoginFailed"), c)
return
}
ids, err := userRoleService.GetUserRoleIdsByUserId(addUser.ID)
if err != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "LoginFailed"), c)
return
}
accessToken, refreshToken, expiresAt := utils.GenerateAccessTokenAndRefreshToken(utils.JWTPayload{UserID: addUser.UID, RoleID: ids})
data := dto.ResponseData{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
UID: addUser.UID,
}
fail := redis.Set("user:login:token:"+*addUser.UID, data, time.Hour*24*30).Err()
if fail != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "LoginFailed"), c)
return
}
result.OkWithData(data, c)
return
}
} else {
code := redis.Get("user:login:sms:" + phone)
if code == nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaExpired"), c)
return
} else {
if captcha != code.Val() {
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaError"), c)
return
} else {
ids, err := userRoleService.GetUserRoleIdsByUserId(user.ID)
if err != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "LoginFailed"), c)
return
}
accessToken, refreshToken, expiresAt := utils.GenerateAccessTokenAndRefreshToken(utils.JWTPayload{UserID: user.UID, RoleID: ids})
data := dto.ResponseData{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
UID: user.UID,
}
fail := redis.Set("user:login:token:"+*user.UID, data, time.Hour*24*30).Err()
if fail != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "LoginFailed"), c)
return
}
result.OkWithData(data, c)
return
}
}
}
}
// RefreshHandler 刷新token
// @Summary 刷新token
// @Tags 鉴权模块
// @Param refresh_token query string true "刷新token"
// @Success 200 {string} json
// @Router /api/auth/token/refresh [post]
func (UserAPI) RefreshHandler(c *gin.Context) {
refreshToken := c.Query("refresh_token")
if refreshToken == "" {
result.FailWithMessage("refresh_token不能为空", c)
return
}
parseRefreshToken, isUpd, err := utils.ParseToken(refreshToken)
if err != nil {
global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "LoginExpired"), c)
return
}
if isUpd {
accessTokenString, refreshTokenString, expiresAt := utils.GenerateAccessTokenAndRefreshToken(utils.JWTPayload{UserID: parseRefreshToken.UserID, RoleID: parseRefreshToken.RoleID})
data := dto.ResponseData{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
ExpiresAt: expiresAt,
UID: parseRefreshToken.UserID,
}
fail := redis.Set("user:login:token:"+*parseRefreshToken.UserID, data, time.Hour*24*30).Err()
if fail != nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "LoginExpired"), c)
return
}
result.OkWithData(data, c)
return
}
}

7
common/enum/role.go Normal file
View File

@@ -0,0 +1,7 @@
package enum
var (
SuperAdmin int64 = 1
Admin int64 = 2
User int64 = 3
)

View File

@@ -1,10 +1,8 @@
package config
type JWT struct {
Secret string `yaml:"secret"`
Expiration string `yaml:"expiration"`
RefreshExpiration string `yaml:"refresh-expiration"`
RefreshTokenKey string `yaml:"refresh-token-key"`
HeaderKey string `yaml:"header-key"`
HeaderPrefix string `yaml:"header-prefix"`
Secret string `yaml:"secret"`
HeaderKey string `yaml:"header-key"`
HeaderPrefix string `yaml:"header-prefix"`
Issuer string `yaml:"issuer"`
}

10
core/idgenerator.go Normal file
View File

@@ -0,0 +1,10 @@
package core
import "github.com/yitter/idgenerator-go/idgen"
func InitIDGenerator() {
var options = idgen.NewIdGeneratorOptions(1)
options.WorkerIdBitLength = 6 // 默认值6限定 WorkerId 最大值为2^6-1即默认最多支持64个节点。
options.SeqBitLength = 6 // 默认值6限制每毫秒生成的ID个数。若生成速度超过5万个/秒,建议加大 SeqBitLength 到 10。
idgen.SetIdGenerator(options)
}

View File

@@ -176,7 +176,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.ScaAuthUser"
"$ref": "#/definitions/dto.ScaAuthUser"
}
}
],
@@ -429,7 +429,7 @@ const docTemplate = `{
}
},
"definitions": {
"model.ScaAuthUser": {
"dto.ScaAuthUser": {
"type": "object",
"properties": {
"avatar": {

4
go.mod
View File

@@ -28,7 +28,7 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.12.0 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -41,6 +41,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
@@ -60,6 +61,7 @@ require (
github.com/pkg6/go-sms v0.1.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yitter/idgenerator-go v1.3.3 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.19.0 // indirect

6
go.sum
View File

@@ -15,6 +15,8 @@ github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
@@ -53,6 +55,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -151,6 +155,8 @@ github.com/wenlng/go-captcha-assets v1.0.1 h1:AdjRFMKmadPRWRTv0XEYfjDvcaayZ2yExI
github.com/wenlng/go-captcha-assets v1.0.1/go.mod h1:yQqc7rRbxgLCg+tWtVp+7Y317D1wIZDan/yIwt8wSac=
github.com/wenlng/go-captcha/v2 v2.0.0 h1:7Z4Zy09SIHgvj9e8ZxP4VhYOwg7IHt8kGlVrE5jP5Z8=
github.com/wenlng/go-captcha/v2 v2.0.0/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
github.com/yitter/idgenerator-go v1.3.3 h1:i6rzmpbCL0vlmr/tuW5+lSQzNuDG9vYBjIYRvnRcHE8=
github.com/yitter/idgenerator-go v1.3.3/go.mod h1:VVjbqFjGUsIkaXVkXEdmx1LiXUL3K1NvyxWPJBPbBpE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=

View File

@@ -33,3 +33,11 @@ CaptchaSendSuccess = "captcha send successfully!"
CaptchaTooOften = "captcha too often!"
PhoneNotEmpty = "phone number can not be empty!"
EmailNotEmpty = "email can not be empty!"
AuthVerifyFailed = "auth verify failed!, please try again!"
AuthVerifySuccess = "auth verify successfully!"
AuthVerifyExpired = "auth verify expired!"
ParamsError = "params error!"
PhoneAndCaptchaNotEmpty = "phone number and captcha can not be empty!"
PhoneErrorFormat = "phone number error format!"
RegisterUserError = "register user error!"
LoginExpired = "login expired!, please try again!"

View File

@@ -33,3 +33,11 @@ CaptchaSendSuccess = "验证码发送成功!"
CaptchaTooOften = "验证码发送过于频繁!"
PhoneNotEmpty = "手机号不能为空!"
EmailNotEmpty = "邮箱不能为空!"
AuthVerifyFailed = "认证失败!,请重新登录!"
AuthVerifySuccess = "认证成功!"
AuthVerifyExpired = "认证已过期!,请重新登录!"
ParamsError = "参数错误!"
PhoneAndCaptchaNotEmpty = "手机号和验证码不能为空!"
PhoneErrorFormat = "手机号格式错误!"
RegisterUserError = "注册用户错误!"
LoginExpired = "登录已过期!,请重新登录!"

11
main.go
View File

@@ -9,11 +9,12 @@ import (
func main() {
// 初始化配置
core.InitConfig() // 读取配置文件
core.InitLogger() // 初始化日志
core.InitGorm() // 初始化数据库
core.InitRedis() // 初始化redis
core.InitCaptcha() // 初始化验证码
core.InitConfig() // 读取配置文件
core.InitLogger() // 初始化日志
core.InitGorm() // 初始化数据库
core.InitRedis() // 初始化redis
core.InitCaptcha() // 初始化验证码
core.InitIDGenerator() // 初始化ID生成器
// 命令行参数绑定
option := cmd.Parse()
if cmd.IsStopWeb(&option) {

38
middleware/jwt.go Normal file
View File

@@ -0,0 +1,38 @@
package middleware
import (
ginI18n "github.com/gin-contrib/i18n"
"github.com/gin-gonic/gin"
"schisandra-cloud-album/common/result"
"schisandra-cloud-album/global"
"schisandra-cloud-album/utils"
"strings"
)
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 默认双Token放在请求头Authorization的Bearer中并以空格隔开
authHeader := c.GetHeader(global.CONFIG.JWT.HeaderKey)
if authHeader == "" {
c.Abort()
result.FailWithMessage(ginI18n.MustGetMessage(c, "AuthVerifyFailed"), c)
return
}
headerPrefix := global.CONFIG.JWT.HeaderPrefix
accessToken := strings.TrimPrefix(authHeader, headerPrefix+" ")
if accessToken == "undefined" || accessToken == "" {
c.Abort()
result.FailWithMessage(ginI18n.MustGetMessage(c, "AuthVerifyFailed"), c)
return
}
parseToken, isUpd, err := utils.ParseToken(accessToken)
if err != nil || !isUpd {
c.Abort()
result.FailWithCodeAndMessage(401, ginI18n.MustGetMessage(c, "AuthVerifyExpired"), c)
return
}
c.Set("userId", parseToken.UserID)
c.Next()
}
}

View File

@@ -1,6 +1,7 @@
package model
import (
"encoding/json"
"time"
)
@@ -9,7 +10,7 @@ const TableNameScaAuthUser = "sca_auth_user"
// ScaAuthUser 用户表
type ScaAuthUser struct {
ID int64 `gorm:"column:id;type:bigint(255);primaryKey;autoIncrement:true;comment:自增ID" json:"-"` // 自增ID
UUID *string `gorm:"column:uuid;type:varchar(255);comment:唯一ID" json:"uuid"` // 唯一ID
UID *string `gorm:"column:uid;type:varchar(255);comment:唯一ID" json:"uid"` // 唯一ID
Username *string `gorm:"column:username;type:varchar(32);comment:用户名" json:"username"` // 用户名
Nickname *string `gorm:"column:nickname;type:varchar(32);comment:昵称" json:"nickname"` // 昵称
Email *string `gorm:"column:email;type:varchar(32);comment:邮箱" json:"email"` // 邮箱
@@ -17,14 +18,14 @@ type ScaAuthUser struct {
Password *string `gorm:"column:password;type:varchar(64);comment:密码" json:"-"` // 密码
Gender *string `gorm:"column:gender;type:varchar(32);comment:性别" json:"gender"` // 性别
Avatar *string `gorm:"column:avatar;type:varchar(255);comment:头像" json:"avatar"` // 头像
Status *int64 `gorm:"column:status;type:tinyint(4);comment:状态 0 正常 1 封禁" json:"status"` // 状态 0 正常 1 封禁
Status *int64 `gorm:"column:status;type:tinyint(4);default:0;comment:状态 0 正常 1 封禁" json:"status"` // 状态 0 正常 1 封禁
Introduce *string `gorm:"column:introduce;type:varchar(255);comment:介绍" json:"introduce"` // 介绍
ExtJSON *string `gorm:"column:ext_json;type:varchar(255);comment:额外字段" json:"-"` // 额外字段
CreatedBy *string `gorm:"column:created_by;type:varchar(32);comment:创建人" json:"created_by"` // 创建人
CreatedTime *time.Time `gorm:"column:created_time;type:datetime;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_time"` // 创建时间
UpdateBy *string `gorm:"column:update_by;type:varchar(32);comment:更新人" json:"update_by"` // 更新人
UpdateTime *time.Time `gorm:"column:update_time;type:datetime;default:CURRENT_TIMESTAMP;comment:更新时间" json:"update_time"` // 更新时间
Deleted *int64 `gorm:"column:deleted;type:int(11);comment:是否删除 0 未删除 1 已删除" json:"-"` // 是否删除 0 未删除 1 已删除
Deleted *int64 `gorm:"column:deleted;type:int(11);default:0;comment:是否删除 0 未删除 1 已删除" json:"-"` // 是否删除 0 未删除 1 已删除
Blog *string `gorm:"column:blog;type:varchar(255);comment:博客" json:"blog"` // 博客
Location *string `gorm:"column:location;type:varchar(255);comment:地址" json:"location"` // 地址
Company *string `gorm:"column:company;type:varchar(255);comment:公司" json:"company"` // 公司
@@ -34,3 +35,11 @@ type ScaAuthUser struct {
func (*ScaAuthUser) TableName() string {
return TableNameScaAuthUser
}
func (user *ScaAuthUser) MarshalBinary() ([]byte, error) {
return json.Marshal(user)
}
func (user *ScaAuthUser) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, user)
}

View File

@@ -1,19 +0,0 @@
package modules
import (
"github.com/gin-gonic/gin"
"schisandra-cloud-album/api"
)
var authApi = api.Api.AuthApi
func AuthRouter(router *gin.RouterGroup) {
group := router.Group("auth")
group.GET("/user/List", authApi.GetUserList)
group.GET("/user/query_by_username", authApi.QueryUserByUsername)
group.GET("/user/query_by_uuid", authApi.QueryUserByUuid)
group.DELETE("/user/delete", authApi.DeleteUser)
group.GET("/user/query_by_phone", authApi.QueryUserByPhone)
group.POST("/user/login", authApi.AccountLogin)
group.POST("/user/register", authApi.Register)
}

View File

@@ -11,4 +11,5 @@ func SmsRouter(router *gin.RouterGroup) {
group := router.Group("/sms")
group.GET("/ali/send", smsApi.SendMessageByAli)
group.GET("/smsbao/send", smsApi.SendMessageBySmsBao)
group.GET("/test/send", smsApi.SendMessageTest)
}

View File

@@ -0,0 +1,25 @@
package modules
import (
"github.com/gin-gonic/gin"
"schisandra-cloud-album/api"
"schisandra-cloud-album/middleware"
)
var userApi = api.Api.UserApi
// UserRouter 用户相关路由 有auth接口组需要token验证,没有auth接口组不需要token验证
func UserRouter(router *gin.RouterGroup) {
userGroup := router.Group("user")
{
userGroup.POST("/login", userApi.AccountLogin)
userGroup.POST("/phone_login", userApi.PhoneLogin)
}
authGroup := router.Group("auth").Use(middleware.JWTAuthMiddleware())
{
authGroup.GET("/user/List", userApi.GetUserList)
authGroup.GET("/user/query_by_uuid", userApi.QueryUserByUuid)
authGroup.POST("/token/refresh", userApi.RefreshHandler)
}
}

View File

@@ -23,7 +23,7 @@ func InitRouter() *gin.Engine {
publicGroup.Use(middleware.I18n())
modules.SwaggerRouter(router) // 注册swagger路由
modules.AuthRouter(publicGroup) // 注册鉴权路由
modules.UserRouter(publicGroup) // 注册鉴权路由
modules.CaptchaRouter(publicGroup) // 注册验证码路由
modules.SmsRouter(publicGroup) // 注册短信验证码路由
return router

View File

@@ -1,3 +0,0 @@
package auth_service
type AuthService struct{}

View File

@@ -0,0 +1,3 @@
package role_service
type RoleService struct{}

View File

@@ -0,0 +1,15 @@
package role_service
import (
"schisandra-cloud-album/global"
"schisandra-cloud-album/model"
)
// GetRoleById : 通过Id获取角色信息
func (RoleService) GetRoleById(id int64) (model.ScaAuthRole, error) {
var role model.ScaAuthRole
if err := global.DB.Where("id = ? and deleted = 0", id).First(&role).Error; err != nil {
return model.ScaAuthRole{}, err
}
return role, nil
}

View File

@@ -1,12 +1,16 @@
package service
import (
"schisandra-cloud-album/service/auth_service"
"schisandra-cloud-album/service/role_service"
"schisandra-cloud-album/service/user_role_service"
"schisandra-cloud-album/service/user_service"
)
// Services 统一导出的service
type Services struct {
AuthService auth_service.AuthService
UserService user_service.UserService
RoleService role_service.RoleService
UserRoleService user_role_service.UserRoleService
}
// Service new函数实例化实例化完成后会返回结构体地指针类型

View File

@@ -0,0 +1,3 @@
package user_role_service
type UserRoleService struct{}

View File

@@ -0,0 +1,23 @@
package user_role_service
import (
"schisandra-cloud-album/global"
"schisandra-cloud-album/model"
)
// GetUserRoleIdsByUserId 通过用户ID获取用户角色ID列表
func (UserRoleService) GetUserRoleIdsByUserId(userId int64) ([]*int64, error) {
var roleIds []*int64
if err := global.DB.Table("sca_auth_user_role").Where("user_id = ?", userId).Pluck("role_id", &roleIds).Error; err != nil {
return nil, err
}
return roleIds, nil
}
// AddUserRole 新增用户角色
func (UserRoleService) AddUserRole(userRole model.ScaAuthUserRole) error {
if err := global.DB.Create(&userRole).Error; err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,3 @@
package user_service
type UserService struct{}

View File

@@ -1,4 +1,4 @@
package auth_service
package user_service
import (
"gorm.io/gorm"
@@ -8,52 +8,60 @@ import (
)
// GetUserList 获取所有用户列表
func (AuthService) GetUserList() []*model.ScaAuthUser {
func (UserService) GetUserList() []*model.ScaAuthUser {
data := make([]*model.ScaAuthUser, 0)
global.DB.Where("deleted = 0 ").Find(&data)
return data
}
// QueryUserByUsername 根据用户名查询用户
func (AuthService) QueryUserByUsername(username string) model.ScaAuthUser {
func (UserService) QueryUserByUsername(username string) model.ScaAuthUser {
authUser := model.ScaAuthUser{}
global.DB.Where("username = ? and deleted = 0", username).First(&authUser)
return authUser
}
// QueryUserByUuid 根据用户uuid查询用户
func (AuthService) QueryUserByUuid(uuid string) model.ScaAuthUser {
func (UserService) QueryUserByUuid(uuid string) model.ScaAuthUser {
authUser := model.ScaAuthUser{}
global.DB.Where("uuid = ? and deleted = 0", uuid).First(&authUser)
return authUser
}
// AddUser 添加用户
func (AuthService) AddUser(user model.ScaAuthUser) error {
return global.DB.Create(&user).Error
func (UserService) AddUser(user model.ScaAuthUser) (model.ScaAuthUser, error) {
if err := global.DB.Create(&user).Error; err != nil {
return model.ScaAuthUser{}, err
}
// 查询创建后的用户信息
var createdUser model.ScaAuthUser
if err := global.DB.First(&createdUser, user.ID).Error; err != nil {
return model.ScaAuthUser{}, err
}
return createdUser, nil
}
// UpdateUser 更新用户
func (AuthService) UpdateUser(user model.ScaAuthUser) *gorm.DB {
func (UserService) UpdateUser(user model.ScaAuthUser) *gorm.DB {
authUser := model.ScaAuthUser{}
return global.DB.Model(&authUser).Where("uuid = ?", user.UUID).Updates(user)
return global.DB.Model(&authUser).Where("uuid = ?", user.UID).Updates(user)
}
// DeleteUser 删除用户
func (AuthService) DeleteUser(uuid string) error {
func (UserService) DeleteUser(uuid string) error {
authUser := model.ScaAuthUser{}
return global.DB.Model(&authUser).Where("uuid = ?", uuid).Updates(&model.ScaAuthUser{Deleted: &enum.DELETED}).Error
}
// QueryUserByPhone 根据手机号查询用户
func (AuthService) QueryUserByPhone(phone string) model.ScaAuthUser {
func (UserService) QueryUserByPhone(phone string) model.ScaAuthUser {
authUser := model.ScaAuthUser{}
global.DB.Where("phone = ? and deleted = 0", phone).First(&authUser)
return authUser
}
// QueryUserByEmail 根据邮箱查询用户
func (AuthService) QueryUserByEmail(email string) model.ScaAuthUser {
func (UserService) QueryUserByEmail(email string) model.ScaAuthUser {
authUser := model.ScaAuthUser{}
global.DB.Where("email = ? and deleted = 0", email).First(&authUser)
return authUser

View File

@@ -7,9 +7,8 @@ import (
)
type JWTPayload struct {
UserID int `json:"user_id"`
Role string `json:"role"`
Username string `json:"username"`
UserID *string `json:"user_id"`
RoleID []*int64 `json:"role_id"`
}
type JWTClaims struct {
@@ -17,10 +16,11 @@ type JWTClaims struct {
jwt.RegisteredClaims
}
var MySecret = []byte(global.CONFIG.JWT.Secret)
var MySecret []byte
// GenerateToken generates a JWT token with the given payload
func GenerateToken(payload JWTPayload) (string, error) {
MySecret = []byte(global.CONFIG.JWT.Secret)
claims := JWTClaims{
JWTPayload: payload,
RegisteredClaims: jwt.RegisteredClaims{
@@ -33,17 +33,55 @@ func GenerateToken(payload JWTPayload) (string, error) {
return token.SignedString(MySecret)
}
// GenerateAccessTokenAndRefreshToken generates a JWT token with the given payload, and returns the accessToken and refreshToken
func GenerateAccessTokenAndRefreshToken(payload JWTPayload) (string, string, int64) {
MySecret = []byte(global.CONFIG.JWT.Secret)
// accessToken 的数据
accessClaims := JWTClaims{
JWTPayload: payload,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 2)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: global.CONFIG.JWT.Issuer,
},
}
refreshClaims := JWTClaims{
JWTPayload: payload,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)), // 7天
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: global.CONFIG.JWT.Issuer,
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
accessTokenString, err := accessToken.SignedString(MySecret)
if err != nil {
global.LOG.Error(err)
return "", "", 0
}
refreshTokenString, err := refreshToken.SignedString(MySecret)
if err != nil {
global.LOG.Error(err)
return "", "", 0
}
return accessTokenString, refreshTokenString, refreshClaims.ExpiresAt.Time.Unix()
}
// ParseToken parses a JWT token and returns the payload
func ParseToken(tokenString string) (*JWTPayload, error) {
func ParseToken(tokenString string) (*JWTPayload, bool, error) {
MySecret = []byte(global.CONFIG.JWT.Secret)
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
return MySecret, nil
})
if err != nil {
global.LOG.Error(err)
return nil, err
return nil, false, err
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return &claims.JWTPayload, nil
return &claims.JWTPayload, true, nil
}
return nil, err
return nil, false, err
}