feat: 登录实现

This commit is contained in:
ssongliu 2022-08-10 23:45:01 +08:00
parent 44eb1aa0de
commit 37986510d5
15 changed files with 1553 additions and 12660 deletions

View file

@ -7,6 +7,7 @@ import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/init/session"
"github.com/1Panel-dev/1Panel/utils/encrypt"
"github.com/1Panel-dev/1Panel/utils/jwt"
"github.com/gin-gonic/gin"
@ -95,12 +96,15 @@ func (u *UserService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo,
if sID != "" {
c.SetCookie(global.CONF.Session.SessionName, "", -1, "", "", false, false)
}
session, err := global.SESSION.New(c.Request, global.CONF.Session.SessionName)
sessionItem, err := global.SESSION.Get(c.Request, global.CONF.Session.SessionName)
if err != nil {
return nil, err
}
session.Values[global.CONF.Session.SessionUserKey] = user
if err := global.SESSION.Save(c.Request, c.Writer, session); err != nil {
sessionItem.Values[global.CONF.Session.SessionUserKey] = session.SessionUser{
ID: user.ID,
Name: user.Name,
}
if err := global.SESSION.Save(c.Request, c.Writer, sessionItem); err != nil {
return nil, err
}

View file

@ -2,6 +2,7 @@ package i18n
import (
"embed"
"strings"
ginI18n "github.com/gin-contrib/i18n"
"github.com/gin-gonic/gin"
@ -20,10 +21,18 @@ func GetMsg(msg string) string {
}
func GetMsgWithMap(msg string, maps map[string]interface{}) string {
content := ginI18n.MustGetMessage(&i18n.LocalizeConfig{
MessageID: msg,
TemplateData: maps,
})
content := ""
if maps == nil {
content = ginI18n.MustGetMessage(&i18n.LocalizeConfig{
MessageID: msg,
})
} else {
content = ginI18n.MustGetMessage(&i18n.LocalizeConfig{
MessageID: msg,
TemplateData: maps,
})
}
content = strings.ReplaceAll(content, ": <no value>", "")
if content == "" {
return msg
} else {

View file

@ -16,3 +16,8 @@ func Init() {
}
global.SESSION = cs
}
type SessionUser struct {
ID uint `json:"id"`
Name string `json:"name"`
}

View file

@ -1,6 +1,7 @@
package server
import (
"encoding/gob"
"fmt"
"time"
@ -23,6 +24,7 @@ func Start() {
db.Init()
migration.Init()
validator.Init()
gob.Register(session.SessionUser{})
session.Init()
routers := router.Routers()
address := fmt.Sprintf(":%d", global.CONF.System.Port)

View file

@ -49,12 +49,7 @@ func (j *JWT) CreateClaims(baseClaims BaseClaims) CustomClaims {
}
func (j *JWT) CreateToken(request CustomClaims) (string, error) {
request.RegisteredClaims = jwt.RegisteredClaims{
Issuer: global.CONF.JWT.Issuer,
NotBefore: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(global.CONF.JWT.ExpiresTime))),
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, &request)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &request)
return token.SignedString(j.SigningKey)
}

13903
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
// * 后端微服务端口名
export const PORT1 = '/1panel';
export const PORT1 = '9999';
export const PORT2 = '/hooks';

View file

@ -13,7 +13,6 @@ import { ResultData } from '@/api/interface';
import { ResultEnum } from '@/enums/httpEnum';
import { checkStatus } from './helper/checkStatus';
import { ElMessage } from 'element-plus';
import { GlobalStore } from '@/store';
import router from '@/routers';
/**
@ -38,25 +37,13 @@ const config = {
class RequestHttp {
service: AxiosInstance;
public constructor(config: AxiosRequestConfig) {
// 实例化axios
this.service = axios.create(config);
/**
* @description
* -> [] ->
* token校验(JWT) : token,vuex/pinia/
*/
this.service.interceptors.request.use(
(config: AxiosRequestConfig) => {
const globalStore = GlobalStore();
// * 将当前请求添加到 pending 中
axiosCanceler.addPending(config);
// * 如果当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { headers: { noLoading: true } }来控制不显示loading参见loginApi
config.headers!.noLoading || showFullScreenLoading();
const token: string = globalStore.token;
return {
...config,
headers: { ...config.headers, 'x-access-token': token },
};
},
(error: AxiosError) => {
@ -71,14 +58,12 @@ class RequestHttp {
this.service.interceptors.response.use(
(response: AxiosResponse) => {
const { data, config } = response;
const globalStore = GlobalStore();
// * 在请求结束后,移除本次请求,并关闭请求 loading
axiosCanceler.removePending(config);
tryHideFullScreenLoading();
// * 登陆失效code == 599
if (data.code == ResultEnum.OVERDUE) {
ElMessage.error(data.msg);
globalStore.setToken('');
router.replace({
path: '/login',
});
@ -108,20 +93,16 @@ class RequestHttp {
}
// * 常用请求方法封装
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
get(url: string, params?: object, _object = {}): Promise<ResultData> {
return this.service.get(url, { params, ..._object });
}
post<T>(
url: string,
params?: object,
_object = {},
): Promise<ResultData<T>> {
post(url: string, params?: object, _object = {}): Promise<ResultData> {
return this.service.post(url, params, _object);
}
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
put(url: string, params?: object, _object = {}): Promise<ResultData> {
return this.service.put(url, params, _object);
}
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
delete(url: string, params?: any, _object = {}): Promise<ResultData> {
return this.service.delete(url, { params, ..._object });
}
}

View file

@ -1,12 +1,14 @@
// * 请求响应参数(不包含data)
export interface Result {
code: string;
msg: string;
code: number;
message: string;
}
// * 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
data?: T;
export interface ResultData {
code: number;
message: string;
data: any;
}
// * 分页响应参数
@ -31,11 +33,25 @@ export interface CommonModel {
// * 登录模块
export namespace Login {
export interface ReqLoginForm {
username: string;
name: string;
password: string;
captcha: string;
captchaID: string;
authMethod: string;
}
export interface ResLogin {
access_token: string;
code: number;
data: logInfo;
message: string;
}
export interface logInfo {
name: string;
token: string;
}
export interface ResCaptcha {
imagePath: string;
captchaID: string;
captchaLength: number;
}
export interface ResAuthButtons {
[propName: string]: any;

View file

@ -1,31 +1,10 @@
import { Login } from '@/api/interface/index';
import { PORT1 } from '@/api/config/servicePort';
// import qs from 'qs';
import http from '@/api';
/**
* @name
*/
// * 用户登录接口
export const loginApi = (params: Login.ReqLoginForm) => {
console.log(params);
return http.post<Login.ResLogin>(PORT1 + `base/login`, params); // 正常 post json 请求 ==> application/json
// return http.post<Login.ResLogin>(PORT1 + `/login`, {}, { params }); // post 请求携带 query 参数 ==> ?username=admin&password=123456
// return http.post<Login.ResLogin>(PORT1 + `/login`, qs.stringify(params)); // post 请求携带 表单 参数 ==> application/x-www-form-urlencoded
// return http.post<Login.ResLogin>(PORT1 + `/login`, params, {
// headers: { noLoading: true },
// }); // 控制当前请求不显示 loading
// return { data: { access_token: '565656565' } };
return http.post(`/auth/login`, params);
};
// // * 获取按钮权限
// export const getAuthButtons = () => {
// return http.get<Login.ResAuthButtons>(PORT1 + `/auth/buttons`);
// };
// * 获取验证码
export const getCaptcha = () => {
return http.post<Login.ResCaptcha>(PORT1 + `base/captcha`);
return http.get(`/auth/captcha`);
};

View file

@ -20,7 +20,7 @@ router.beforeEach((to, from, next) => {
// * 判断是否有Token
const globalStore = GlobalStore();
if (!globalStore.token) {
if (!globalStore.isLogin) {
next({
path: '/login',
});

View file

@ -4,57 +4,37 @@ import { createPinia } from 'pinia';
import piniaPersistConfig from '@/config/piniaPersist';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
// defineStore 调用后返回一个函数,调用该函数获得 Store 实体
export const GlobalStore = defineStore({
// id: 必须的,在所有 Store 中唯一
id: 'GlobalState',
// state: 返回对象的函数
state: (): GlobalState => ({
// token
token: '',
// userInfo
isLogin: false,
userInfo: '',
// element组件大小
assemblySize: 'default',
// language
language: '',
// themeConfig
themeConfig: {
// 默认 primary 主题颜色
primary: '#409EFF',
// 深色模式
isDark: false,
// 灰色模式
isGrey: false,
// 色弱模式
isWeak: false,
// 面包屑导航
breadcrumb: true,
// 标签页
tabs: false,
// 页脚
footer: true,
},
}),
getters: {},
actions: {
// setToken
setToken(token: string) {
this.token = token;
setLogStatus(login: boolean) {
this.isLogin = login;
},
// setUserInfo
setUserInfo(userInfo: any) {
this.userInfo = userInfo;
},
// setAssemblySizeSize
setAssemblySizeSize(assemblySize: string) {
this.assemblySize = assemblySize;
},
// updateLanguage
updateLanguage(language: string) {
this.language = language;
},
// setThemeConfig
setThemeConfig(themeConfig: ThemeConfigProp) {
this.themeConfig = themeConfig;
},

View file

@ -13,7 +13,7 @@ export interface ThemeConfigProp {
/* GlobalState */
export interface GlobalState {
token: string;
isLogin: boolean;
userInfo: any;
assemblySize: string;
language: string;

View file

@ -1,39 +1,78 @@
<template>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名admin / user">
<template #prefix>
<el-icon class="el-input__icon">
<user />
</el-icon>
</template>
</el-input>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
size="large"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.name"
placeholder="用户名admin / user"
>
<template #prefix>
<el-icon class="el-input__icon">
<user />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
v-model="loginForm.password"
placeholder="密码123456"
show-password
autocomplete="new-password"
>
<template #prefix>
<el-icon class="el-input__icon">
<lock />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-form>
<el-form-item prop="captcha">
<div class="vPicBox">
<el-input
v-model="loginForm.captcha"
placeholder="请输入验证码"
style="width: 60%"
/>
<div class="vPic">
<img
v-if="captcha.imagePath"
:src="captcha.imagePath"
alt="请输入验证码"
@click="loginVerify()"
/>
</div>
</div>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" v-model="loginForm.password" placeholder="密码123456" show-password autocomplete="new-password">
<template #prefix>
<el-icon class="el-input__icon">
<lock />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-form>
<div class="login-btn">
<el-button :icon="CircleClose" round @click="resetForm(loginFormRef)" size="large">重置</el-button>
<el-button :icon="UserFilled" round @click="login(loginFormRef)" size="large" type="primary" :loading="loading">
登录
</el-button>
</div>
<div class="login-btn">
<el-button round @click="resetForm(loginFormRef)" size="large"
>重置</el-button
>
<el-button
round
@click="login(loginFormRef)"
size="large"
type="primary"
:loading="loading"
>
登录
</el-button>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { Login,getCaptcha } from '@/api/interface';
import { Login } from '@/api/interface';
import type { ElForm } from 'element-plus';
import { ElMessage } from 'element-plus';
import { loginApi } from '@/api/modules/login';
import { loginApi, getCaptcha } from '@/api/modules/login';
import { GlobalStore } from '@/store';
import { MenuStore } from '@/store/modules/menu';
@ -50,13 +89,22 @@ const loginRules = reactive({
//
const loginForm = reactive<Login.ReqLoginForm>({
name: '',
password: '',
name: 'admin',
password: 'Songliu123++',
captcha: '',
captchaID: '',
authMethod: '',
});
const captcha = reactive<Login.ResCaptcha>({
captchaID: '',
imagePath: '',
captchaLength: 0,
});
const loading = ref<boolean>(false);
const router = useRouter();
// login
const login = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
@ -64,29 +112,42 @@ const login = (formEl: FormInstance | undefined) => {
loading.value = true;
try {
const requestLoginForm: Login.ReqLoginForm = {
name: loginForm.username,
name: loginForm.name,
password: loginForm.password,
captcha: loginForm.captcha,
captchaID: captcha.captchaID,
authMethod: '',
};
const res = await loginApi(requestLoginForm);
// * token
globalStore.setToken(res.data!.access_token);
// * menulist tabs
globalStore.setUserInfo(res.data.name);
globalStore.setLogStatus(true);
menuStore.setMenuList([]);
ElMessage.success('登录成功!');
router.push({ name: 'home' });
} catch (error) {
loginVerify();
} finally {
loading.value = false;
}
});
};
// resetForm
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
const loginVerify = () => {
getCaptcha().then(async (ele) => {
captcha.imagePath = ele.data?.imagePath ? ele.data.imagePath : '';
captcha.captchaID = ele.data?.captchaID ? ele.data.captchaID : '';
captcha.captchaLength = ele.data?.captchaLength
? ele.data.captchaLength
: 0;
});
};
loginVerify();
onMounted(() => {
// enter
document.onkeydown = (e: any) => {
@ -105,4 +166,20 @@ onMounted(() => {
<style scoped lang="scss">
@import '../index.scss';
.vPicBox {
display: flex;
justify-content: space-between;
width: 100%;
}
.vPic {
width: 33%;
height: 38px;
background: #ccc;
img {
width: 100%;
height: 100%;
vertical-align: middle;
}
}
</style>

View file

@ -36,24 +36,18 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
},
},
},
// server config
server: {
host: '0.0.0.0', // 服务器主机名,如果允许外部访问,可设置为"0.0.0.0"
port: viteEnv.VITE_PORT,
open: viteEnv.VITE_OPEN,
cors: true,
// https: false,
// 代理跨域mock 不需要配置,这里只是个事列)
proxy: {
'/api': {
// target: "https://www.fastmock.site/mock/f81e8333c1a9276214bcdbc170d9e0a0", // fastmock
target: 'https://mock.mengxuegu.com/mock/629d727e6163854a32e8307e', // easymock
target: 'http://localhost:9999',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// plugins
plugins: [
vue(),
createHtmlPlugin({