From 435a5ca2bc6938354ff1ee5a8d5ec5481f1c481e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E6=99=93=E4=B8=9C?= <763795151@qq.com> Date: Sun, 14 May 2023 21:25:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xuxd/kafka/console/beans/Credentials.java | 21 ++++ .../xuxd/kafka/console/beans/LoginResult.java | 13 +++ .../kafka/console/beans/dto/LoginUserDTO.java | 15 +++ .../xuxd/kafka/console/config/AuthConfig.java | 21 ++++ .../console/controller/AuthController.java | 36 ++++++ .../xuxd/kafka/console/dao/SysUserMapper.java | 2 + .../kafka/console/interceptor/AuthFilter.java | 64 +++++++++++ .../console/interceptor/ContextSetFilter.java | 2 + .../kafka/console/service/AuthService.java | 13 +++ .../console/service/impl/AuthServiceImpl.java | 54 +++++++++ .../xuxd/kafka/console/utils/AuthUtil.java | 54 +++++++++ .../com/xuxd/kafka/console/utils/MD5Util.java | 32 ++++++ src/main/resources/application.yml | 6 + ui/src/App.vue | 57 ++++++---- ui/src/router/index.js | 58 +++++++++- ui/src/store/index.js | 11 +- ui/src/store/mutation-types.js | 5 + ui/src/utils/api.js | 11 ++ ui/src/utils/request.js | 26 ++++- ui/src/views/login/Login.vue | 103 ++++++++++++++++++ 20 files changed, 575 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/xuxd/kafka/console/beans/Credentials.java create mode 100644 src/main/java/com/xuxd/kafka/console/beans/LoginResult.java create mode 100644 src/main/java/com/xuxd/kafka/console/beans/dto/LoginUserDTO.java create mode 100644 src/main/java/com/xuxd/kafka/console/config/AuthConfig.java create mode 100644 src/main/java/com/xuxd/kafka/console/controller/AuthController.java create mode 100644 src/main/java/com/xuxd/kafka/console/interceptor/AuthFilter.java create mode 100644 src/main/java/com/xuxd/kafka/console/service/AuthService.java create mode 100644 src/main/java/com/xuxd/kafka/console/service/impl/AuthServiceImpl.java create mode 100644 src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java create mode 100644 src/main/java/com/xuxd/kafka/console/utils/MD5Util.java create mode 100644 ui/src/views/login/Login.vue diff --git a/src/main/java/com/xuxd/kafka/console/beans/Credentials.java b/src/main/java/com/xuxd/kafka/console/beans/Credentials.java new file mode 100644 index 0000000..fe6db9c --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/Credentials.java @@ -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; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/LoginResult.java b/src/main/java/com/xuxd/kafka/console/beans/LoginResult.java new file mode 100644 index 0000000..75faeaa --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/LoginResult.java @@ -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; +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dto/LoginUserDTO.java b/src/main/java/com/xuxd/kafka/console/beans/dto/LoginUserDTO.java new file mode 100644 index 0000000..b4fc2f4 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dto/LoginUserDTO.java @@ -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; +} diff --git a/src/main/java/com/xuxd/kafka/console/config/AuthConfig.java b/src/main/java/com/xuxd/kafka/console/config/AuthConfig.java new file mode 100644 index 0000000..04b2d42 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/config/AuthConfig.java @@ -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; +} diff --git a/src/main/java/com/xuxd/kafka/console/controller/AuthController.java b/src/main/java/com/xuxd/kafka/console/controller/AuthController.java new file mode 100644 index 0000000..f71e806 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/controller/AuthController.java @@ -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); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java b/src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java index 7a3af87..73a0060 100644 --- a/src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java +++ b/src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java @@ -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 { } diff --git a/src/main/java/com/xuxd/kafka/console/interceptor/AuthFilter.java b/src/main/java/com/xuxd/kafka/console/interceptor/AuthFilter.java new file mode 100644 index 0000000..0a32555 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/interceptor/AuthFilter.java @@ -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); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/interceptor/ContextSetFilter.java b/src/main/java/com/xuxd/kafka/console/interceptor/ContextSetFilter.java index cd5acf9..d336f9c 100644 --- a/src/main/java/com/xuxd/kafka/console/interceptor/ContextSetFilter.java +++ b/src/main/java/com/xuxd/kafka/console/interceptor/ContextSetFilter.java @@ -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 { diff --git a/src/main/java/com/xuxd/kafka/console/service/AuthService.java b/src/main/java/com/xuxd/kafka/console/service/AuthService.java new file mode 100644 index 0000000..c437dc1 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/service/AuthService.java @@ -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); +} diff --git a/src/main/java/com/xuxd/kafka/console/service/impl/AuthServiceImpl.java b/src/main/java/com/xuxd/kafka/console/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..d44d4af --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/service/impl/AuthServiceImpl.java @@ -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 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(); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java b/src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java new file mode 100644 index 0000000..dd5d745 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java @@ -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; + } + } +} diff --git a/src/main/java/com/xuxd/kafka/console/utils/MD5Util.java b/src/main/java/com/xuxd/kafka/console/utils/MD5Util.java new file mode 100644 index 0000000..801699f --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/utils/MD5Util.java @@ -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); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8400628..e372073 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -47,3 +47,9 @@ logging: cron: # clear-dirty-user: 0 * * * * ? clear-dirty-user: 0 0 1 * * ? + +# 权限认证设置,设置为true,需要先登录才能访问 +auth: + enable: true + # 登录用户token的过期时间,单位:小时 + expire-hours: 24 \ No newline at end of file diff --git a/ui/src/App.vue b/ui/src/App.vue index a07c4a0..2d8dde4 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -25,12 +25,12 @@ diff --git a/ui/src/router/index.js b/ui/src/router/index.js index d69f292..789b782 100644 --- a/ui/src/router/index.js +++ b/ui/src/router/index.js @@ -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; diff --git a/ui/src/store/index.js b/ui/src/store/index.js index 78d8c4a..9ef4040 100644 --- a/ui/src/store/index.js +++ b/ui/src/store/index.js @@ -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: {}, diff --git a/ui/src/store/mutation-types.js b/ui/src/store/mutation-types.js index f5ec573..227a790 100644 --- a/ui/src/store/mutation-types.js +++ b/ui/src/store/mutation-types.js @@ -1,3 +1,8 @@ export const CLUSTER = { SWITCH: "switchCluster", }; + +export const AUTH = { + ENABLE: "enable", + SET: "setToken", +}; diff --git a/ui/src/utils/api.js b/ui/src/utils/api.js index b098723..27549b7 100644 --- a/ui/src/utils/api.js +++ b/ui/src/utils/api.js @@ -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", + }, }; \ No newline at end of file diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js index 119e8d8..8bf2a3d 100644 --- a/ui/src/utils/request.js +++ b/ui/src/utils/request.js @@ -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); diff --git a/ui/src/views/login/Login.vue b/ui/src/views/login/Login.vue new file mode 100644 index 0000000..1e3eb63 --- /dev/null +++ b/ui/src/views/login/Login.vue @@ -0,0 +1,103 @@ + + + + +