版本升级

1. 新增(kinit-api):防止密码破解,新增输入5次错误密码,账号锁定,使用过期时间为1天
2. 新增(kinit-api):新增 OpenAuth 开放认证方式
3. 新增(kinit-api,kinit-uni):创建用户时,如果没有头像,则添加默认头像链接
4. 优化(kinit-api):登录接口优化
5. 新增(kinit-api):新增Count计数类
6. 更新(kinit-admin):前端升级到vue-element-plus-admin最新版本1.9.4
This commit is contained in:
ktianc 2023-03-05 22:55:24 +08:00
parent 7fbdcb3b0f
commit ebcdac7796
12 changed files with 183 additions and 97 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "vue-element-plus-admin", "name": "vue-element-plus-admin",
"version": "1.9.2", "version": "1.9.4",
"description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。", "description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。",
"author": "Archer <502431556@qq.com>", "author": "Archer <502431556@qq.com>",
"private": false, "private": false,
@ -25,28 +25,28 @@
}, },
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1", "@amap/amap-jsapi-loader": "^1.0.1",
"@iconify/iconify": "^3.0.1", "@iconify/iconify": "^3.1.0",
"@kjgl77/datav-vue3": "^1.4.2", "@kjgl77/datav-vue3": "^1.4.2",
"@vueuse/core": "^9.10.0", "@vueuse/core": "^9.13.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^2.1.0", "@zxcvbn-ts/core": "^2.2.1",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.2.2", "axios": "^1.3.4",
"echarts": "^5.4.1", "echarts": "^5.4.1",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.2.28", "element-plus": "2.2.32",
"intro.js": "^6.0.0", "intro.js": "^6.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "2.0.29", "pinia": "^2.0.32",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"qs": "^6.11.0", "qs": "^6.11.0",
"url": "^0.11.0", "url": "^0.11.0",
"vue": "3.2.45", "vue": "3.2.47",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue-types": "^5.0.2", "vue-types": "^5.0.2",
@ -55,48 +55,48 @@
"web-storage-cache": "^1.1.1" "web-storage-cache": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.2", "@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.2", "@commitlint/config-conventional": "^17.4.4",
"@iconify/json": "^2.2.7", "@iconify/json": "^2.2.29",
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.2",
"@purge-icons/generated": "^0.9.0", "@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.0", "@types/intro.js": "^5.1.1",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
"@types/node": "^18.11.18", "@types/node": "^18.14.2",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.48.1", "@typescript-eslint/parser": "^5.54.0",
"@vitejs/plugin-legacy": "^3.0.1", "@vitejs/plugin-legacy": "^4.0.1",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"consola": "^2.15.3", "consola": "^2.15.3",
"eslint": "^8.32.0", "eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.6.0",
"eslint-define-config": "^1.14.0", "eslint-define-config": "^1.15.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0", "eslint-plugin-vue": "^9.9.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"less": "^4.1.3", "less": "^4.1.3",
"lint-staged": "^13.1.0", "lint-staged": "^13.1.2",
"plop": "^3.1.1", "plop": "^3.1.2",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"prettier": "^2.8.3", "prettier": "^2.8.4",
"rimraf": "^4.0.7", "rimraf": "^4.1.2",
"rollup": "^3.10.0", "rollup": "^3.17.3",
"stylelint": "^14.16.1", "stylelint": "^15.2.0",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.4", "stylelint-config-prettier": "^9.0.5",
"stylelint-config-recommended": "^9.0.0", "stylelint-config-recommended": "^10.0.1",
"stylelint-config-standard": "^29.0.0", "stylelint-config-standard": "^30.0.1",
"stylelint-order": "^6.0.1", "stylelint-order": "^6.0.2",
"terser": "^5.16.1", "terser": "^5.16.5",
"typescript": "4.9.4", "typescript": "4.9.5",
"vite": "4.0.4", "vite": "4.1.4",
"vite-plugin-ejs": "^1.6.4", "vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
@ -105,7 +105,7 @@
"vite-plugin-style-import": "2.0.0", "vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-windicss": "^1.8.10", "vite-plugin-windicss": "^1.8.10",
"vue-tsc": "^1.0.24", "vue-tsc": "^1.2.0",
"windicss": "^3.5.6", "windicss": "^3.5.6",
"windicss-analysis": "^0.3.5" "windicss-analysis": "^0.3.5"
}, },

View File

@ -107,7 +107,13 @@ const toggleClick = () => {
v-bind="getBindItemValue(item)" v-bind="getBindItemValue(item)"
> >
<template #label> <template #label>
<slot :name="`${item.field}-label`" :label="item.label">{{ item.label }}</slot> <slot
:name="`${item.field}-label`"
:row="{
label: item.label
}"
>{{ item.label }}</slot
>
</template> </template>
<template #default> <template #default>

View File

@ -412,7 +412,10 @@ watch(
{ {
icon: 'ant-design:close-outlined', icon: 'ant-design:close-outlined',
label: t('common.closeTab'), label: t('common.closeTab'),
disabled: !!visitedViews?.length && selectedTag?.meta.affix disabled: !!visitedViews?.length && selectedTag?.meta.affix,
command: () => {
closeSelectedTag(selectedTag!)
}
}, },
{ {
divided: true, divided: true,

View File

@ -39,3 +39,9 @@ const themeChange = (val: boolean) => {
@change="themeChange" @change="themeChange"
/> />
</template> </template>
<style lang="less" scoped>
:deep(.el-switch__core .el-switch__inner .is-icon) {
overflow: visible;
}
</style>

View File

@ -214,7 +214,6 @@ const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): For
for (const task of formRequestTask) { for (const task of formRequestTask) {
task() task()
} }
console.log(formSchema)
return formSchema return formSchema
} }
@ -243,7 +242,7 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
// 给options添加国际化 // 给options添加国际化
const filterOptions = (options: Recordable, labelField?: string) => { const filterOptions = (options: Recordable, labelField?: string) => {
return options.map((v: Recordable) => { return options?.map((v: Recordable) => {
if (labelField) { if (labelField) {
v['labelField'] = t(v.labelField) v['labelField'] = t(v.labelField)
} else { } else {

View File

@ -1,2 +1,8 @@
@import './var.css'; @import './var.css';
@import 'element-plus/theme-chalk/dark/css-vars.css'; @import 'element-plus/theme-chalk/dark/css-vars.css';
// 解决抽屉弹出时body宽度变化的问题
.el-popup-parent--hidden {
width: 100% !important;
}

View File

@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
""" """
系统版本 系统版本
""" """
VERSION = "1.6.0" VERSION = "1.6.1"
"""安全警告: 不要在生产中打开调试运行!""" """安全警告: 不要在生产中打开调试运行!"""
DEBUG = True DEBUG = True
@ -105,6 +105,10 @@ EVENTS = [
""" """
# 默认密码,"0" 默认为手机号后六位 # 默认密码,"0" 默认为手机号后六位
DEFAULT_PASSWORD = "0" DEFAULT_PASSWORD = "0"
# 默认头像
DEFAULT_AVATAR = "https://vv-reserve.oss-cn-hangzhou.aliyuncs.com/avatar/2023-01-27/1674820804e81e7631.png"
# 默认登陆时最大输入密码或验证码错误次数
DEFAULT_AUTH_ERROR_MAX_NUMBER = 5
# 是否开启保存登录日志 # 是否开启保存登录日志
LOGIN_LOG_RECORD = not DEBUG LOGIN_LOG_RECORD = not DEBUG
# 是否开启保存每次请求日志到本地 # 是否开启保存每次请求日志到本地

View File

@ -60,6 +60,7 @@ class UserDal(DalBase):
raise CustomException("手机号已存在!", code=status.HTTP_ERROR) raise CustomException("手机号已存在!", code=status.HTTP_ERROR)
password = data.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD password = data.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD
data.password = self.model.get_password_hash(password) data.password = self.model.get_password_hash(password)
data.avatar = data.avatar if data.avatar else settings.DEFAULT_AVATAR
obj = self.model(**data.dict(exclude={'role_ids'})) obj = self.model(**data.dict(exclude={'role_ids'}))
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", data.role_ids), v_return_objs=True) roles = await RoleDal(self.db).get_datas(limit=0, id=("in", data.role_ids), v_return_objs=True)
for role in roles: for role in roles:

View File

@ -6,6 +6,8 @@
# @desc : 获取认证后的信息工具 # @desc : 获取认证后的信息工具
from typing import List, Optional from typing import List, Optional
from jose import jwt, JWTError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from apps.vadmin.auth.crud import UserDal from apps.vadmin.auth.crud import UserDal
@ -33,6 +35,61 @@ def get_user_permissions(user: VadminUser) -> set:
return permissions return permissions
class OpenAuth(AuthValidation):
"""
开放认证无认证也可以访问
认证了以后可以获取到用户信息无认证则获取不到
"""
@classmethod
def validate_token(cls, token: str | None, db: AsyncSession) -> str | None:
"""
验证用户 token没有则返回 None
"""
if not token:
return None
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
telephone: str = payload.get("sub")
if telephone is None:
return None
except JWTError:
return None
return telephone
@classmethod
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession) -> Auth:
"""
验证用户信息
"""
if user is None:
return Auth(db=db)
elif not user.is_active:
return Auth(db=db)
request.scope["telephone"] = user.telephone
try:
request.scope["body"] = await request.body()
except RuntimeError:
request.scope["body"] = "获取失败"
return Auth(user=user, db=db)
async def __call__(
self,
request: Request,
token: str = Depends(settings.oauth2_scheme),
db: AsyncSession = Depends(db_getter)
):
"""
每次调用依赖此类的接口会执行该方法
"""
telephone = self.validate_token(token, db)
if telephone:
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
return await self.validate_user(request, user, db)
return Auth(db=db)
class AllUserAuth(AuthValidation): class AllUserAuth(AuthValidation):
""" """

View File

@ -39,77 +39,66 @@ from core.data_types import Telephone
app = APIRouter() app = APIRouter()
@app.post("/login/", summary="手机号密码登录") @app.post("/login/", summary="手机号密码登录", description="员工登录通道限制最多输错次数达到最大值后将is_active=False")
async def login_for_access_token( async def login_for_access_token(
request: Request, request: Request,
data: LoginForm, data: LoginForm,
manage: LoginManage = Depends(), manage: LoginManage = Depends(),
db: AsyncSession = Depends(db_getter) db: AsyncSession = Depends(db_getter)
): ):
if data.method == "0": try:
result = await manage.password_login(data, db, request) if data.method == "0":
elif data.method == "1": result = await manage.password_login(data, db, request)
result = await manage.sms_login(data, db, request) elif data.method == "1":
else: result = await manage.sms_login(data, db, request)
return ErrorResponse(msg="请使用正确的登录方式") else:
if not result.status: raise ValueError("无效参数")
resp = {"message": result.msg}
await VadminLoginRecord.create_login_record(db, data, result.status, request, resp)
return ErrorResponse(msg=result.msg)
user = result.user
if data.platform in ["0", "1"] and not user.is_staff: if not result.status:
msg = "此手机号无登录权限" raise ValueError(result.msg)
await VadminLoginRecord.create_login_record(db, data, result.status, request, {"message": msg})
return ErrorResponse(msg=msg)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires) token = LoginManage.create_access_token(data={"sub": result.user.telephone}, expires_delta=token_expires)
resp = { resp = {
"access_token": access_token, "access_token": token,
"token_type": "bearer", "token_type": "bearer",
"is_reset_password": user.is_reset_password, "is_reset_password": result.user.is_reset_password,
"is_wx_server_openid": user.is_wx_server_openid "is_wx_server_openid": result.user.is_wx_server_openid
} }
await VadminLoginRecord.create_login_record(db, data, result.status, request, resp) await VadminLoginRecord.create_login_record(db, data, True, request, resp)
return SuccessResponse(resp) return SuccessResponse(resp)
except ValueError as e:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
return ErrorResponse(msg=str(e))
@app.post("/wx/login/", summary="微信服务端一键登录") @app.post("/wx/login/", summary="微信服务端一键登录", description="员工登录通道")
async def wx_login_for_access_token(request: Request, data: WXLoginForm, db: AsyncSession = Depends(db_getter)): async def wx_login_for_access_token(request: Request, data: WXLoginForm, db: AsyncSession = Depends(db_getter)):
if data.platform not in ["0", "1"]: try:
msg = "错误平台" if data.platform != "1" or data.method != "2":
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg}) raise ValueError("无效参数")
return ErrorResponse(msg=msg) wx = WXOAuth(request.app.state.redis, 0)
wx = WXOAuth(request.app.state.redis, 0) telephone = await wx.parsing_phone_number(data.code)
telephone = await wx.parsing_phone_number(data.code) if not telephone:
if not telephone: raise ValueError("无效Code")
msg = "无效Code" data.telephone = telephone
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg}) user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
return ErrorResponse(msg=msg) if not user:
data.telephone = telephone raise ValueError("此手机号不存在")
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True) elif not user.is_active:
msg = None raise ValueError("此手机号已被冻结")
if not user: except ValueError as e:
# 手机号不存在,创建新用户 await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
# model = UserIn(name=generate_string(), telephone=Telephone(telephone)) return ErrorResponse(msg=str(e))
# user = await UserDal(db).create_data(model, v_return_obj=True)
msg = "手机号不存在!"
elif not user.is_active:
msg = "此手机号已被冻结!"
elif data.platform in ["0", "1"] and not user.is_staff:
msg = "此手机号无登录权限"
if msg:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
return ErrorResponse(msg=msg)
# 更新登录时间 # 更新登录时间
await user.update_login_info(db, request.client.host) await user.update_login_info(db, request.client.host)
# 登录成功创建 token # 登录成功创建 token
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires) token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=token_expires)
resp = { resp = {
"access_token": access_token, "access_token": token,
"token_type": "bearer", "token_type": "bearer",
"is_reset_password": user.is_reset_password, "is_reset_password": user.is_reset_password,
"is_wx_server_openid": user.is_wx_server_openid "is_wx_server_openid": user.is_wx_server_openid

View File

@ -9,10 +9,13 @@
from fastapi import Request, Depends from fastapi import Request, Depends
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from application.settings import DEFAULT_AUTH_ERROR_MAX_NUMBER
from apps.vadmin.auth import models, crud, schemas from apps.vadmin.auth import models, crud, schemas
from core.database import db_getter from core.database import db_getter
from core.validator import vali_telephone from core.validator import vali_telephone
from typing import Optional from typing import Optional
from utils.count import Count
class LoginForm(BaseModel): class LoginForm(BaseModel):
@ -52,8 +55,8 @@ class LoginValidation:
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult: async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
self.result = LoginResult() self.result = LoginResult()
if data.platform not in ["0", "1"]: if data.platform not in ["0", "1"] or data.method not in ["0", "1"]:
self.result.msg = "错误平台" self.result.msg = "无效参数"
return self.result return self.result
user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True) user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True)
if not user: if not user:
@ -62,11 +65,23 @@ class LoginValidation:
result = await self.func(self, data=data, user=user, request=request) result = await self.func(self, data=data, user=user, request=request)
count_key = f"{data.telephone}_password_auth" if data.method == '0' else f"{data.telephone}_sms_auth"
count = Count(request.app.state.redis, count_key)
if not result.status: if not result.status:
self.result.msg = result.msg self.result.msg = result.msg
number = await count.add(ex=86400)
if number >= DEFAULT_AUTH_ERROR_MAX_NUMBER:
await count.reset()
# 如果等于最大次数,那么就将用户 is_active=False
user.is_active = False
await db.flush()
elif not user.is_active: elif not user.is_active:
self.result.msg = "此手机号已被冻结!" self.result.msg = "此手机号已被冻结!"
elif user: elif data.platform in ["0", "1"] and not user.is_staff:
self.result.msg = "此手机号无权限!"
else:
await count.delete()
self.result.msg = "OK" self.result.msg = "OK"
self.result.status = True self.result.status = True
self.result.user = schemas.UserSimpleOut.from_orm(user) self.result.user = schemas.UserSimpleOut.from_orm(user)

View File

@ -107,7 +107,7 @@ const actions = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getInfo().then(res => { getInfo().then(res => {
const user = res.data const user = res.data
const avatar = (user == null || user.avatar == "" || user.avatar == null) ? require("@/static/images/avatar.jpg") : user.avatar const avatar = (user == null || user.avatar == "" || user.avatar == null) ? "https://vv-reserve.oss-cn-hangzhou.aliyuncs.com/avatar/2023-01-27/1674820804e81e7631.png" : user.avatar
const name = (user == null || user.name == "" || user.name == null) ? "" : user.name const name = (user == null || user.name == "" || user.name == null) ? "" : user.name
commit('SET_ROLES', user.roles.map((item) => item.name) || ['ROLE_DEFAULT']) commit('SET_ROLES', user.roles.map((item) => item.name) || ['ROLE_DEFAULT'])
commit('SET_PERMISSIONS', user.permissions) commit('SET_PERMISSIONS', user.permissions)