支持登录

This commit is contained in:
许晓东
2023-05-14 21:25:13 +08:00
parent be8e567684
commit 435a5ca2bc
20 changed files with 575 additions and 29 deletions

View File

@@ -0,0 +1,21 @@
package com.xuxd.kafka.console.beans;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/5/14 19:37
**/
@Data
public class Credentials {
public static final Credentials INVALID = new Credentials();
private String username;
private long expiration;
public boolean isInvalid() {
return this == INVALID;
}
}

View File

@@ -0,0 +1,13 @@
package com.xuxd.kafka.console.beans;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/5/14 20:44
**/
@Data
public class LoginResult {
private String token;
}

View File

@@ -0,0 +1,15 @@
package com.xuxd.kafka.console.beans.dto;
import lombok.Data;
/**
* @author: xuxd
* @date: 2023/5/14 18:59
**/
@Data
public class LoginUserDTO {
private String username;
private String password;
}

View File

@@ -0,0 +1,21 @@
package com.xuxd.kafka.console.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author: xuxd
* @date: 2023/5/9 21:08
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "auth")
public class AuthConfig {
private boolean enable;
private String secret = "kafka-console-ui-default-secret";
private long expireHours;
}

View File

@@ -0,0 +1,36 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.service.AuthService;
import org.springframework.web.bind.annotation.*;
/**
* @author: xuxd
* @date: 2023/5/11 18:54
**/
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthConfig authConfig;
private final AuthService authService;
public AuthController(AuthConfig authConfig, AuthService authService) {
this.authConfig = authConfig;
this.authService = authService;
}
@GetMapping("/enable")
public boolean enable() {
return authConfig.isEnable();
}
@PostMapping("/login")
public ResponseData login(@RequestBody LoginUserDTO userDTO) {
return authService.login(userDTO);
}
}

View File

@@ -2,10 +2,12 @@ package com.xuxd.kafka.console.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import org.apache.ibatis.annotations.Mapper;
/**
* @author: xuxd
* @date: 2023/4/11 21:22
**/
@Mapper
public interface SysUserMapper extends BaseMapper<SysUserDO> {
}

View File

@@ -0,0 +1,64 @@
package com.xuxd.kafka.console.interceptor;
import com.xuxd.kafka.console.beans.Credentials;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.utils.AuthUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author: xuxd
* @date: 2023/5/9 21:20
**/
@Order(1)
@WebFilter(filterName = "auth-filter", urlPatterns = {"/*"})
@Slf4j
public class AuthFilter implements Filter {
private final AuthConfig authConfig;
private final String TOKEN_HEADER = "X-Auth-Token";
private final String AUTH_URI_PREFIX = "/auth";
public AuthFilter(AuthConfig authConfig) {
this.authConfig = authConfig;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (!authConfig.isEnable()) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String accessToken = request.getHeader(TOKEN_HEADER);
String requestURI = request.getRequestURI();
if (requestURI.startsWith(AUTH_URI_PREFIX)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if (StringUtils.isEmpty(accessToken)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
Credentials credentials = AuthUtil.parseToken(authConfig.getSecret(), accessToken);
if (credentials.isInvalid()) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}

View File

@@ -9,6 +9,7 @@ import com.xuxd.kafka.console.utils.ConvertUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import javax.servlet.*;
@@ -24,6 +25,7 @@ import java.util.Set;
* @author xuxd
* @date 2022-01-05 19:56:25
**/
@Order(100)
@WebFilter(filterName = "context-set-filter", urlPatterns = {"/acl/*", "/user/*", "/cluster/*", "/config/*", "/consumer/*", "/message/*", "/topic/*", "/op/*", "/client/*"})
@Slf4j
public class ContextSetFilter implements Filter {

View File

@@ -0,0 +1,13 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
/**
* @author: xuxd
* @date: 2023/5/14 19:00
**/
public interface AuthService {
ResponseData login(LoginUserDTO userDTO);
}

View File

@@ -0,0 +1,54 @@
package com.xuxd.kafka.console.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.Credentials;
import com.xuxd.kafka.console.beans.LoginResult;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dos.SysUserDO;
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
import com.xuxd.kafka.console.config.AuthConfig;
import com.xuxd.kafka.console.dao.SysUserMapper;
import com.xuxd.kafka.console.service.AuthService;
import com.xuxd.kafka.console.utils.AuthUtil;
import com.xuxd.kafka.console.utils.UUIDStrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author: xuxd
* @date: 2023/5/14 19:01
**/
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
private final SysUserMapper userMapper;
private final AuthConfig authConfig;
public AuthServiceImpl(SysUserMapper userMapper, AuthConfig authConfig) {
this.userMapper = userMapper;
this.authConfig = authConfig;
}
@Override
public ResponseData login(LoginUserDTO userDTO) {
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", userDTO.getUsername());
SysUserDO userDO = userMapper.selectOne(queryWrapper);
if (userDO == null) {
return ResponseData.create().failed("用户名/密码不正确");
}
String encrypt = UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt());
if (!userDO.getPassword().equals(encrypt)) {
return ResponseData.create().failed("用户名/密码不正确");
}
Credentials credentials = new Credentials();
credentials.setUsername(userDO.getUsername());
credentials.setExpiration(System.currentTimeMillis() + authConfig.getExpireHours() * 3600 * 1000);
String token = AuthUtil.generateToken(authConfig.getSecret(), credentials);
LoginResult loginResult = new LoginResult();
loginResult.setToken(token);
return ResponseData.create().data(loginResult).success();
}
}

View File

@@ -0,0 +1,54 @@
package com.xuxd.kafka.console.utils;
import com.google.gson.Gson;
import com.xuxd.kafka.console.beans.Credentials;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;
import java.nio.charset.StandardCharsets;
/**
* @author: xuxd
* @date: 2023/5/14 19:34
**/
@Slf4j
public class AuthUtil {
private static Gson gson = GsonUtil.INSTANCE.get();
public static String generateToken(String secret, Credentials info) {
String json = gson.toJson(info);
String str = json + secret;
String signature = MD5Util.md5(str);
return Base64Utils.encodeToString(json.getBytes(StandardCharsets.UTF_8)) + "." +
Base64Utils.encodeToString(signature.getBytes(StandardCharsets.UTF_8));
}
public static boolean isToken(String token) {
return token.split("\\.").length == 2;
}
public static Credentials parseToken(String secret, String token) {
if (!isToken(token)) {
return Credentials.INVALID;
}
String[] arr = token.split("\\.");
String infoStr = new String(Base64Utils.decodeFromString(arr[0]), StandardCharsets.UTF_8);
String signature = new String(Base64Utils.decodeFromString(arr[1]), StandardCharsets.UTF_8);
String encrypt = MD5Util.md5(infoStr + secret);
if (!encrypt.equals(signature)) {
return Credentials.INVALID;
}
try {
Credentials credentials = gson.fromJson(infoStr, Credentials.class);
if (credentials.getExpiration() < System.currentTimeMillis()) {
return Credentials.INVALID;
}
return credentials;
} catch (Exception e) {
log.error("解析token失败: {}", token, e);
return Credentials.INVALID;
}
}
}

View File

@@ -0,0 +1,32 @@
package com.xuxd.kafka.console.utils;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @author: xuxd
* @date: 2023/5/14 20:25
**/
@Slf4j
public class MD5Util {
public static MessageDigest getInstance() {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
return md5;
} catch (NoSuchAlgorithmException e) {
return null;
}
}
public static String md5(String str) {
MessageDigest digest = getInstance();
if (digest == null) {
return null;
}
return new String(digest.digest(str.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
}
}

View File

@@ -47,3 +47,9 @@ logging:
cron:
# clear-dirty-user: 0 * * * * ?
clear-dirty-user: 0 0 1 * * ?
# 权限认证设置设置为true需要先登录才能访问
auth:
enable: true
# 登录用户token的过期时间单位小时
expire-hours: 24

View File

@@ -25,12 +25,12 @@
</div>
</template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import { KafkaClusterApi, AuthApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
import {AUTH, CLUSTER} from "@/store/mutation-types";
export default {
data() {
@@ -39,24 +39,8 @@ export default {
};
},
created() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
this.intAuthState();
this.initClusterInfo();
},
computed: {
...mapState({
@@ -67,7 +51,40 @@ export default {
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
enableAuth: AUTH.ENABLE,
}),
intAuthState() {
request({
url: AuthApi.enable.url,
method: AuthApi.enable.method,
}).then((res) => {
const enable = res;
this.enableAuth(enable);
// if (!enable){
// this.initClusterInfo();
// }
});
},
initClusterInfo() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
},
},
};
</script>

View File

@@ -1,6 +1,7 @@
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import Store from "@/store";
Vue.use(VueRouter);
@@ -53,13 +54,21 @@ const routes = [
path: "/client-quota-page",
name: "ClientQuota",
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"),
import(
/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"
),
},
{
path: "/user-page",
name: "UserManage",
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/user/UserManage.vue"),
import(/* webpackChunkName: "cluster" */ "../views/user/UserManage.vue"),
},
{
path: "/login-page",
name: "Login",
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/login/Login.vue"),
},
];
@@ -70,4 +79,49 @@ const router = new VueRouter({
routes,
});
router.beforeEach((to, from, next) => {
const enableAuth = Store.state.auth.enable;
if (!enableAuth) {
next();
} else {
if (to.path === "/login-page") {
next();
} else {
let token = localStorage.getItem("access_token");
if (token === null || token === "") {
next("/login-page");
} else {
next();
}
}
}
});
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;
VueRouter.prototype.push = function (location, resolve, reject) {
if (resolve && reject) {
originPush.call(this, location, resolve, reject);
} else {
originPush.call(
this,
location,
() => {},
() => {}
);
}
};
VueRouter.prototype.replace = function (location, resolve, reject) {
if (resolve && reject) {
originReplace.call(this, location, resolve, reject);
} else {
originReplace.call(
this,
location,
() => {},
() => {}
);
}
};
export default router;

View File

@@ -1,6 +1,6 @@
import Vue from "vue";
import Vuex from "vuex";
import { CLUSTER } from "@/store/mutation-types";
import { CLUSTER, AUTH } from "@/store/mutation-types";
import { setClusterInfo } from "@/utils/local-cache";
Vue.use(Vuex);
@@ -12,6 +12,9 @@ export default new Vuex.Store({
clusterName: undefined,
enableSasl: false,
},
auth: {
enable: false,
},
},
mutations: {
[CLUSTER.SWITCH](state, clusterInfo) {
@@ -28,6 +31,12 @@ export default new Vuex.Store({
state.clusterInfo.enableSasl = enableSasl;
setClusterInfo(clusterInfo);
},
[AUTH.ENABLE](state, enable) {
state.auth.enable = enable;
},
[AUTH.SET](state, info) {
localStorage.setItem("access_token", info);
},
},
actions: {},
modules: {},

View File

@@ -1,3 +1,8 @@
export const CLUSTER = {
SWITCH: "switchCluster",
};
export const AUTH = {
ENABLE: "enable",
SET: "setToken",
};

View File

@@ -346,4 +346,15 @@ export const UserManageApi = {
url: "/sys/user/manage/user/password",
method: "post",
},
};
export const AuthApi = {
enable: {
url: "/auth/enable",
method: "get",
},
login: {
url: "/auth/login",
method: "post",
},
};

View File

@@ -2,7 +2,7 @@ import axios from "axios";
import notification from "ant-design-vue/es/notification";
import { VueAxios } from "./axios";
import { getClusterInfo } from "@/utils/local-cache";
import Router from "@/router";
// 创建 axios 实例
const request = axios.create({
// API 请求的默认前缀
@@ -10,14 +10,24 @@ const request = axios.create({
timeout: 120000, // 请求超时时间
});
// axios.defaults.headers.common['X-Auth-Token'] = localStorage.getItem('access_token');
// 异常拦截处理器
const errorHandler = (error) => {
if (error.response) {
const data = error.response.data;
notification.error({
message: error.response.status,
description: JSON.stringify(data),
});
if (error.response.status == 401) {
notification.error({
message: error.response.status,
description: "请登录",
});
Router.push({ path: "/login-page" });
} else {
const data = error.response.data;
notification.error({
message: error.response.status,
description: JSON.stringify(data),
});
}
}
return Promise.reject(error);
};
@@ -29,6 +39,10 @@ request.interceptors.request.use((config) => {
config.headers["X-Cluster-Info-Id"] = clusterInfo.id;
// config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName);
}
const token = localStorage.getItem('access_token')
if (token) {
config.headers["X-Auth-Token"] = token;
}
return config;
}, errorHandler);

View File

@@ -0,0 +1,103 @@
<template>
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
class="login-box"
>
<h3 class="login-title">登录kafka-console-ui</h3>
<a-form-item label="账号">
<a-input
style="width: 200px"
allowClear
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入账号' }] },
]"
>
</a-input>
</a-form-item>
<a-form-item label="密码">
<a-input-password
style="width: 200px"
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码' }] },
]"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 16, offset: 5 }">
<a-button type="primary" @click="handleSubmit" :loading="loading"
>登录</a-button
>
</a-form-item>
</a-form>
</template>
<script>
import request from "@/utils/request";
import {AuthApi} from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import {mapMutations} from "vuex";
import {AUTH} from "@/store/mutation-types";
export default {
name: "Login",
data() {
return {
form: this.$form.createForm(this, { name: "login-form" }),
loading: false,
};
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = Object.assign({}, values);
request({
url: AuthApi.login.url,
method: AuthApi.login.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.setToken(res.data.token);
this.$router.push("/");
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
...mapMutations({
setToken: AUTH.SET,
}),
},
};
</script>
<style scoped>
.login-box {
border: 1px solid #dcdfe6;
width: 350px;
height: 300px;
margin: 120px auto;
padding: 35px 35px 15px 35px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
.login-title {
text-align: center;
margin: 0 auto 40px auto;
color: #303133;
}
</style>