This commit is contained in:
2023-11-02 04:34:46 +08:00
commit c4548fe498
369 changed files with 40208 additions and 0 deletions

178
oauth2/handle/oauth2.go Normal file
View File

@@ -0,0 +1,178 @@
package handle
import (
"context"
"encoding/json"
er "errors"
"fmt"
"git.echol.cn/loser/logger/log"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/errors"
"miniapp/client"
"miniapp/model/app"
"miniapp/model/cache"
"miniapp/model/common/constant"
"miniapp/service"
"net/http"
"strconv"
"strings"
"time"
)
// UserAuthorizationHandler 获取用户Id
func UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (userId string, err error) {
loginType := constant.LoginType(r.FormValue("type")) // 登录类型
userIdentity := constant.UserIdentity(r.FormValue("identity")) // 身份类型
account := r.FormValue("username") // 用户传入账号(或者授权Code)
nikeName := r.FormValue("nickName") // 昵称
avatarUrl := r.FormValue("avatarUrl") // 头像
log.Debugf("预处理用户登录请求,身份类型: %v => 登录类型: %s => 账号: %v", userIdentity, loginType, account)
var roleCode []string
// 处理用户Id
switch userIdentity {
case constant.UserIdentityAdmin:
// 管理员
case constant.UserIdentityUser:
// 普通用户
userId, err = getUser(account, loginType, nikeName, avatarUrl)
default:
err = er.New("未知的用户身份类型")
}
if err != nil {
return
}
// 组装缓存用户信息
m := cache.UserInfo{
RoleCodes: strings.Join(roleCode, ","),
UserId: userId,
UserType: userIdentity.String(),
}
userInfo, err := m.String()
if err != nil {
err = errors.New("登录失败,请联系管理员")
return
}
if err = client.Redis.Set(context.Background(), fmt.Sprintf("%s%v", constant.OAuth2UserCacheKey, userId), userInfo, time.Hour*24*7).Err(); err != nil {
log.Errorf("缓存用户信息失败用户ID%v错误信息%s", userId, err.Error())
err = errors.New("登录失败,请联系管理员")
return
}
return
}
// LoginWithPassword 账号密码登录模式
func LoginWithPassword(ctx context.Context, clientId, userId, password string) (userID string, err error) {
log.Debugf("[%v]处理登录请求用户Id%s --> %s", clientId, userId, password)
userID = userId
return
}
// CheckClient 检查是否允许该客户端通过该授权模式请求令牌
func CheckClient(clientID string, grant oauth2.GrantType) (allowed bool, err error) {
// 解出租户Id和传入的客户端Id
c := app.OAuth2Client{ClientId: clientID}
// 查询客户端配置信息
if err = service.ServiceGroupApp.AppServiceGroup.Oauth2ClientService.FindOne(&c); err != nil {
log.Errorf("客户端信息查询失败: %v", err.Error())
err = errors.New("客户端信息查询失败: " + err.Error())
allowed = false
return
}
// 判断是否包含授权范围
allowed = strings.Contains(c.Grant, string(grant))
if !allowed {
err = errors.New("不受允许的grant_type")
}
return
}
// ExtensionFields 自定义响应Token的扩展字段
func ExtensionFields(ti oauth2.TokenInfo) (fieldsValue map[string]any) {
fieldsValue = map[string]any{}
fieldsValue["license"] = "Made By Lee"
// 取出用户信息
var userInfo app.User
tid, _ := strconv.Atoi(ti.GetUserID())
userInfo.ID = uint(tid)
//if err := repository.User().GetUser(&userInfo); err != nil {
// return
//}
service.ServiceGroupApp.AppServiceGroup.UserService.GetUser(&userInfo)
fieldsValue["newUser"] = time.Now().Sub(userInfo.CreatedAt).Minutes() <= 1
fieldsValue["nickname"] = userInfo.Nickname
fieldsValue["phone"] = userInfo.Phone
fieldsValue["userId"] = userInfo.ID
fieldsValue["avatar"] = userInfo.Avatar
fieldsValue["TimeNote"] = userInfo.Avatar
return
}
// ResponseToken 返回Token生成结果
func ResponseToken(w http.ResponseWriter, data map[string]any, header http.Header, statusCode ...int) error {
log.Debugf("返回Token原始数据: %+v", data)
type response struct {
Code int `json:"code"`
Data map[string]any `json:"data"`
Msg string `json:"message"`
}
status := http.StatusOK
msg := "login success"
if len(statusCode) > 0 && statusCode[0] > 0 {
status = statusCode[0]
msg = fmt.Sprintf("%v", data["error_description"])
// 处理特殊返回 - 刷新Token到期了
switch data["error"] {
case "invalid_grant":
msg = "登录已过期,请重新授权登录"
case "invalid_request":
msg = "登录参数错误"
default:
log.Errorf("收到未定义的登录错误: %v", data["error_description"])
}
data = nil
}
res := response{
Code: status,
Msg: msg,
Data: data,
}
jsonBytes, err := json.Marshal(res)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
for key := range header {
w.Header().Set(key, header.Get(key))
}
w.WriteHeader(status)
_, err = w.Write(jsonBytes)
if err != nil {
log.Errorf("返回Token失败: %v", err.Error())
return err
}
return err
}
// InternalErrorHandler 自定义内部错误处理
func InternalErrorHandler(err error) (re *errors.Response) {
re = errors.NewResponse(err, http.StatusUnauthorized)
re.Description = err.Error()
return
}

View File

@@ -0,0 +1,65 @@
package handle
import (
"context"
"encoding/json"
"fmt"
"git.echol.cn/loser/logger/log"
"github.com/go-oauth2/oauth2/v4"
"github.com/google/uuid"
"miniapp/client"
"miniapp/model/common/constant"
"strings"
)
func NewAccessGenerate() *AccessGenerate {
return &AccessGenerate{}
}
type AccessGenerate struct {
}
// Token 手动实现Token生成直接生成UUID替换掉自带的那个憨得一批的长长的字符串
func (ag *AccessGenerate) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (string, string, error) {
u, _ := uuid.NewUUID()
access := strings.ReplaceAll(u.String(), "-", "")
refresh := ""
if isGenRefresh {
u, _ = uuid.NewUUID()
refresh = strings.ReplaceAll(u.String(), "-", "")
}
// 生成新的,清理掉旧的
ag.clearOldToken(ctx, data.UserID)
// 返回结果
return access, refresh, nil
}
// 清理掉旧的Token和RefreshToken
func (ag *AccessGenerate) clearOldToken(ctx context.Context, userId string) {
rdsKey := constant.OAuth2RedisKey + "*-*"
// 获取所有符合条件的Key
keys, err := client.Redis.Keys(ctx, rdsKey).Result()
if err != nil {
log.Errorf("清理失败,跳过清理")
return
}
for _, key := range keys {
dataStr, err := client.Redis.Get(ctx, key).Result()
if err != nil {
continue
}
var m map[string]any
if err = json.Unmarshal([]byte(dataStr), &m); err != nil {
continue
}
// 找到匹配的数据
if m["UserID"] == userId {
// 删除AccessToken
client.Redis.Del(ctx, fmt.Sprintf("%v%v", constant.OAuth2RedisKey, m["Access"]))
client.Redis.Del(ctx, fmt.Sprintf("%v%v", constant.OAuth2RedisKey, m["Refresh"]))
client.Redis.Del(ctx, key)
continue
}
}
}

69
oauth2/handle/user.go Normal file
View File

@@ -0,0 +1,69 @@
package handle
import (
"errors"
"git.echol.cn/loser/logger/log"
"miniapp/model/app"
"miniapp/model/common/constant"
"miniapp/service"
"miniapp/utils"
"strconv"
)
// 获取普通用户信息
func getUser(account string, loginType constant.LoginType, nikeName string, avatarUrl string) (userId string, err error) {
// 根据登录类型获取用户信息
// 定义微信小程序信息
//var unionId, openId, sessionKey string
var mobile string
// 定义用户信息
var user app.User
//user.InviteCode = &inviteCode
switch loginType {
case constant.LoginTypeWeChatMiniApp:
mobile, err = utils.WeChatUtils().GetPhoneNumber(account)
if err != nil {
return
}
if mobile == "" {
err = errors.New("获取手机号失败")
return
}
user.Phone = mobile
user.Nickname = nikeName
user.Avatar = avatarUrl
default:
user.Phone = account
user.Nickname = nikeName
user.Avatar = avatarUrl
}
// 查询用户信息
if err = service.ServiceGroupApp.AppServiceGroup.UserService.GetOrCreate(&user); err != nil {
log.Errorf("获取用户信息或创建用户失败,错误信息:%s", err.Error())
err = errors.New("登录失败,请联系管理员")
return
}
// 校验用户状态
if user.Status == constant.UserStatusDisabled {
err = errors.New("账户已被禁用")
return
}
// 异步缓存小程序SessionKey
//go func() {
// if loginType == constant.LoginTypeWeChatMiniApp {
// // 缓存SessionKey
// if client.Redis.Set(context.Background(), constant.WeChatSessionKey+user.Id, sessionKey, 3*24*time.Hour).Err() != nil {
// log.Errorf("缓存SessionKey失败用户Id%s", user.Id)
// }
// }
//}()
// 返回用户Id
userId = strconv.Itoa(int(user.ID))
return
}

76
oauth2/server.go Normal file
View File

@@ -0,0 +1,76 @@
package oauth2
import (
"git.echol.cn/loser/logger/log"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
od "github.com/go-oauth2/redis/v4"
"miniapp/client"
"miniapp/model/app"
"miniapp/model/common/constant"
"miniapp/oauth2/handle"
"miniapp/service"
"time"
)
var OAuthServer *server.Server // Oauth服务
// InitOAuth2Server 初始化OAuth2服务端
func InitOAuth2Server() {
manager := manage.NewDefaultManager()
// 配置信息
cfg := &manage.Config{
AccessTokenExp: time.Hour * 3, // 访问令牌过期时间,三小时
RefreshTokenExp: time.Hour * 24 * 7, // 更新令牌过期时间,一周
IsGenerateRefresh: true, // 是否生成新的更新令牌
}
// 设置密码模式的配置参数
manager.SetPasswordTokenCfg(cfg)
manager.MapTokenStorage(od.NewRedisStoreWithCli(client.Redis, constant.OAuth2RedisKey))
// 生成Token方式
manager.MapAccessGenerate(handle.NewAccessGenerate())
// 配置客户端
clientStore := store.NewClientStore()
// 从数据库查询所有client信息
var clients []app.OAuth2Client
if err := service.ServiceGroupApp.AppServiceGroup.Oauth2ClientService.FinAll(&clients); err != nil {
log.Panicf("OAuth2服务启动失败[Client信息拉取失败]: %v", err.Error())
}
if len(clients) == 0 {
log.Panic("未配置OAuth2客户端信息")
}
// 组装数据
for _, c := range clients {
_ = clientStore.Set(c.ClientId, &models.Client{
ID: c.ClientId,
Secret: c.ClientSecret,
})
}
log.Debug("客户端信息初始化完成")
manager.MapClientStorage(clientStore)
srv := server.NewServer(server.NewConfig(), manager)
// 设置密码登录模式处理逻辑
srv.SetPasswordAuthorizationHandler(handle.LoginWithPassword)
// 允许密码模式、刷新Token
srv.SetAllowedGrantType(oauth2.PasswordCredentials, oauth2.Refreshing)
// 客户端ID和授权模式检查
srv.SetClientAuthorizedHandler(handle.CheckClient)
// 自定义响应Token的扩展字段
srv.SetExtensionFieldsHandler(handle.ExtensionFields)
// 自定义返回数据接口
srv.SetResponseTokenHandler(handle.ResponseToken)
// 自定义内部错误处理
srv.SetInternalErrorHandler(handle.InternalErrorHandler)
// 响应错误处理(支持自定义URI及错误明细)
//srv.SetResponseErrorHandler(handle.ResponseErrorHandler)
// 自定义解析用户Id函数
srv.SetUserAuthorizationHandler(handle.UserAuthorizationHandler)
OAuthServer = srv
}