feat(auth): 实现登录认证和权限控制功能

- 添加用户登录和登出功能
- 实现 Token 过期和无效的错误处理
- 添加路由权限控制,未登录用户重定向到登录页
- 优化登录失败的错误提示
- 修复搜索功能的返回数据问题
This commit is contained in:
ikmkj
2025-08-06 23:42:18 +08:00
parent 2a507659c2
commit 72c916da4d
9 changed files with 89 additions and 34 deletions

View File

@@ -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, "资源不存在"),

View File

@@ -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
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
// 创建一个包含中文错误信息的R对象
R<String> result = R.fail("认证失败,请重新登录");
// 创建一个包含中文错误信息的R对象
R<String> 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));
}
}

View File

@@ -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<Map<String, String>> login(String username, String password){
String token = userService.login(username, password);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
return R.success(tokenMap);
try {
String token = userService.login(username, password);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
return R.success(tokenMap);
} catch (BadCredentialsException e) {
return R.fail("用户名或密码错误");
}
}
@Operation(summary = "删除当前登录的用户")

View File

@@ -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)));
}
}

View File

@@ -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<UserMapper, User> 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);
}

View File

@@ -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);
}

View File

@@ -65,7 +65,7 @@ const handleLogin = async () => {
ElMessage.success('登录成功');
router.push('/home');
} else {
ElMessage.error('用户名或密码错误');
// ElMessage.error('用户名或密码错误'); // 错误已由 axios 拦截器处理
}
}
};

View File

@@ -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;

View File

@@ -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);
}
)