feat(auth): 实现登录认证和权限控制功能
- 添加用户登录和登出功能 - 实现 Token 过期和无效的错误处理 - 添加路由权限控制,未登录用户重定向到登录页 - 优化登录失败的错误提示 - 修复搜索功能的返回数据问题
This commit is contained in:
@@ -12,6 +12,8 @@ public enum ResultCode {
|
|||||||
FAILED(500, "操作失败"),
|
FAILED(500, "操作失败"),
|
||||||
VALIDATE_FAILED(400, "参数校验失败"),
|
VALIDATE_FAILED(400, "参数校验失败"),
|
||||||
UNAUTHORIZED(401, "未授权"),
|
UNAUTHORIZED(401, "未授权"),
|
||||||
|
TOKEN_EXPIRED(4011, "Token已过期"),
|
||||||
|
TOKEN_INVALID(4012, "Token无效"),
|
||||||
FORBIDDEN(403, "禁止访问"),
|
FORBIDDEN(403, "禁止访问"),
|
||||||
NOT_FOUND(404, "资源不存在"),
|
NOT_FOUND(404, "资源不存在"),
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameters;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -57,10 +58,14 @@ public class UserController {
|
|||||||
})
|
})
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public R<Map<String, String>> login(String username, String password){
|
public R<Map<String, String>> login(String username, String password){
|
||||||
|
try {
|
||||||
String token = userService.login(username, password);
|
String token = userService.login(username, password);
|
||||||
Map<String, String> tokenMap = new HashMap<>();
|
Map<String, String> tokenMap = new HashMap<>();
|
||||||
tokenMap.put("token", token);
|
tokenMap.put("token", token);
|
||||||
return R.success(tokenMap);
|
return R.success(tokenMap);
|
||||||
|
} catch (BadCredentialsException e) {
|
||||||
|
return R.fail("用户名或密码错误");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "删除当前登录的用户")
|
@Operation(summary = "删除当前登录的用户")
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.test.bijihoudaun.interceptor;
|
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 com.test.bijihoudaun.util.JwtTokenUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import io.jsonwebtoken.ExpiredJwtException;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import io.jsonwebtoken.security.SignatureException;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@@ -35,6 +38,7 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
|
|||||||
String authHeader = request.getHeader(this.tokenHeader);
|
String authHeader = request.getHeader(this.tokenHeader);
|
||||||
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
|
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
|
||||||
final String authToken = authHeader.substring(this.tokenHead.length());
|
final String authToken = authHeader.substring(this.tokenHead.length());
|
||||||
|
try {
|
||||||
String username = jwtTokenUtil.getUsernameFromToken(authToken);
|
String username = jwtTokenUtil.getUsernameFromToken(authToken);
|
||||||
|
|
||||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
@@ -46,7 +50,21 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
|
|||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
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);
|
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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import com.test.bijihoudaun.util.JwtTokenUtil;
|
|||||||
import com.test.bijihoudaun.util.PasswordUtils;
|
import com.test.bijihoudaun.util.PasswordUtils;
|
||||||
import com.test.bijihoudaun.util.UuidV7;
|
import com.test.bijihoudaun.util.UuidV7;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
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) {
|
public String login(String username, String password) {
|
||||||
UserDetails userDetails = loadUserByUsername(username);
|
UserDetails userDetails = loadUserByUsername(username);
|
||||||
if (!PasswordUtils.verify(password, userDetails.getPassword())) {
|
if (!PasswordUtils.verify(password, userDetails.getPassword())) {
|
||||||
throw new RuntimeException("密码错误");
|
throw new BadCredentialsException("用户名或密码错误");
|
||||||
}
|
}
|
||||||
return jwtTokenUtil.generateToken(userDetails);
|
return jwtTokenUtil.generateToken(userDetails);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -864,7 +864,7 @@ const handleSearch = async () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await searchMarkdown(searchKeyword.value);
|
const response = await searchMarkdown(searchKeyword.value);
|
||||||
groupMarkdownFiles.value = response.data;
|
groupMarkdownFiles.value = response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('搜索失败: ' + error.message);
|
ElMessage.error('搜索失败: ' + error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const handleLogin = async () => {
|
|||||||
ElMessage.success('登录成功');
|
ElMessage.success('登录成功');
|
||||||
router.push('/home');
|
router.push('/home');
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('用户名或密码错误');
|
// ElMessage.error('用户名或密码错误'); // 错误已由 axios 拦截器处理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import LoginPage from '../components/LoginPage.vue';
|
|||||||
import RegisterPage from '../components/RegisterPage.vue';
|
import RegisterPage from '../components/RegisterPage.vue';
|
||||||
import TrashPage from '../components/TrashPage.vue';
|
import TrashPage from '../components/TrashPage.vue';
|
||||||
|
|
||||||
|
import { useUserStore } from '../stores/user';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -12,7 +14,8 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/home',
|
path: '/home',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
component: HomePage
|
component: HomePage,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@@ -27,7 +30,8 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/trash',
|
path: '/trash',
|
||||||
name: 'Trash',
|
name: 'Trash',
|
||||||
component: TrashPage
|
component: TrashPage,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -36,4 +40,15 @@ const router = createRouter({
|
|||||||
routes
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
@@ -29,18 +31,30 @@ instance.interceptors.response.use(
|
|||||||
response => {
|
response => {
|
||||||
const res = response.data;
|
const res = response.data;
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
// ElMessage({
|
ElMessage({
|
||||||
// message: res.msg || 'Error',
|
message: res.msg || 'Error',
|
||||||
// type: 'error',
|
type: 'error',
|
||||||
// duration: 5 * 1000
|
duration: 5 * 1000
|
||||||
// });
|
});
|
||||||
return Promise.reject(new Error(res.msg || 'Error'));
|
return Promise.reject(new Error(res.msg || 'Error'));
|
||||||
} else {
|
} else {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
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);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user