perf: 通用模块分出认证模块

This commit is contained in:
TsMask
2025-03-19 11:32:57 +08:00
parent add8b9d581
commit 5040a5ae40
14 changed files with 120 additions and 113 deletions

View File

@@ -0,0 +1,203 @@
package controller
import (
"fmt"
"strings"
"be.ems/src/framework/config"
"be.ems/src/framework/constants"
"be.ems/src/framework/i18n"
"be.ems/src/framework/reqctx"
"be.ems/src/framework/resp"
"be.ems/src/framework/token"
"be.ems/src/modules/auth/model"
"be.ems/src/modules/auth/service"
systemModelVO "be.ems/src/modules/system/model/vo"
systemService "be.ems/src/modules/system/service"
"github.com/gin-gonic/gin"
)
// 实例化控制层 AccountController 结构体
var NewAccount = &AccountController{
accountService: service.NewAccount,
sysLogLoginService: systemService.NewSysLogLogin,
}
// 账号身份操作处理
//
// PATH /
type AccountController struct {
accountService *service.Account // 账号身份操作服务
sysLogLoginService *systemService.SysLogLogin // 系统登录访问
}
// Login 系统登录
//
// POST /login
//
// @Tags common/authorization
// @Accept json
// @Produce json
// @Param data body object true "Request Param"
// @Success 200 {object} object "Response Results"
// @Summary System Login
// @Description System Login
// @Router /login [post]
func (s AccountController) Login(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
var body model.LoginBody
if err := c.ShouldBindJSON(&body); err != nil {
errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err))
c.JSON(422, resp.CodeMsg(40422, errMsgs))
return
}
// 当前请求信息
ipaddr, location := reqctx.IPAddrLocation(c)
os, browser := reqctx.UaOsBrowser(c)
// 校验验证码 根据错误信息,创建系统访问记录
if err := s.accountService.ValidateCaptcha(body.Code, body.UUID); err != nil {
msg := fmt.Sprintf("%s code: %s", err.Error(), body.Code)
s.sysLogLoginService.Insert(
body.Username, constants.STATUS_NO, msg,
[4]string{ipaddr, location, os, browser},
)
c.JSON(400, resp.CodeMsg(40012, i18n.TKey(language, err.Error())))
return
}
// 登录用户信息
loginUser, err := s.accountService.ByUsername(body.Username, body.Password)
if err != nil {
c.JSON(200, resp.ErrMsg(i18n.TKey(language, err.Error())))
return
}
// 生成令牌,创建系统访问记录
tokenStr := token.Create(&loginUser, [4]string{ipaddr, location, os, browser})
if tokenStr == "" {
c.JSON(200, resp.Err(nil))
return
} else {
s.accountService.UpdateLoginDateAndIP(loginUser)
// 登录成功
s.sysLogLoginService.Insert(
body.Username, constants.STATUS_YES, "app.common.loginSuccess",
[4]string{ipaddr, location, os, browser},
)
}
c.JSON(200, resp.OkData(map[string]any{
"accessToken": tokenStr,
"tokenType": strings.TrimRight(constants.HEADER_PREFIX, " "),
"expiresIn": (loginUser.ExpireTime - loginUser.LoginTime) / 1000,
"userId": loginUser.UserId,
}))
}
// Me 登录用户信息
//
// GET /me
//
// @Tags common/authorization
// @Accept json
// @Produce json
// @Success 200 {object} object "Response Results"
// @Security TokenAuth
// @Summary Login User Information
// @Description Login User Information
// @Router /me [get]
func (s AccountController) Me(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
info, err := reqctx.LoginUser(c)
if err != nil {
c.JSON(401, resp.CodeMsg(40003, err.Error()))
return
}
// 角色权限集合,系统管理员拥有所有权限
isSystemUser := config.IsSystemUser(info.UserId)
roles, perms := s.accountService.RoleAndMenuPerms(info.UserId, isSystemUser)
info.User.NickName = i18n.TKey(language, info.User.NickName)
info.User.Remark = i18n.TKey(language, info.User.Remark)
info.User.Dept.DeptName = i18n.TKey(language, info.User.Dept.DeptName)
for ri := range info.User.Roles {
info.User.Roles[ri].RoleName = i18n.TKey(language, info.User.Roles[ri].RoleName)
}
c.JSON(200, resp.OkData(map[string]any{
"user": info.User,
"roles": roles,
"permissions": perms,
}))
}
// Router 登录用户路由信息
//
// GET /router
//
// @Tags common/authorization
// @Accept json
// @Produce json
// @Success 200 {object} object "Response Results"
// @Security TokenAuth
// @Summary Login User Routing Information
// @Description Login User Routing Information
// @Router /router [get]
func (s AccountController) Router(c *gin.Context) {
userId := reqctx.LoginUserToUserID(c)
// 前端路由,系统管理员拥有所有
isSystemUser := config.IsSystemUser(userId)
buildMenus := s.accountService.RouteMenus(userId, isSystemUser)
// 闭包函数处理多语言
language := reqctx.AcceptLanguage(c)
var converI18n func(language string, arr *[]systemModelVO.Router)
converI18n = func(language string, arr *[]systemModelVO.Router) {
for i := range *arr {
(*arr)[i].Meta.Title = i18n.TKey(language, (*arr)[i].Meta.Title)
if len((*arr)[i].Children) > 0 {
converI18n(language, &(*arr)[i].Children)
}
}
}
converI18n(language, &buildMenus)
c.JSON(200, resp.OkData(buildMenus))
}
// Logout 系统登出
//
// POST /logout
//
// @Tags common/authorization
// @Accept json
// @Produce json
// @Success 200 {object} object "Response Results"
// @Security TokenAuth
// @Summary System Logout
// @Description System Logout
// @Router /logout [post]
func (s AccountController) Logout(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
tokenStr := reqctx.Authorization(c)
if tokenStr != "" {
// 存在token时记录退出信息
userName := token.Remove(tokenStr)
if userName != "" {
// 当前请求信息
ipaddr, location := reqctx.IPAddrLocation(c)
os, browser := reqctx.UaOsBrowser(c)
// 创建系统访问记录
s.sysLogLoginService.Insert(
userName, constants.STATUS_YES, "app.common.logoutSuccess",
[4]string{ipaddr, location, os, browser},
)
}
}
c.JSON(200, resp.OkMsg(i18n.TKey(language, "app.common.logoutSuccess")))
}

View File

@@ -0,0 +1,182 @@
package controller
import (
"strings"
"be.ems/src/framework/constants"
"be.ems/src/framework/i18n"
"be.ems/src/framework/reqctx"
"be.ems/src/framework/resp"
"be.ems/src/framework/token"
"be.ems/src/framework/utils/machine"
"be.ems/src/framework/utils/regular"
"be.ems/src/modules/auth/service"
systemService "be.ems/src/modules/system/service"
"github.com/gin-gonic/gin"
)
// 实例化控制层 BootloaderController 结构体
var NewBootloader = &BootloaderController{
accountService: service.NewAccount,
sysUserService: systemService.NewSysUser,
}
// 系统引导初始化
//
// PATH /bootloader
type BootloaderController struct {
accountService *service.Account // 账号身份操作服务
sysUserService *systemService.SysUser // 用户信息服务
}
// 首次引导开始
//
// POST /
func (s *BootloaderController) Start(c *gin.Context) {
// 是否完成引导
launchInfo := machine.LaunchInfo
if launchInfo == nil {
c.JSON(200, resp.Err(nil))
return
}
if v, ok := launchInfo[constants.LAUNCH_BOOTLOADER]; ok && !v.(bool) {
c.JSON(200, resp.ErrMsg("bootloader done"))
return
}
// 查询用户登录账号
sysUser := s.sysUserService.FindById(1)
if sysUser.UserId != 1 {
c.JSON(200, resp.ErrMsg("not found user data"))
return
}
// 登录用户信息
loginUser := token.TokenInfo{
UserId: sysUser.UserId,
DeptId: sysUser.DeptId,
User: sysUser,
Permissions: []string{constants.SYS_PERMISSION_SYSTEM},
}
// 当前请求信息
ipaddr, location := reqctx.IPAddrLocation(c)
os, browser := reqctx.UaOsBrowser(c)
// 生成令牌,创建系统访问记录
tokenStr := token.Create(&loginUser, [4]string{ipaddr, location, os, browser})
if tokenStr == "" {
c.JSON(200, resp.Err(nil))
return
} else {
s.accountService.UpdateLoginDateAndIP(loginUser)
}
c.JSON(200, resp.OkData(map[string]any{
"accessToken": tokenStr,
"tokenType": strings.TrimRight(constants.HEADER_PREFIX, " "),
"expiresIn": (loginUser.ExpireTime - loginUser.LoginTime) / 1000,
"userId": loginUser.UserId,
}))
}
// 首次引导完成
//
// PUT /
func (s *BootloaderController) Done(c *gin.Context) {
// 是否完成引导
launchInfo := machine.LaunchInfo
if launchInfo == nil {
c.JSON(200, resp.Err(nil))
return
}
if v, ok := launchInfo[constants.LAUNCH_BOOTLOADER]; ok && !v.(bool) {
c.JSON(200, resp.ErrMsg("bootloader done"))
return
}
// 标记引导完成
if err := machine.Bootloader(false); err != nil {
c.JSON(200, resp.ErrMsg(err.Error()))
return
}
// 清除授权信息
token.Remove(reqctx.Authorization(c))
c.JSON(200, resp.Ok(nil))
}
// 引导系统数据重置
//
// DELETE /
func (s *BootloaderController) Reset(c *gin.Context) {
// 是否完成引导
launchInfo := machine.LaunchInfo
if launchInfo == nil {
c.JSON(200, resp.Err(nil))
return
}
if v, ok := launchInfo[constants.LAUNCH_BOOTLOADER]; ok && v.(bool) {
c.JSON(200, resp.ErrMsg("bootloader not done"))
return
}
if err := machine.Reset(); err != nil {
c.JSON(200, resp.ErrMsg(err.Error()))
return
}
// 清除授权信息
token.Remove(reqctx.Authorization(c))
c.JSON(200, resp.Ok(nil))
}
// 账号变更
//
// PUT /account
func (s *BootloaderController) Account(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
var body struct {
UserName string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, resp.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
if !regular.ValidPassword(body.Password) {
// 登录密码至少包含大小写字母、数字、特殊符号且不少于6位
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "user.errPasswd")))
return
}
// 是否完成引导
launchInfo := machine.LaunchInfo
if launchInfo == nil {
c.JSON(200, resp.Err(nil))
return
}
if v, ok := launchInfo[constants.LAUNCH_BOOTLOADER]; ok && !v.(bool) {
c.JSON(200, resp.ErrMsg("bootloader done"))
return
}
// 查询用户登录账号
sysUser := s.sysUserService.FindById(2)
if sysUser.UserId != 2 {
c.JSON(200, resp.ErrMsg("not found user data"))
return
}
sysUser.UserName = body.UserName
sysUser.NickName = body.UserName
sysUser.Password = body.Password
sysUser.UpdateBy = reqctx.LoginUserToUserName(c)
rows := s.sysUserService.Update(sysUser)
if rows > 0 {
c.JSON(200, resp.Ok(nil))
return
}
c.JSON(200, resp.Err(nil))
}

View File

@@ -0,0 +1,136 @@
package controller
import (
"time"
"be.ems/src/framework/config"
"be.ems/src/framework/constants"
"be.ems/src/framework/database/redis"
"be.ems/src/framework/logger"
"be.ems/src/framework/resp"
"be.ems/src/framework/utils/parse"
systemService "be.ems/src/modules/system/service"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
)
// 实例化控制层 CaptchaController 结构体
var NewCaptcha = &CaptchaController{
sysConfigService: systemService.NewSysConfig,
}
// 验证码操作处理
//
// PATH /
type CaptchaController struct {
sysConfigService *systemService.SysConfig // 参数配置服务
}
// 获取验证码
//
// GET /captchaImage
//
// @Tags common
// @Accept json
// @Produce json
// @Success 200 {object} object "Response Results"
// @Security TokenAuth
// @Summary Get CAPTCHA
// @Description Get CAPTCHA
// @Router /captchaImage [get]
func (s *CaptchaController) Image(c *gin.Context) {
// 从数据库配置获取验证码开关 true开启false关闭
captchaEnabledStr := s.sysConfigService.FindValueByKey("sys.account.captchaEnabled")
captchaEnabled := parse.Boolean(captchaEnabledStr)
if !captchaEnabled {
c.JSON(200, resp.Ok(map[string]any{
"captchaEnabled": captchaEnabled,
}))
return
}
// 生成唯一标识
verifyKey := ""
data := map[string]any{
"captchaEnabled": captchaEnabled,
"uuid": "",
"img": "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
}
// 从数据库配置获取验证码类型 math 数值计算 char 字符验证
captchaType := s.sysConfigService.FindValueByKey("sys.account.captchaType")
if captchaType == constants.CAPTCHA_TYPE_MATH {
math := config.Get("mathCaptcha").(map[string]any)
driverCaptcha := &base64Captcha.DriverMath{
//Height png height in pixel.
Height: math["height"].(int),
// Width Captcha png width in pixel.
Width: math["width"].(int),
//NoiseCount text noise count.
NoiseCount: math["noise"].(int),
//ShowLineOptions := OptionShowHollowLine | OptionShowSlimeLine | OptionShowSineLine .
ShowLineOptions: base64Captcha.OptionShowHollowLine,
}
if math["color"].(bool) {
//BgColor captcha image background color (optional)
driverCaptcha.BgColor = parse.Color(math["background"].(string))
}
// 验证码生成
id, question, answer := driverCaptcha.GenerateIdQuestionAnswer()
// 验证码表达式解析输出
item, err := driverCaptcha.DrawCaptcha(question)
if err != nil {
logger.Infof("Generate Id Question Answer %s %s : %v", captchaType, question, err)
} else {
data["uuid"] = id
data["img"] = item.EncodeB64string()
expiration := constants.CAPTCHA_EXPIRATION * time.Second
verifyKey = constants.CACHE_CAPTCHA_CODE + ":" + id
redis.SetByExpire("", verifyKey, answer, expiration)
}
}
if captchaType == constants.CAPTCHA_TYPE_CHAR {
char := config.Get("charCaptcha").(map[string]any)
driverCaptcha := &base64Captcha.DriverString{
//Height png height in pixel.
Height: char["height"].(int),
// Width Captcha png width in pixel.
Width: char["width"].(int),
//NoiseCount text noise count.
NoiseCount: char["noise"].(int),
//Length random string length.
Length: char["size"].(int),
//Source is a unicode which is the rand string from.
Source: char["chars"].(string),
//ShowLineOptions := OptionShowHollowLine | OptionShowSlimeLine | OptionShowSineLine .
ShowLineOptions: base64Captcha.OptionShowHollowLine,
}
if char["color"].(bool) {
//BgColor captcha image background color (optional)
driverCaptcha.BgColor = parse.Color(char["background"].(string))
}
// 验证码生成
id, question, answer := driverCaptcha.GenerateIdQuestionAnswer()
// 验证码表达式解析输出
item, err := driverCaptcha.DrawCaptcha(question)
if err != nil {
logger.Infof("Generate Id Question Answer %s %s : %v", captchaType, question, err)
} else {
data["uuid"] = id
data["img"] = item.EncodeB64string()
expiration := constants.CAPTCHA_EXPIRATION * time.Second
verifyKey = constants.CACHE_CAPTCHA_CODE + ":" + id
redis.SetByExpire("", verifyKey, answer, expiration)
}
}
// 本地开发下返回验证码结果,方便接口调试
if config.Env() == "local" {
text, _ := redis.Get("", verifyKey)
data["text"] = text
c.JSON(200, resp.Ok(data))
return
}
c.JSON(200, resp.Ok(data))
}

View File

@@ -0,0 +1,93 @@
package controller
import (
"fmt"
"be.ems/src/framework/constants"
"be.ems/src/framework/i18n"
"be.ems/src/framework/reqctx"
"be.ems/src/framework/resp"
"be.ems/src/framework/utils/regular"
"be.ems/src/modules/auth/model"
"be.ems/src/modules/auth/service"
systemService "be.ems/src/modules/system/service"
"github.com/gin-gonic/gin"
)
// 实例化控制层 RegisterController 结构体
var NewRegister = &RegisterController{
registerService: service.NewRegister,
sysLogLoginService: systemService.NewSysLogLogin,
}
// 账号注册操作处理
//
// PATH /
type RegisterController struct {
registerService *service.Register // 账号注册操作服务
sysLogLoginService *systemService.SysLogLogin // 系统登录访问服务
}
// 账号注册
//
// GET /register
func (s *RegisterController) Register(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
var body model.RegisterBody
if err := c.ShouldBindJSON(&body); err != nil {
errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err))
c.JSON(422, resp.CodeMsg(40422, errMsgs))
return
}
// 判断必传参数
if !regular.ValidUsername(body.Username) {
// 账号不能以数字开头可包含大写小写字母数字且不少于5位
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "register.errUsername")))
return
}
if !regular.ValidPassword(body.Password) {
// 登录密码至少包含大小写字母、数字、特殊符号且不少于6位
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "register.errPasswd")))
return
}
if body.Password != body.ConfirmPassword {
// 用户确认输入密码不一致
c.JSON(200, resp.ErrMsg(i18n.TKey(language, "register.errPasswdNotEq")))
return
}
// 当前请求信息
ipaddr, location := reqctx.IPAddrLocation(c)
os, browser := reqctx.UaOsBrowser(c)
// 校验验证码
err := s.registerService.ValidateCaptcha(
body.Code,
body.UUID,
)
// 根据错误信息,创建系统访问记录
if err != nil {
msg := err.Error() + " code: " + body.Code
s.sysLogLoginService.Insert(
body.Username, constants.STATUS_NO, msg,
[4]string{ipaddr, location, os, browser},
)
c.JSON(200, resp.ErrMsg(err.Error()))
return
}
userId, err := s.registerService.ByUserName(body.Username, body.Password)
if err == nil {
msg := i18n.TTemplate(language, "register.successMsg", map[string]any{"name": body.Username, "id": userId})
s.sysLogLoginService.Insert(
body.Username, constants.STATUS_YES, msg,
[4]string{ipaddr, location, os, browser},
)
// 注册成功
c.JSON(200, resp.OkMsg(i18n.TKey(language, "register.success")))
return
}
c.JSON(200, resp.ErrMsg(err.Error()))
}

View File

@@ -0,0 +1,100 @@
package controller
import (
"fmt"
"be.ems/src/framework/config"
"be.ems/src/framework/constants"
"be.ems/src/framework/i18n"
"be.ems/src/framework/reqctx"
"be.ems/src/framework/resp"
"be.ems/src/framework/utils/machine"
systemService "be.ems/src/modules/system/service"
"github.com/gin-gonic/gin"
)
// 实例化控制层 SysConfController 结构体
var NewSysConf = &SysConfController{
sysUserService: systemService.NewSysUser,
sysConfigService: systemService.NewSysConfig,
}
// 系统的配置信息
//
// PATH /sys-conf
type SysConfController struct {
sysUserService *systemService.SysUser // 用户信息服务
sysConfigService *systemService.SysConfig // 参数配置服务
}
// 系统的配置信息
//
// GET /
//
// @Tags common
// @Accept json
// @Produce json
// @Success 200 {object} object "Response Results"
// @Summary Configuration information for the system
// @Description Configuration information for the system
// @Router /sys-conf [get]
func (s SysConfController) Handler(c *gin.Context) {
language := reqctx.AcceptLanguage(c)
infoMap := map[string]string{}
// 获取打包注入的全局变量信息
infoMap["version"] = config.Version
// 系统首次使用标记
launchInfo := machine.LaunchInfo
if launchInfo != nil {
if v, ok := launchInfo[constants.LAUNCH_BOOTLOADER]; ok {
infoMap[constants.LAUNCH_BOOTLOADER] = fmt.Sprint(v)
} else {
infoMap[constants.LAUNCH_BOOTLOADER] = "true"
}
} else {
infoMap[constants.LAUNCH_BOOTLOADER] = "true"
}
// 服务版本
infoMap["serverVersion"] = fmt.Sprint(config.Get("serverVersion"))
// 用户登录认证
infoMap["loginAuth"] = fmt.Sprint(config.Get("serverLoginAuth"))
// 用户接口加密
infoMap["cryptoApi"] = fmt.Sprint(config.Get("serverCryptoApi"))
// 序列号
infoMap["serialNum"] = fmt.Sprint(config.Get("omc.sn"))
// 获取LOGO类型
logoType := s.sysConfigService.FindValueByKey("sys.logo.type")
infoMap["logoType"] = logoType
// 获取LOGO文件
filePathIcon := s.sysConfigService.FindValueByKey("sys.logo.filePathIcon")
infoMap["filePathIcon"] = filePathIcon
filePathBrand := s.sysConfigService.FindValueByKey("sys.logo.filePathBrand")
infoMap["filePathBrand"] = filePathBrand
// 获取系统名称
title := s.sysConfigService.FindValueByKey("sys.title")
infoMap["title"] = i18n.TKey(language, title)
// 获取版权声明
copyright := s.sysConfigService.FindValueByKey("sys.copyright")
infoMap["copyright"] = i18n.TKey(language, copyright)
// 获取是否开启用户注册功能
registerUser := s.sysConfigService.FindValueByKey("sys.account.registerUser")
infoMap["registerUser"] = registerUser
// 获取登录界面背景
loginBackground := s.sysConfigService.FindValueByKey("sys.loginBackground")
infoMap["loginBackground"] = loginBackground
// 系统设置-官网网址
officialUrl := s.sysConfigService.FindValueByKey("sys.officialUrl")
infoMap["officialUrl"] = officialUrl
// 系统设置-系统使用文档
helpDoc := s.sysConfigService.FindValueByKey("sys.helpDoc")
infoMap["helpDoc"] = helpDoc
// 国际化切换
i18nOpen := s.sysConfigService.FindValueByKey("sys.i18n.open")
infoMap["i18nOpen"] = i18nOpen
// 国际化默认语言
i18nDefault := s.sysConfigService.FindValueByKey("sys.i18n.default")
infoMap["i18nDefault"] = i18nDefault
c.JSON(200, resp.OkData(infoMap))
}