构建一个 Springboot3 + Vue3 的全栈项目的步骤
-
创建 Vue3 项目
1
npm create vite@latest
-
创建项目之后,安装一些必要的依赖, 这个icon图标很有必要安装
进入vue项目,安装依赖
1
cd vue-project && npm install axios element-plus @element-plus/icons-vue
-
首先配置vite.config.js
1
2
3
4
5
6
7
8
9server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
} -
创建api请求工具request.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43import axios from 'axios'
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: '/api',
timeout: 5000
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 在这里可以添加token等认证信息
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
error => {
console.error(error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
// 这里可以根据后端返回的状态码进行不同的处理
if (res.code === 200) {
return res
} else {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
}
},
error => {
console.error('请求错误:', error)
ElMessage.error(error.message || '请求失败')
return Promise.reject(error)
}
)
export default service -
创建API接口文件:新建api包,里面新增js文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import request from '@/utils/request'
// 用户登录
export function login(data) {
return request({
url: '/user/login',
method: 'post',
data
})
}
// 获取用户信息
export function getUserInfo() {
return request({
url: '/user/info',
method: 'get'
})
}
// 用户注册
export function register(data) {
return request({
url: '/user/register',
method: 'post',
data
})
}
// 退出登录
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
} -
在组件中使用API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { login } from '@/api/user'
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
loading.value = true
const res = await login(loginForm)
if (res.code === 200) {
ElMessage.success('登录成功')
localStorage.setItem('token', res.data.token)
router.push('/')
}
} catch (error) {
console.error('登录失败:', error)
} finally {
loading.value = false
}
} -
在主应用文件main.js引入Element Plus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
// 样式导入顺序很重要
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app') -
在index.vue文件中配置路由守卫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104import { createRouter, createWebHistory } from 'vue-router'
import { ElMessage } from 'element-plus'
// 路由配置
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/components/Login.vue'),
meta: {
title: '登录',
requiresAuth: false
}
},
{
path: '/',
name: 'Layout',
component: () => import('@/components/Layout.vue'),
meta: {
requiresAuth: true
},
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
title: '首页',
requiresAuth: true
}
},
{
path: 'profile',
name: 'Profile',
component: () => import('@/views/Profile.vue'),
meta: {
title: '个人中心',
requiresAuth: true
}
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: {
title: '404',
requiresAuth: false
}
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 白名单路由
const whiteList = ['/login']
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - Vue Demo` : 'Vue Demo'
const token = localStorage.getItem('token')
if (token) {
if (to.path === '/login') {
// 已登录状态下访问登录页,重定向到首页
next({ path: '/' })
} else {
try {
// 这里可以添加获取用户信息的逻辑
// const userStore = useUserStore()
// await userStore.getUserInfo()
next()
} catch (error) {
// 获取用户信息失败,可能是token过期
localStorage.removeItem('token')
ElMessage.error('登录状态已过期,请重新登录')
next(`/login?redirect=${to.path}`)
}
}
} else {
// 未登录
if (whiteList.includes(to.path)) {
// 白名单路由,直接访问
next()
} else {
// 其他路由重定向到登录页
next(`/login?redirect=${to.path}`)
}
}
})
// 全局后置钩子
router.afterEach(() => {
// 路由切换后的操作,例如关闭loading等
// loading.value = false
})
export default router -
配置根组件
1
2
3
4
5
6
7
8
9
10
11<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView></RouterView>
</template>
<style scoped>
</style> -
安装依赖 启动服务器
1
2pnpm install
pnpm dev -
安装后端必要的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85<dependencies>
<!--springboot 基础依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 安全验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis数据库操作 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<!-- mysql数据库 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 注解依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>annotationProcessor</scope>
</dependency>
<!-- 规则校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- jwt登录鉴权 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> -
配置相应的包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25├── BackendInitConfigApplication.java # 启动类
├── common/
│ ├── Result.java
│ └── exception/
│ ├── BusinessException.java
│ └── GlobalExceptionHandler.java
├── config/
│ └── SecurityConfig.java
├── controller/
│ └── UserController.java
├── mapper/
│ └── UserMapper.java
├── model/
│ ├── dto/
│ │ ├── request/
│ │ │ └── UserLoginRequestDTO.java
│ │ └── response/
│ │ └── UserLoginResponseDTO.java
│ └── entity/
│ └── User.java
└── service/
├── impl/
│ └── UserServiceImpl.java
└── interfaces/
└── UserService.java -
Result.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32package jiejun_vuespringboot.backend_init_config.common;
import lombok.Data;
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
} -
GrobalExceptionHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package jiejun_vuespringboot.backend_init_config.common.exception;
import jiejun_vuespringboot.backend_init_config.common.Result;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getMessage());
}
@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e) {
return Result.error(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
e.printStackTrace();
return Result.error("系统错误");
}
} -
BusinessException.java
1
2
3
4
5
6
7package jiejun_vuespringboot.backend_init_config.common.exception;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
} -
CorsConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package jiejun_vuespringboot.backend_init_config.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许前端的域名(开发环境可以用*,生产建议写具体域名)
config.addAllowedOriginPattern("*");
// 允许的请求头
config.addAllowedHeader("*");
// 允许的请求方法
config.addAllowedMethod("*");
// 允许携带cookie
config.setAllowCredentials(true);
// 预检请求的缓存时间(秒)
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
} -
SecurityConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package jiejun_vuespringboot.backend_init_config.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 禁用CSRF
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login").permitAll() // 允许登录接口匿名访问
.anyRequest().authenticated() // 其他接口需要认证
)
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
} -
JwtUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package jiejun_vuespringboot.backend_init_config.config;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.Map;
import javax.crypto.SecretKey;
public class JwtUtil {
// 建议放到配置文件
private static final String SECRET = "your-256-bit-secret-your-256-bit-secret"; // 长度要足够
private static final long EXPIRATION = 86400000L; // 24小时
private static SecretKey getKey() {
return Keys.hmacShaKeyFor(SECRET.getBytes());
}
public static String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(getKey())
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getKey())
.build()
.parseClaimsJws(token)
.getBody();
}
} -
JwtAuthenticationFilter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package jiejun_vuespringboot.backend_init_config.config;
import io.jsonwebtoken.Claims;
import jiejun_vuespringboot.backend_init_config.config.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Claims claims = JwtUtil.parseToken(token);
String username = claims.get("username", String.class);
if (username != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// token无效,忽略,后续会被Spring Security拦截
}
}
filterChain.doFilter(request, response);
}
} -
UseController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package jiejun_vuespringboot.backend_init_config.controller;
import jiejun_vuespringboot.backend_init_config.common.Result;
import jiejun_vuespringboot.backend_init_config.model.dto.request.UserLoginRequestDTO;
import jiejun_vuespringboot.backend_init_config.model.dto.response.UserLoginResponseDTO;
import jiejun_vuespringboot.backend_init_config.service.interfaces.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public Result<UserLoginResponseDTO> login(@Valid @RequestBody UserLoginRequestDTO requestDTO) {
UserLoginResponseDTO responseDTO = userService.login(requestDTO);
return Result.success(responseDTO);
}
} -
UserLoginRequestDTO.java
1
2
3
4
5
6
7
8
9
10
11
12
13package jiejun_vuespringboot.backend_init_config.model.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class UserLoginRequestDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
} -
UserService.java
1
2
3
4
5
6
7
8package jiejun_vuespringboot.backend_init_config.service.interfaces;
import jiejun_vuespringboot.backend_init_config.model.dto.request.UserLoginRequestDTO;
import jiejun_vuespringboot.backend_init_config.model.dto.response.UserLoginResponseDTO;
public interface UserService {
UserLoginResponseDTO login(UserLoginRequestDTO requestDTO);
} -
UserServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57package jiejun_vuespringboot.backend_init_config.service.impl;
import jiejun_vuespringboot.backend_init_config.common.exception.BusinessException;
import jiejun_vuespringboot.backend_init_config.mapper.UserMapper;
import jiejun_vuespringboot.backend_init_config.model.dto.request.UserLoginRequestDTO;
import jiejun_vuespringboot.backend_init_config.model.dto.response.UserLoginResponseDTO;
import jiejun_vuespringboot.backend_init_config.model.entity.User;
import jiejun_vuespringboot.backend_init_config.service.interfaces.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserLoginResponseDTO login(UserLoginRequestDTO requestDTO) {
// 1. 查询用户
User user = userMapper.findByUsername(requestDTO.getUsername());
if (user == null) {
throw new BusinessException("用户不存在");
}
// 2. 验证密码
if (!passwordEncoder.matches(requestDTO.getPassword(), user.getPassword())) {
throw new BusinessException("密码错误");
}
// 3. 检查用户状态
if (user.getStatus() != 1) {
throw new BusinessException("用户已被禁用");
}
// 4. 使用jwt,生成token
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("userId", user.getId());
String token = JwtUtil.generateToken(claims);
// 5. 构建返回对象
UserLoginResponseDTO responseDTO = new UserLoginResponseDTO();
responseDTO.setToken(token);
responseDTO.setUsername(user.getUsername());
responseDTO.setPermissions(new ArrayList<>()); // 这里简化处理,实际应该查询用户权限
return responseDTO;
}
} -
UserLoginResponseDTO.java
1
2
3
4
5
6
7
8
9
10
11package jiejun_vuespringboot.backend_init_config.model.dto.response;
import lombok.Data;
import java.util.List;
@Data
public class UserLoginResponseDTO {
private String token;
private String username;
private List<String> permissions;
} -
User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14package jiejun_vuespringboot.backend_init_config.model.entity;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String password;
private String salt;
private Integer status;
private String createTime;
private String updateTime;
} -
UserMapper.java
1
2
3
4
5
6
7
8
9
10
11package jiejun_vuespringboot.backend_init_config.mapper;
import jiejun_vuespringboot.backend_init_config.model.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper {
@Select("SELECT * FROM sys_user WHERE username = #{username}")
User findByUsername(String username);
}