From 72c916da4d992753014545d4b117eebe76c2b221 Mon Sep 17 00:00:00 2001 From: ikmkj <1@qq,com> Date: Wed, 6 Aug 2025 23:42:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E8=AE=A4=E8=AF=81=E5=92=8C=E6=9D=83=E9=99=90=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加用户登录和登出功能 - 实现 Token 过期和无效的错误处理 - 添加路由权限控制,未登录用户重定向到登录页 - 优化登录失败的错误提示 - 修复搜索功能的返回数据问题 --- .../common/response/ResultCode.java | 2 ++ .../security/JwtAuthenticationEntryPoint.java | 20 +++++------ .../controller/UserController.java | 13 ++++--- .../JwtAuthenticationTokenFilter.java | 36 ++++++++++++++----- .../service/impl/UserServiceImpl.java | 3 +- biji-qianduan/src/components/HomePage.vue | 2 +- biji-qianduan/src/components/LoginPage.vue | 2 +- biji-qianduan/src/router/index.js | 19 ++++++++-- biji-qianduan/src/utils/axios.js | 26 ++++++++++---- 9 files changed, 89 insertions(+), 34 deletions(-) diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/common/response/ResultCode.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/common/response/ResultCode.java index 661d198..ff3e4b7 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/common/response/ResultCode.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/common/response/ResultCode.java @@ -12,6 +12,8 @@ public enum ResultCode { FAILED(500, "操作失败"), VALIDATE_FAILED(400, "参数校验失败"), UNAUTHORIZED(401, "未授权"), + TOKEN_EXPIRED(4011, "Token已过期"), + TOKEN_INVALID(4012, "Token无效"), FORBIDDEN(403, "禁止访问"), NOT_FOUND(404, "资源不存在"), diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/security/JwtAuthenticationEntryPoint.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/security/JwtAuthenticationEntryPoint.java index 5ca8d57..de782d4 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/security/JwtAuthenticationEntryPoint.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/security/JwtAuthenticationEntryPoint.java @@ -23,15 +23,15 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - response.setCharacterEncoding("UTF-8"); - response.setContentType("application/json"); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 - - // 创建一个包含中文错误信息的R对象 - R result = R.fail("认证失败,请重新登录"); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 + + // 创建一个包含中文错误信息的R对象 + R result = R.fail("认证失败,请重新登录"); - // 使用ObjectMapper将对象转换为JSON字符串并写入响应 - ObjectMapper objectMapper = new ObjectMapper(); - response.getWriter().write(objectMapper.writeValueAsString(result)); - } + // 使用ObjectMapper将对象转换为JSON字符串并写入响应 + ObjectMapper objectMapper = new ObjectMapper(); + response.getWriter().write(objectMapper.writeValueAsString(result)); + } } \ No newline at end of file diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java index aa66962..2e5ec70 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; @@ -57,10 +58,14 @@ public class UserController { }) @PostMapping("/login") public R> login(String username, String password){ - String token = userService.login(username, password); - Map tokenMap = new HashMap<>(); - tokenMap.put("token", token); - return R.success(tokenMap); + try { + String token = userService.login(username, password); + Map tokenMap = new HashMap<>(); + tokenMap.put("token", token); + return R.success(tokenMap); + } catch (BadCredentialsException e) { + return R.fail("用户名或密码错误"); + } } @Operation(summary = "删除当前登录的用户") diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/JwtAuthenticationTokenFilter.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/JwtAuthenticationTokenFilter.java index 76466b2..c7e34be 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/JwtAuthenticationTokenFilter.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/JwtAuthenticationTokenFilter.java @@ -1,8 +1,11 @@ package com.test.bijihoudaun.interceptor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.test.bijihoudaun.common.response.R; +import com.test.bijihoudaun.common.response.ResultCode; import com.test.bijihoudaun.util.JwtTokenUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.security.SignatureException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -35,18 +38,33 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(this.tokenHead)) { final String authToken = authHeader.substring(this.tokenHead.length()); - String username = jwtTokenUtil.getUsernameFromToken(authToken); + try { + String username = jwtTokenUtil.getUsernameFromToken(authToken); - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); - if (jwtTokenUtil.validateToken(authToken, userDetails)) { - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); + if (jwtTokenUtil.validateToken(authToken, userDetails)) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } } + } catch (ExpiredJwtException e) { + sendErrorResponse(response, ResultCode.TOKEN_EXPIRED); + return; + } catch (SignatureException e) { + sendErrorResponse(response, ResultCode.TOKEN_INVALID); + return; } } chain.doFilter(request, response); } + + private void sendErrorResponse(HttpServletResponse response, ResultCode resultCode) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + ObjectMapper mapper = new ObjectMapper(); + response.getWriter().write(mapper.writeValueAsString(R.fail(resultCode))); + } } \ No newline at end of file diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java index 45e2147..361efe6 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java @@ -14,6 +14,7 @@ import com.test.bijihoudaun.util.JwtTokenUtil; import com.test.bijihoudaun.util.PasswordUtils; import com.test.bijihoudaun.util.UuidV7; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -69,7 +70,7 @@ public class UserServiceImpl extends ServiceImpl implements Us public String login(String username, String password) { UserDetails userDetails = loadUserByUsername(username); if (!PasswordUtils.verify(password, userDetails.getPassword())) { - throw new RuntimeException("密码错误"); + throw new BadCredentialsException("用户名或密码错误"); } return jwtTokenUtil.generateToken(userDetails); } diff --git a/biji-qianduan/src/components/HomePage.vue b/biji-qianduan/src/components/HomePage.vue index 09c8579..7ccf9ef 100644 --- a/biji-qianduan/src/components/HomePage.vue +++ b/biji-qianduan/src/components/HomePage.vue @@ -864,7 +864,7 @@ const handleSearch = async () => { } try { const response = await searchMarkdown(searchKeyword.value); - groupMarkdownFiles.value = response.data; + groupMarkdownFiles.value = response.data || []; } catch (error) { ElMessage.error('搜索失败: ' + error.message); } diff --git a/biji-qianduan/src/components/LoginPage.vue b/biji-qianduan/src/components/LoginPage.vue index bf6815a..10adec7 100644 --- a/biji-qianduan/src/components/LoginPage.vue +++ b/biji-qianduan/src/components/LoginPage.vue @@ -65,7 +65,7 @@ const handleLogin = async () => { ElMessage.success('登录成功'); router.push('/home'); } else { - ElMessage.error('用户名或密码错误'); + // ElMessage.error('用户名或密码错误'); // 错误已由 axios 拦截器处理 } } }; diff --git a/biji-qianduan/src/router/index.js b/biji-qianduan/src/router/index.js index 2f0bd20..8b7725f 100644 --- a/biji-qianduan/src/router/index.js +++ b/biji-qianduan/src/router/index.js @@ -4,6 +4,8 @@ import LoginPage from '../components/LoginPage.vue'; import RegisterPage from '../components/RegisterPage.vue'; import TrashPage from '../components/TrashPage.vue'; +import { useUserStore } from '../stores/user'; + const routes = [ { path: '/', @@ -12,7 +14,8 @@ const routes = [ { path: '/home', name: 'Home', - component: HomePage + component: HomePage, + meta: { requiresAuth: true } }, { path: '/login', @@ -27,7 +30,8 @@ const routes = [ { path: '/trash', name: 'Trash', - component: TrashPage + component: TrashPage, + meta: { requiresAuth: true } } ]; @@ -36,4 +40,15 @@ const router = createRouter({ routes }); +router.beforeEach((to, from, next) => { + const userStore = useUserStore(); + const requiresAuth = to.matched.some(record => record.meta.requiresAuth); + + if (requiresAuth && !userStore.isLoggedIn) { + next('/login'); + } else { + next(); + } +}); + export default router; diff --git a/biji-qianduan/src/utils/axios.js b/biji-qianduan/src/utils/axios.js index a4a23ca..b76f6c8 100644 --- a/biji-qianduan/src/utils/axios.js +++ b/biji-qianduan/src/utils/axios.js @@ -1,5 +1,7 @@ import axios from 'axios' import { useUserStore } from '../stores/user' +import { ElMessage } from 'element-plus' +import router from '../router' const instance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, @@ -29,18 +31,30 @@ instance.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { - // ElMessage({ - // message: res.msg || 'Error', - // type: 'error', - // duration: 5 * 1000 - // }); + ElMessage({ + message: res.msg || 'Error', + type: 'error', + duration: 5 * 1000 + }); return Promise.reject(new Error(res.msg || 'Error')); } else { return res.data; } }, error => { - return Promise.reject(error) + if (error.response && error.response.status === 401) { + const userStore = useUserStore(); + userStore.logout(); + ElMessage.error('登录已过期,请重新登录'); + router.push('/login'); + } else { + ElMessage({ + message: error.message, + type: 'error', + duration: 5 * 1000 + }); + } + return Promise.reject(error); } )