mirror of https://github.com/cloudreve/Cloudreve
				
				
				
			Feat: 2-FA login verification
							parent
							
								
									11e45bc751
								
							
						
					
					
						commit
						7c07b623f6
					
				| 
						 | 
				
			
			@ -172,6 +172,13 @@ func GetActiveUserByID(ID interface{}) (User, error) {
 | 
			
		|||
	return user, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetActiveUserByOpenID 用OpenID获取可登录用户
 | 
			
		||||
func GetActiveUserByOpenID(openid string) (User, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
	result := DB.Set("gorm:auto_preload", true).Where("status = ? and open_id = ?", Active, openid).Find(&user)
 | 
			
		||||
	return user, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserByEmail 用Email获取用户
 | 
			
		||||
func GetUserByEmail(email string) (User, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ type User struct {
 | 
			
		|||
	CreatedAt      int64  `json:"created_at"`
 | 
			
		||||
	PreferredTheme string `json:"preferred_theme"`
 | 
			
		||||
	Score          int    `json:"score"`
 | 
			
		||||
	Anonymous      bool   `json:"anonymous"`
 | 
			
		||||
	Policy         policy `json:"policy"`
 | 
			
		||||
	Group          group  `json:"group"`
 | 
			
		||||
	Tags           []tag  `json:"tags"`
 | 
			
		||||
| 
						 | 
				
			
			@ -97,6 +98,7 @@ func BuildUser(user model.User) User {
 | 
			
		|||
		CreatedAt:      user.CreatedAt.Unix(),
 | 
			
		||||
		PreferredTheme: user.OptionsSerialized.PreferredTheme,
 | 
			
		||||
		Score:          user.Score,
 | 
			
		||||
		Anonymous:      user.IsAnonymous(),
 | 
			
		||||
		Policy: policy{
 | 
			
		||||
			SaveType:       user.Policy.Type,
 | 
			
		||||
			MaxSize:        fmt.Sprintf("%.2fmb", float64(user.Policy.MaxSize)/(1024*1024)),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
	model "github.com/HFO4/cloudreve/models"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/authn"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/qq"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/serializer"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/thumb"
 | 
			
		||||
	"github.com/HFO4/cloudreve/pkg/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -126,6 +127,34 @@ func UserLogin(c *gin.Context) {
 | 
			
		|||
	} else {
 | 
			
		||||
		c.JSON(200, ErrorResponse(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// User2FALogin 用户二步验证登录
 | 
			
		||||
func User2FALogin(c *gin.Context) {
 | 
			
		||||
	var service user.Enable2FA
 | 
			
		||||
	if err := c.ShouldBindJSON(&service); err == nil {
 | 
			
		||||
		res := service.Login(c)
 | 
			
		||||
		c.JSON(200, res)
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(200, ErrorResponse(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UserQQLogin 初始化QQ登录
 | 
			
		||||
func UserQQLogin(c *gin.Context) {
 | 
			
		||||
	// 新建绑定
 | 
			
		||||
	res, err := qq.NewLoginRequest()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(200, serializer.Err(serializer.CodeNotSet, "无法使用QQ登录", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 设定QQ登录会话Secret
 | 
			
		||||
	util.SetSession(c, map[string]interface{}{"qq_login_secret": res.SecretKey})
 | 
			
		||||
 | 
			
		||||
	c.JSON(200, serializer.Response{
 | 
			
		||||
		Data: res.URL,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -103,6 +103,10 @@ func InitMasterRouter() *gin.Engine {
 | 
			
		|||
		{
 | 
			
		||||
			// 用户登录
 | 
			
		||||
			user.POST("session", controllers.UserLogin)
 | 
			
		||||
			// 用户登录
 | 
			
		||||
			user.POST("2fa", controllers.User2FALogin)
 | 
			
		||||
			// 初始化QQ登录
 | 
			
		||||
			user.POST("qq", controllers.UserQQLogin)
 | 
			
		||||
			// WebAuthn登陆初始化
 | 
			
		||||
			user.GET("authn/:username",
 | 
			
		||||
				middleware.IsFunctionEnabled("authn_enabled"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import (
 | 
			
		|||
	"github.com/HFO4/cloudreve/pkg/util"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/mojocn/base64Captcha"
 | 
			
		||||
	"github.com/pquerna/otp/totp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// UserLoginService 管理用户登录的服务
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +17,32 @@ type UserLoginService struct {
 | 
			
		|||
	CaptchaCode string `form:"captchaCode" json:"captchaCode"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Login 二步验证继续登录
 | 
			
		||||
func (service *Enable2FA) Login(c *gin.Context) serializer.Response {
 | 
			
		||||
	if uid, ok := util.GetSession(c, "2fa_user_id").(uint); ok {
 | 
			
		||||
		// 查找用户
 | 
			
		||||
		expectedUser, err := model.GetActiveUserByID(uid)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return serializer.Err(serializer.CodeNotFound, "用户不存在", nil)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 验证二步验证代码
 | 
			
		||||
		if !totp.Validate(service.Code, expectedUser.TwoFactor) {
 | 
			
		||||
			return serializer.ParamErr("验证代码不正确", nil)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//登陆成功,清空并设置session
 | 
			
		||||
		util.DeleteSession(c, "2fa_user_id")
 | 
			
		||||
		util.SetSession(c, map[string]interface{}{
 | 
			
		||||
			"user_id": expectedUser.ID,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		return serializer.BuildUserResponse(expectedUser)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return serializer.Err(serializer.CodeNotFound, "登录会话不存在", nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Login 用户登录函数
 | 
			
		||||
func (service *UserLoginService) Login(c *gin.Context) serializer.Response {
 | 
			
		||||
	isCaptchaRequired := model.GetSettingByName("login_captcha")
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +71,11 @@ func (service *UserLoginService) Login(c *gin.Context) serializer.Response {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if expectedUser.TwoFactor != "" {
 | 
			
		||||
		//TODO 二步验证处理
 | 
			
		||||
		// 需要二步验证
 | 
			
		||||
		util.SetSession(c, map[string]interface{}{
 | 
			
		||||
			"2fa_user_id": expectedUser.ID,
 | 
			
		||||
		})
 | 
			
		||||
		return serializer.Response{Code: 203}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//登陆成功,清空并设置session
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,11 +16,14 @@ type QQCallbackService struct {
 | 
			
		|||
 | 
			
		||||
// Callback 处理QQ互联回调
 | 
			
		||||
func (service *QQCallbackService) Callback(c *gin.Context, user *model.User) serializer.Response {
 | 
			
		||||
 | 
			
		||||
	state := util.GetSession(c, "qq_login_secret")
 | 
			
		||||
	if stateStr, ok := state.(string); !ok || stateStr != service.State {
 | 
			
		||||
		return serializer.Err(serializer.CodeSignExpired, "请求过期,请重试", nil)
 | 
			
		||||
	}
 | 
			
		||||
	util.DeleteSession(c, "qq_login_secret")
 | 
			
		||||
 | 
			
		||||
	// 获取OpenID
 | 
			
		||||
	credential, err := qq.Callback(service.Code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return serializer.Err(serializer.CodeNotSet, "无法获取登录状态", err)
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +31,7 @@ func (service *QQCallbackService) Callback(c *gin.Context, user *model.User) ser
 | 
			
		|||
 | 
			
		||||
	// 如果已登录,则绑定已有用户
 | 
			
		||||
	if user != nil {
 | 
			
		||||
 | 
			
		||||
		if user.OpenID != "" {
 | 
			
		||||
			return serializer.Err(serializer.CodeCallbackError, "您已绑定了QQ账号,请先解除绑定", nil)
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +41,21 @@ func (service *QQCallbackService) Callback(c *gin.Context, user *model.User) ser
 | 
			
		|||
		return serializer.Response{
 | 
			
		||||
			Data: "/setting",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 未登录,尝试查找用户
 | 
			
		||||
	if expectedUser, err := model.GetActiveUserByOpenID(credential.OpenID); err == nil {
 | 
			
		||||
		// 用户绑定了此QQ,设定为登录状态
 | 
			
		||||
		util.SetSession(c, map[string]interface{}{
 | 
			
		||||
			"user_id": expectedUser.ID,
 | 
			
		||||
		})
 | 
			
		||||
		res := serializer.BuildUserResponse(expectedUser)
 | 
			
		||||
		res.Code = 203
 | 
			
		||||
		return res
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		// 无匹配用户,创建新用户
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return serializer.Response{}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue