♻️ reconstruct the authentication mode
This commit is contained in:
@@ -10,6 +10,11 @@ type Config struct {
|
||||
Auth struct {
|
||||
AccessSecret string
|
||||
}
|
||||
|
||||
Encrypt struct {
|
||||
Key string
|
||||
IV string
|
||||
}
|
||||
Mysql struct {
|
||||
DataSource string
|
||||
MaxOpenConn int
|
||||
|
@@ -24,7 +24,7 @@ import (
|
||||
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
@@ -45,7 +45,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
@@ -61,7 +61,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.CasbinVerifyMiddleware, serverCtx.AuthorizationMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.CasbinVerifyMiddleware, serverCtx.AuthorizationMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
@@ -108,7 +108,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
@@ -159,7 +159,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
@@ -185,7 +185,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.CasbinVerifyMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.CasbinVerifyMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
@@ -202,7 +202,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
@@ -218,7 +218,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware},
|
||||
[]rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
|
@@ -73,9 +73,10 @@ func (l *GetCommentListLogic) GetCommentList(req *types.CommentListRequest) (res
|
||||
}
|
||||
if count == 0 || len(commentQueryList) == 0 {
|
||||
return response.SuccessWithData(types.CommentListPageResponse{
|
||||
Total: count,
|
||||
Size: req.Size,
|
||||
Current: req.Page,
|
||||
Total: count,
|
||||
Size: req.Size,
|
||||
Current: req.Page,
|
||||
Comments: []types.CommentContent{},
|
||||
}), nil
|
||||
}
|
||||
// **************** 获取评论Id和用户Id ************
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
models2 "github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/models"
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/officialAccount/server/handlers/models"
|
||||
"net/http"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/encrypt"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/i18n"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/response"
|
||||
"schisandra-album-cloud-microservices/app/core/api/internal/logic/websocket"
|
||||
@@ -96,9 +97,17 @@ func (l *WechatOffiaccountCallbackLogic) WechatOffiaccountCallback(r *http.Reque
|
||||
|
||||
// SendMessage 发送消息到客户端
|
||||
func (l *WechatOffiaccountCallbackLogic) SendMessage(openId string, clientId string) error {
|
||||
encryptClientId, err := encrypt.Encrypt(clientId, l.svcCtx.Config.Encrypt.Key, l.svcCtx.Config.Encrypt.IV)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptOpenId, err := encrypt.Encrypt(openId, l.svcCtx.Config.Encrypt.Key, l.svcCtx.Config.Encrypt.IV)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
messageData := MessageData{
|
||||
Openid: openId,
|
||||
ClientId: clientId,
|
||||
Openid: encryptOpenId,
|
||||
ClientId: encryptClientId,
|
||||
}
|
||||
jsonData, err := json.Marshal(response.SuccessWithData(messageData))
|
||||
if err != nil {
|
||||
|
@@ -20,6 +20,10 @@ type RefreshTokenLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
type AccessToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpireAt int64 `json:"expire_at"`
|
||||
}
|
||||
|
||||
func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic {
|
||||
return &RefreshTokenLogic{
|
||||
@@ -34,6 +38,7 @@ func (l *RefreshTokenLogic) RefreshToken(r *http.Request) (resp *types.Response,
|
||||
if userId == "" {
|
||||
return response.ErrorWithCode(403), nil
|
||||
}
|
||||
// 从redis中获取refresh token
|
||||
tokenData := l.svcCtx.RedisClient.Get(l.ctx, constant.UserTokenPrefix+userId).Val()
|
||||
if tokenData == "" {
|
||||
return response.ErrorWithCode(403), nil
|
||||
@@ -43,30 +48,42 @@ func (l *RefreshTokenLogic) RefreshToken(r *http.Request) (resp *types.Response,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 判断是否已经被吊销
|
||||
if redisTokenData.Revoked {
|
||||
return response.ErrorWithCode(403), nil
|
||||
}
|
||||
// 判断是否是同一个设备
|
||||
if redisTokenData.AllowAgent != r.UserAgent() {
|
||||
return response.ErrorWithCode(403), nil
|
||||
}
|
||||
// 判断refresh token是否在有效期内
|
||||
refreshToken, result := jwt.ParseRefreshToken(l.svcCtx.Config.Auth.AccessSecret, redisTokenData.RefreshToken)
|
||||
if !result {
|
||||
return response.ErrorWithCode(403), nil
|
||||
}
|
||||
accessToken := jwt.GenerateAccessToken(l.svcCtx.Config.Auth.AccessSecret, jwt.AccessJWTPayload{
|
||||
// 生成新的access token
|
||||
accessToken, expireAt := jwt.GenerateAccessToken(l.svcCtx.Config.Auth.AccessSecret, jwt.AccessJWTPayload{
|
||||
UserID: refreshToken.UserID,
|
||||
Type: constant.JWT_TYPE_ACCESS,
|
||||
})
|
||||
if accessToken == "" {
|
||||
return response.ErrorWithCode(403), nil
|
||||
}
|
||||
// 更新redis中的access token
|
||||
redisToken := types.RedisToken{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: redisTokenData.RefreshToken,
|
||||
UID: refreshToken.UserID,
|
||||
Revoked: false,
|
||||
GeneratedAt: redisTokenData.GeneratedAt,
|
||||
AllowAgent: redisTokenData.AllowAgent,
|
||||
GeneratedIP: redisTokenData.GeneratedIP,
|
||||
UpdatedAt: time.Now().Format(constant.TimeFormat),
|
||||
}
|
||||
err = l.svcCtx.RedisClient.Set(l.ctx, constant.UserTokenPrefix+refreshToken.UserID, redisToken, time.Hour*24*7).Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.SuccessWithData(accessToken), nil
|
||||
token := AccessToken{
|
||||
AccessToken: accessToken,
|
||||
ExpireAt: expireAt,
|
||||
}
|
||||
return response.SuccessWithData(token), nil
|
||||
}
|
||||
|
@@ -81,15 +81,15 @@ func HandleLoginJWT(user *model.ScaAuthUser, svcCtx *svc.ServiceContext, autoLog
|
||||
return nil, err
|
||||
}
|
||||
// 生成jwt token
|
||||
accessToken := jwt.GenerateAccessToken(svcCtx.Config.Auth.AccessSecret, jwt.AccessJWTPayload{
|
||||
accessToken, expireAt := jwt.GenerateAccessToken(svcCtx.Config.Auth.AccessSecret, jwt.AccessJWTPayload{
|
||||
UserID: user.UID,
|
||||
Type: constant.JWT_TYPE_ACCESS,
|
||||
})
|
||||
var days time.Duration
|
||||
if autoLogin {
|
||||
days = 24 * time.Hour
|
||||
days = 3 * 24 * time.Hour
|
||||
} else {
|
||||
days = time.Hour * 1
|
||||
days = time.Hour * 24
|
||||
}
|
||||
refreshToken := jwt.GenerateRefreshToken(svcCtx.Config.Auth.AccessSecret, jwt.RefreshJWTPayload{
|
||||
UserID: user.UID,
|
||||
@@ -97,6 +97,7 @@ func HandleLoginJWT(user *model.ScaAuthUser, svcCtx *svc.ServiceContext, autoLog
|
||||
}, days)
|
||||
data := types.LoginResponse{
|
||||
AccessToken: accessToken,
|
||||
ExpireAt: expireAt,
|
||||
UID: user.UID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
@@ -109,6 +110,10 @@ func HandleLoginJWT(user *model.ScaAuthUser, svcCtx *svc.ServiceContext, autoLog
|
||||
RefreshToken: refreshToken,
|
||||
UID: user.UID,
|
||||
Revoked: false,
|
||||
GeneratedAt: time.Now().Format(constant.TimeFormat),
|
||||
AllowAgent: r.UserAgent(),
|
||||
GeneratedIP: utils.GetClientIP(r),
|
||||
UpdatedAt: time.Now().Format(constant.TimeFormat),
|
||||
}
|
||||
err = svcCtx.RedisClient.Set(ctx, constant.UserTokenPrefix+user.UID, redisToken, days).Err()
|
||||
if err != nil {
|
||||
|
@@ -31,7 +31,7 @@ func NewGetWechatOffiaccountQrcodeLogic(ctx context.Context, svcCtx *svc.Service
|
||||
}
|
||||
|
||||
func (l *GetWechatOffiaccountQrcodeLogic) GetWechatOffiaccountQrcode(r *http.Request, req *types.OAuthWechatRequest) (resp *types.Response, err error) {
|
||||
ip := utils.GetClientIP(r) // 使用工具函数获取客户端IP
|
||||
ip := utils.GetClientIP(r)
|
||||
key := constant.UserQrcodePrefix + ip
|
||||
|
||||
// 从Redis获取二维码数据
|
||||
@@ -45,7 +45,7 @@ func (l *GetWechatOffiaccountQrcodeLogic) GetWechatOffiaccountQrcode(r *http.Req
|
||||
}
|
||||
|
||||
// 生成临时二维码
|
||||
data, err := l.svcCtx.WechatOfficial.QRCode.Temporary(l.ctx, req.Client_id, 7*24*3600)
|
||||
data, err := l.svcCtx.WechatOfficial.QRCode.Temporary(l.ctx, req.ClientId, 7*24*3600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/constant"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/encrypt"
|
||||
randomname "schisandra-album-cloud-microservices/app/core/api/common/random_name"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/response"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/utils"
|
||||
@@ -34,9 +35,21 @@ func NewWechatOffiaccountLoginLogic(ctx context.Context, svcCtx *svc.ServiceCont
|
||||
}
|
||||
|
||||
func (l *WechatOffiaccountLoginLogic) WechatOffiaccountLogin(r *http.Request, req *types.WechatOffiaccountLoginRequest) (resp *types.Response, err error) {
|
||||
decryptedClientId, err := encrypt.Decrypt(req.ClientId, l.svcCtx.Config.Encrypt.Key, l.svcCtx.Config.Encrypt.IV)
|
||||
if err != nil {
|
||||
return response.ErrorWithI18n(l.ctx, "login.loginFailed"), nil
|
||||
}
|
||||
clientId := l.svcCtx.RedisClient.Get(r.Context(), constant.UserClientPrefix+decryptedClientId).Val()
|
||||
if clientId == "" {
|
||||
return response.ErrorWithI18n(l.ctx, "login.loginFailed"), nil
|
||||
}
|
||||
Openid, err := encrypt.Decrypt(req.Openid, l.svcCtx.Config.Encrypt.Key, l.svcCtx.Config.Encrypt.IV)
|
||||
if err != nil {
|
||||
return response.ErrorWithI18n(l.ctx, "login.loginFailed"), nil
|
||||
}
|
||||
tx := l.svcCtx.DB.Begin()
|
||||
userSocial := l.svcCtx.DB.ScaAuthUserSocial
|
||||
socialUser, err := tx.ScaAuthUserSocial.Where(userSocial.OpenID.Eq(req.Openid), userSocial.Source.Eq(constant.OAuthSourceWechat)).First()
|
||||
socialUser, err := tx.ScaAuthUserSocial.Where(userSocial.OpenID.Eq(Openid), userSocial.Source.Eq(constant.OAuthSourceWechat)).First()
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,7 +63,7 @@ func (l *WechatOffiaccountLoginLogic) WechatOffiaccountLogin(r *http.Request, re
|
||||
addUser := &model.ScaAuthUser{
|
||||
UID: uidStr,
|
||||
Avatar: avatar,
|
||||
Username: req.Openid,
|
||||
Username: Openid,
|
||||
Nickname: name,
|
||||
Gender: constant.Male,
|
||||
}
|
||||
@@ -62,7 +75,7 @@ func (l *WechatOffiaccountLoginLogic) WechatOffiaccountLogin(r *http.Request, re
|
||||
|
||||
newSocialUser := &model.ScaAuthUserSocial{
|
||||
UserID: uidStr,
|
||||
OpenID: req.Openid,
|
||||
OpenID: Openid,
|
||||
Source: constant.OAuthSourceWechat,
|
||||
}
|
||||
err = tx.ScaAuthUserSocial.Create(newSocialUser)
|
||||
|
@@ -1,29 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"net/http"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/constant"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/response"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthorizationMiddleware struct {
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
func NewAuthorizationMiddleware(redis *redis.Client) *AuthorizationMiddleware {
|
||||
return &AuthorizationMiddleware{
|
||||
Redis: redis,
|
||||
}
|
||||
func NewAuthorizationMiddleware() *AuthorizationMiddleware {
|
||||
return &AuthorizationMiddleware{}
|
||||
}
|
||||
|
||||
func (m *AuthorizationMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userId := r.Context().Value("user_id").(string)
|
||||
redisToken := m.Redis.Get(r.Context(), constant.UserTokenPrefix+userId).Val()
|
||||
if redisToken == "" {
|
||||
httpx.OkJson(w, response.ErrorWithCodeMessage(403, "unauthorized"))
|
||||
expireAtStr := r.Header.Get("X-Expire-At")
|
||||
if expireAtStr == "" {
|
||||
httpx.OkJson(w, response.ErrorWithCodeMessage(http.StatusForbidden, "unauthorized"))
|
||||
return
|
||||
}
|
||||
expireAtInt, err := strconv.ParseInt(expireAtStr, 10, 64)
|
||||
if err != nil {
|
||||
logx.Errorf("Failed to parse X-Expire-At: %v", err)
|
||||
httpx.OkJson(w, response.ErrorWithCodeMessage(http.StatusForbidden, "unauthorized"))
|
||||
return
|
||||
}
|
||||
expireAt := time.Unix(expireAtInt, 0)
|
||||
currentTime := time.Now()
|
||||
|
||||
remainingTime := expireAt.Sub(currentTime)
|
||||
if remainingTime < time.Minute*5 {
|
||||
httpx.OkJson(w, response.ErrorWithCodeMessage(http.StatusUnauthorized, "token about to expire, refresh"))
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
|
44
app/core/api/internal/middleware/nonce_middleware.go
Normal file
44
app/core/api/internal/middleware/nonce_middleware.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"net/http"
|
||||
"schisandra-album-cloud-microservices/app/core/api/common/constant"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NonceMiddleware struct {
|
||||
RedisClient *redis.Client
|
||||
}
|
||||
|
||||
func NewNonceMiddleware(redisClient *redis.Client) *NonceMiddleware {
|
||||
return &NonceMiddleware{
|
||||
RedisClient: redisClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NonceMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
nonce := r.Header.Get("X-Nonce")
|
||||
if nonce == "" {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusBadRequest, "bad request!")
|
||||
return
|
||||
}
|
||||
if len(nonce) != 32 {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusBadRequest, "bad request!")
|
||||
return
|
||||
}
|
||||
result := m.RedisClient.Get(r.Context(), constant.SystemApiNoncePrefix+nonce).Val()
|
||||
if result != "" {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusBadRequest, "bad request!")
|
||||
return
|
||||
}
|
||||
err := m.RedisClient.Set(r.Context(), constant.SystemApiNoncePrefix+nonce, nonce, time.Minute*1).Err()
|
||||
if err != nil {
|
||||
httpx.WriteJsonCtx(r.Context(), w, http.StatusInternalServerError, "internal server error!")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
@@ -29,6 +29,7 @@ type ServiceContext struct {
|
||||
SecurityHeadersMiddleware rest.Middleware
|
||||
CasbinVerifyMiddleware rest.Middleware
|
||||
AuthorizationMiddleware rest.Middleware
|
||||
NonceMiddleware rest.Middleware
|
||||
DB *query.Query
|
||||
RedisClient *redis.Client
|
||||
MongoClient *mongo.Database
|
||||
@@ -48,7 +49,8 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
Config: c,
|
||||
SecurityHeadersMiddleware: middleware.NewSecurityHeadersMiddleware().Handle,
|
||||
CasbinVerifyMiddleware: middleware.NewCasbinVerifyMiddleware(casbinEnforcer).Handle,
|
||||
AuthorizationMiddleware: middleware.NewAuthorizationMiddleware(redisClient).Handle,
|
||||
AuthorizationMiddleware: middleware.NewAuthorizationMiddleware().Handle,
|
||||
NonceMiddleware: middleware.NewNonceMiddleware(redisClient).Handle,
|
||||
DB: queryDB,
|
||||
RedisClient: redisClient,
|
||||
MongoClient: mongodb.NewMongoDB(c.Mongo.Uri, c.Mongo.Username, c.Mongo.Password, c.Mongo.AuthSource, c.Mongo.Database),
|
||||
|
@@ -7,6 +7,10 @@ type RedisToken struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
UID string `json:"uid"`
|
||||
Revoked bool `json:"revoked" default:"false"`
|
||||
AllowAgent string `json:"allow_agent"`
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
GeneratedIP string `json:"generated_ip"`
|
||||
}
|
||||
|
||||
func (res RedisToken) MarshalBinary() ([]byte, error) {
|
||||
|
@@ -39,6 +39,7 @@ type CommentRequest struct {
|
||||
|
||||
type LoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpireAt int64 `json:"expire_at"`
|
||||
UID string `json:"uid"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
@@ -55,7 +56,7 @@ type OAuthRequest struct {
|
||||
}
|
||||
|
||||
type OAuthWechatRequest struct {
|
||||
Client_id string `form:"client_id"`
|
||||
ClientId string `json:"client_id"`
|
||||
}
|
||||
|
||||
type PhoneLoginRequest struct {
|
||||
|
Reference in New Issue
Block a user