web: Support security for login

This commit is contained in:
qianmoQ 2022-10-19 22:32:33 +08:00
parent 89f8e2fd55
commit 61c273f375
19 changed files with 338 additions and 108 deletions

View File

@ -11,7 +11,8 @@ public enum ServiceState
USER_NOT_FOUND(4001, "User dose not exists"),
USER_ROLE_NOT_FOUND(4002, "User role dose not exists"),
USER_UNAUTHORIZED(4003, "Insufficient current user permissions"),
USER_EXISTS(4004, "User exists");
USER_EXISTS(4004, "User exists"),
USER_BAD_CREDENTIALS(4005, "The account or password is incorrect");
private Integer code;
private String value;

View File

@ -4,6 +4,7 @@ import io.edurt.datacap.server.common.JSON;
import io.edurt.datacap.server.common.Response;
import io.edurt.datacap.server.common.ServiceState;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@ -24,6 +25,11 @@ public class JwtAuthEntryPoint
throws IOException
{
log.error("Unauthorized error: {}", authException.getMessage());
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_UNAUTHORIZED)));
if (authException instanceof BadCredentialsException) {
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_BAD_CREDENTIALS)));
}
else {
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_UNAUTHORIZED)));
}
}
}

View File

@ -0,0 +1,3 @@
export default {
token: 'AuthToken'
}

View File

@ -1,27 +1,37 @@
import { ResponseModel } from "@/model/ResponseModel";
import { message } from "ant-design-vue";
import {ResponseModel} from "@/model/ResponseModel";
import {message} from "ant-design-vue";
import axios from 'axios';
import Common from "@/common/Common";
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
const configure = {
headers: {
'Content-Type': 'application/json'
},
cancelToken: undefined
}
export class HttpCommon
{
private configure;
export class HttpCommon {
constructor() {
constructor()
{
if (process.env.NODE_ENV === 'development' ||
window.location.hostname === 'localhost') {
axios.defaults.baseURL = 'http://localhost:9096';
} else {
}
else {
axios.defaults.baseURL = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
}
const auth = JSON.parse(localStorage.getItem(Common.token) || '{}');
this.configure = {
headers: {
'Content-Type': 'application/json',
'Authorization': auth.type + ' ' + auth.token
},
cancelToken: undefined,
params: undefined
}
}
handlerSuccessful(result: any): ResponseModel {
handlerSuccessful(result: any): ResponseModel
{
const data = result.data;
let message = data.message;
if (data.message instanceof Array) {
@ -40,7 +50,8 @@ export class HttpCommon {
return response;
}
handlerFailed(error: any): ResponseModel {
handlerFailed(error: any): ResponseModel
{
const response: ResponseModel = {
code: 0,
message: error.message,
@ -49,9 +60,11 @@ export class HttpCommon {
return response;
}
get(url: string, params?: any): Promise<ResponseModel> {
get(url: string, params?: any): Promise<ResponseModel>
{
return new Promise((resolve) => {
axios.get(url, { params: params })
this.configure.params = params;
axios.get(url, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -61,10 +74,11 @@ export class HttpCommon {
});
}
post(url: string, data = {}, cancelToken?: any): Promise<ResponseModel> {
post(url: string, data = {}, cancelToken?: any): Promise<ResponseModel>
{
return new Promise((resolve) => {
configure.cancelToken = cancelToken;
axios.post(url, data, configure)
this.configure.cancelToken = cancelToken;
axios.post(url, data, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -74,9 +88,10 @@ export class HttpCommon {
});
}
put(url: string, data = {}): Promise<ResponseModel> {
put(url: string, data = {}): Promise<ResponseModel>
{
return new Promise((resolve) => {
axios.put(url, data, configure)
axios.put(url, data, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -86,9 +101,10 @@ export class HttpCommon {
});
}
delete(url: string): Promise<ResponseModel> {
delete(url: string): Promise<ResponseModel>
{
return new Promise((resolve) => {
axios.delete(url)
axios.delete(url, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -98,7 +114,8 @@ export class HttpCommon {
});
}
getAxios() {
getAxios()
{
return axios;
}
}

View File

@ -1,11 +0,0 @@
const en = {
common: {
home: 'Home',
query: 'Query',
admin: 'Admin',
source: 'Source',
history: 'History'
}
}
export default en;

View File

@ -0,0 +1,13 @@
export default {
home: 'Home',
query: 'Query',
admin: 'Admin',
source: 'DataSource',
history: 'History',
username: 'Username',
password: 'Password',
login: 'Login',
logout: 'Logout',
english: 'English',
chinese: 'Chinese'
}

View File

@ -0,0 +1,9 @@
import enGB from 'ant-design-vue/es/locale/en_GB'
import common from "@/i18n/langs/en/common";
import required from "@/i18n/langs/en/required";
export default {
...enGB,
common: common,
required: required
}

View File

@ -0,0 +1,4 @@
export default {
username: 'Please input your username!',
password: 'Please input your password!'
}

View File

@ -1,15 +1,7 @@
import en from "@/i18n/langs/en";
import zhCn from "@/i18n/langs/zhCn";
import enGB from 'ant-design-vue/es/locale/en_GB';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import en from "@/i18n/langs/en/index";
import zhCn from "@/i18n/langs/zhCn/index";
export default {
en: {
...en,
...enGB,
},
zh_cn: {
...zhCn,
...zhCN,
}
en: en,
zh_cn: zhCn
}

View File

@ -1,11 +0,0 @@
const zhCn = {
common: {
home: '主页',
query: '查询',
admin: '管理',
source: '源',
history: '历史'
}
}
export default zhCn;

View File

@ -0,0 +1,13 @@
export default {
home: '主页',
query: '查询',
admin: '管理',
source: '数据源',
history: '历史',
username: '用户名',
password: '密码',
login: '登录',
logout: '注销',
english: '英语',
chinese: '中文'
}

View File

@ -0,0 +1,9 @@
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import common from "@/i18n/langs/zhCn/common";
import required from "@/i18n/langs/zhCn/required";
export default {
...zhCN,
common: common,
required: required
}

View File

@ -0,0 +1,4 @@
export default {
username: '请输入用户名!',
password: '请输入密码!'
}

View File

@ -0,0 +1,8 @@
export interface AuthResponse
{
token: string;
type: string;
id: number;
username: string;
roles: [];
}

View File

@ -0,0 +1,8 @@
export interface AuthUser
{
username: string;
password: string;
// Marks the error message returned after an operation
message?: string;
loading?: boolean;
}

View File

@ -1,8 +1,9 @@
import LayoutContainer from "@/views/layout/Layout.vue";
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import {createRouter, createWebHashHistory, RouteRecordRaw} from "vue-router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import Common from "@/common/Common";
NProgress.configure({
easing: 'ease',
@ -25,6 +26,9 @@ const routes: Array<RouteRecordRaw> = [
name: "dashboard",
redirect: "/dashboard/index",
component: LayoutContainer,
meta: {
requireAuth: true,
},
children: [
{
path: "index",
@ -37,6 +41,9 @@ const routes: Array<RouteRecordRaw> = [
name: "console",
redirect: "/console/index",
component: LayoutContainer,
meta: {
requireAuth: true,
},
children: [
{
path: "index",
@ -48,6 +55,9 @@ const routes: Array<RouteRecordRaw> = [
path: "/admin",
name: "admin",
component: LayoutContainer,
meta: {
requireAuth: true,
},
children: [
{
path: "source",
@ -69,6 +79,17 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("../views/common/NotFound.vue")
}
]
},
{
path: "/auth",
name: "auth",
children: [
{
name: "signin",
path: "signin",
component: () => import("../views/pages/auth/AuthLogin.vue")
}
]
}
];
@ -77,13 +98,30 @@ const router = createRouter({
routes
});
const authRouter = '/auth/signin';
router.beforeEach((to, from, next) => {
NProgress.start();
if (to.matched.length === 0) {
next({ name: "routerNotFound" })
next({name: "routerNotFound"})
}
else {
next();
if (to.meta.requireAuth) {
if (localStorage.getItem(Common.token)) {
next();
}
else {
next(authRouter);
}
}
else {
if (localStorage.getItem(Common.token) && to.path == authRouter) {
next('/');
}
else {
next();
}
}
}
})

View File

@ -0,0 +1,13 @@
import {ResponseModel} from "@/model/ResponseModel";
import {HttpCommon} from "@/common/HttpCommon";
import {AuthUser} from "@/model/AuthUser";
const defaultAuth = "/api/auth";
export class AuthService
{
signin(configure: AuthUser): Promise<ResponseModel>
{
return new HttpCommon().post(defaultAuth + '/signin', configure);
}
}

View File

@ -1,62 +1,100 @@
<template>
<a-menu theme="dark" mode="horizontal" :style="{ lineHeight: '64px' }">
<a-menu-item>DataCap(incubator)</a-menu-item>
<a-menu-item key="dashboard">
<router-link to="/">
<home-filled/>
{{ $t('common.home') }}
</router-link>
</a-menu-item>
<a-menu-item key="console">
<router-link to="/console/index">
<console-sql-outlined/>
{{ $t('common.query') }}
</router-link>
</a-menu-item>
<a-sub-menu key="admin">
<template #icon>
<setting-outlined/>
</template>
<template #title>{{ $t('common.admin') }}</template>
<a-menu-item key="admin_source">
<router-link to="/admin/source">
<aim-outlined/>
{{ $t('common.source') }}
<div :style="{ clear: 'both' }">
<a-menu theme="dark" mode="horizontal" :style="{lineHeight: '64px', float: 'left', width: '60%'}">
<a-menu-item>DataCap(incubator)</a-menu-item>
<a-menu-item key="dashboard">
<router-link to="/">
<home-filled/>
{{ $t('common.home') }}
</router-link>
</a-menu-item>
<a-menu-item key="admin_history">
<router-link to="/admin/history">
<history-outlined/>
{{ $t('common.history') }}
<a-menu-item key="console">
<router-link to="/console/index">
<console-sql-outlined/>
{{ $t('common.query') }}
</router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="language">
<template #icon>
<a-dropdown placement="bottom">
<template #overlay>
<a-menu>
<a-menu-item key="en" @click="handlerChangeLang('en')">
English
</a-menu-item>
<a-menu-item key="zhCN" @click="handlerChangeLang('zh_cn')">
中文
</a-menu-item>
</a-menu>
</template>
<translation-outlined/>
</a-dropdown>
</template>
</a-sub-menu>
</a-menu>
<a-sub-menu key="admin">
<template #icon>
<setting-outlined/>
</template>
<template #title>{{ $t('common.admin') }}</template>
<a-menu-item key="admin_source">
<router-link to="/admin/source">
<aim-outlined/>
{{ $t('common.source') }}
</router-link>
</a-menu-item>
<a-menu-item key="admin_history">
<router-link to="/admin/history">
<history-outlined/>
{{ $t('common.history') }}
</router-link>
</a-menu-item>
</a-sub-menu>
</a-menu>
<a-menu theme="dark" mode="horizontal" :style="{lineHeight: '64px', float: 'right'}">
<a-sub-menu key="language">
<template #icon>
<a-dropdown placement="bottom">
<template #overlay>
<a-menu style="margin-top: 5px;">
<a-menu-item key="en" @click="handlerChangeLang('en')">
{{ $t('common.english') }}
</a-menu-item>
<a-menu-item key="zhCN" @click="handlerChangeLang('zh_cn')">
{{ $t('common.chinese') }}
</a-menu-item>
</a-menu>
</template>
<translation-outlined/>
</a-dropdown>
</template>
</a-sub-menu>
<a-sub-menu>
<template #icon>
<a-dropdown placement="bottomRight">
<template #overlay>
<a-menu style="margin-top: 5px;">
<a-menu-item @click="handlerLogout">
{{ $t('common.logout') }}
</a-menu-item>
</a-menu>
</template>
<a-avatar style="background-color: #87d068">{{ username }}</a-avatar>
</a-dropdown>
</template>
</a-sub-menu>
</a-menu>
</div>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import {AimOutlined, HomeFilled, SettingOutlined} from '@ant-design/icons-vue';
import Common from "@/common/Common";
import {AuthResponse} from "@/model/AuthResponse";
import router from "@/router";
export default defineComponent({
name: "LayoutHeader",
setup()
{
let username;
const authUser = JSON.parse(localStorage.getItem(Common.token) || '{}') as AuthResponse;
if (authUser) {
username = authUser.username;
}
const handlerLogout = () => {
localStorage.removeItem(Common.token);
router.push('/auth/signin')
}
return {
username,
handlerLogout
}
},
components: {HomeFilled, SettingOutlined, AimOutlined},
computed: {},
methods: {

View File

@ -0,0 +1,76 @@
<template>
<div class="main">
<a-row :gutter="[8,8]">
<a-col :span="8"/>
<a-col :span="8">
<a-result :title="'DataCap ' + $t('common.login')">
<template #icon>
<smile-twoTone/>
</template>
<template #extra>
<a-alert v-if="formState.message" :message="formState.message" type="error" show-icon style="margin-bottom: 10px;"/>
<a-form :model="formState" name="basic" :label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }" @finish="handlerAuthLogin">
<a-form-item :label="$t('common.username')" name="username"
:rules="[{ required: true, message: $t('required.username') }]">
<a-input v-model:value="formState.username"/>
</a-form-item>
<a-form-item :label="$t('common.password')" name="password"
:rules="[{ required: true, message: $t('required.password') }]">
<a-input-password v-model:value="formState.password"/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 6, span: 16 }">
<a-button :disabled="disabled" :loading="formState.loading" type="primary" html-type="submit">{{ $t('common.login') }}</a-button>
</a-form-item>
</a-form>
</template>
</a-result>
</a-col>
<a-col :span="8"/>
</a-row>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, reactive} from 'vue';
import {AuthUser} from "@/model/AuthUser";
import {AuthService} from "@/services/AuthService";
import Common from "@/common/Common";
import router from "@/router";
export default defineComponent({
setup()
{
const formState = reactive<AuthUser>({
username: '',
password: ''
});
const disabled = computed(() => {
return !(formState.username && formState.password);
});
const handlerAuthLogin = (values: any) => {
formState.loading = true;
new AuthService().signin(values)
.then(response => {
if (response.status) {
localStorage.setItem(Common.token, JSON.stringify(response.data));
router.push('/');
}
else {
formState.message = response.message;
}
})
.finally(() => {
formState.loading = undefined;
});
};
return {
formState,
disabled,
handlerAuthLogin
};
},
});
</script>