Vue3+Spring Boot登录系统开发指南
Tips
本文适用于Web开发初学者,详细介绍基于Vue3和Spring Boot构建一个简单登录系统的完整过程。
1. 项目概述
源代码资源
本项目完整源代码可通过以下渠道获取:
- GitHub:JDWA-vue-spring-boot-login
- Gitee:vue-spring-boot-login
- 蓝奏云:vue-spring-boot-login.zip(密码:jdwa)
1.1 功能描述
本项目是一个前后端分离的登录系统,主要功能包括:
- 用户登录认证
- JWT令牌生成与验证
- 登录状态保持
- 路由访问控制
1.2 技术栈
前端技术栈:
- Vue 3.3.4 (组合式API)
- Vite 4.3.8 (构建工具)
- Vue Router 4.2.1 (路由)
- Pinia 2.1.3 (状态管理)
- Element Plus 2.3.5 (UI组件库)
- Axios (HTTP客户端)
后端技术栈:
- Spring Boot 2.7.11
- Spring Security (安全框架)
- MyBatis (持久层框架)
- JWT (JSON Web Token)
- MySQL (数据库)
1.3 项目架构

项目采用典型的前后端分离架构:
- 前端负责界面渲染和用户交互
- 后端负责业务逻辑处理和数据持久化
- 通过RESTful API进行通信
- 使用JWT进行身份认证
2. 环境准备
2.1 开发环境要求
- Node.js (v14+)
- JDK 8+ (本项目使用JDK 8)
- Maven 3.6+
- MySQL 5.7+
- IDE推荐:
- 前端:VSCode/WebStorm
- 后端:IntelliJ IDEA/Eclipse
2.2 项目结构
vue3+spring-boot/
├── frontend/ # 前端项目
│ ├── public/ # 静态资源
│ │ ├── assets/ # 资源文件
│ │ ├── src/ # 源代码
│ │ │ ├── components/ # 组件
│ │ │ ├── router/ # 路由
│ │ │ ├── store/ # 状态管理
│ │ │ ├── utils/ # 工具类
│ │ │ ├── views/ # 视图
│ │ │ ├── App.vue # 根组件
│ │ │ └── main.js # 入口文件
│ │ ├── index.html # HTML入口
│ │ ├── package.json # 依赖管理
│ │ └── vite.config.js # Vite配置
│ ├── backend/ # 后端项目
│ │ ├── src/
│ │ │ ├── main/
│ │ │ │ ├── java/com/jdwa/login/
│ │ │ │ │ ├── controller/ # 控制器
│ │ │ │ │ ├── dto/ # 数据传输对象
│ │ │ │ │ ├── mapper/ # MyBatis映射接口
│ │ │ │ │ ├── model/ # 实体模型
│ │ │ │ │ ├── security/ # 安全相关
│ │ │ │ │ ├── service/ # 服务层
│ │ │ │ │ └── JDWALoginApplication.java # 启动类
│ │ │ │ └── resources/
│ │ │ │ ├── mapper/ # MyBatis XML映射文件
│ │ │ │ ├── application.properties # 配置文件
│ │ │ │ └── db/ # 数据库脚本
│ │ │ └── pom.xml # Maven依赖
│ └── README.md # 项目说明
3. 前端开发
3.1 创建前端项目
前端项目使用Vite创建Vue3应用,步骤如下:
# 创建项目目录
mkdir -p frontend
# 创建package.json
cd frontend
package.json:
{
"name": "jdwa-vue3-login-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"axios": "^1.4.0",
"element-plus": "^2.3.5",
"vue": "^3.3.4",
"vue-router": "^4.2.1",
"pinia": "^2.1.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.3.8"
}
}
vite.config.js:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path
}
}
}
})
3.2 配置前端路由
src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: () => import('../views/JDWALogin.vue')
},
{
path: '/home',
name: 'Home',
component: () => import('../views/JDWAHome.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫,检查是否已登录
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!token) {
next({ name: 'Login' })
} else {
next()
}
} else {
next()
}
})
export default router
3.3 状态管理配置
使用Pinia管理用户状态和登录逻辑:
src/store/user.js:
import { defineStore } from 'pinia'
import axios from 'axios'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
user: JSON.parse(localStorage.getItem('user') || '{}')
}),
getters: {
isLoggedIn: (state) => !!state.token,
username: (state) => state.user.username || ''
},
actions: {
async login(username, password) {
try {
const response = await axios.post('/api/login', { username, password })
const { token, user } = response.data
this.token = token
this.user = user
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
return { success: true }
} catch (error) {
console.error('登录失败:', error)
return {
success: false,
message: error.response?.data?.message || '登录失败,请检查用户名和密码'
}
}
},
logout() {
this.token = ''
this.user = {}
localStorage.removeItem('token')
localStorage.removeItem('user')
}
}
})
3.4 HTTP请求配置
src/utils/request.js:
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const service = axios.create({
baseURL: '/', // 不再添加/api前缀,因为后端已经配置了context-path
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(
config => {
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 => {
return response.data
},
error => {
if (error.response) {
const { status, data } = error.response
// 处理不同的错误状态码
switch (status) {
case 401:
ElMessage.error('未授权,请重新登录')
localStorage.removeItem('token')
localStorage.removeItem('user')
break
case 403:
ElMessage.error('没有权限访问该资源')
break
case 500:
ElMessage.error('服务器错误,请稍后再试')
break
default:
ElMessage.error(data.message || '请求失败')
}
} else {
ElMessage.error('网络错误,请检查您的网络连接')
}
return Promise.reject(error)
}
)
export default service
3.5 登录页面实现
src/views/JDWALogin.vue:
<template>
<div class="login-container">
<div class="login-box">
<div class="login-title">
<h2>JDWA登录系统</h2>
</div>
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" class="login-form">
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
prefix-icon="el-icon-user"
@keyup.enter="handleLogin">
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
prefix-icon="el-icon-lock"
@keyup.enter="handleLogin">
</el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loading"
class="login-button"
@click="handleLogin">
登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '../store/user'
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref(null)
const loading = ref(false)
const loginForm = reactive({
username: '',
password: ''
})
const loginRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度必须在3到20个字符之间', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度必须在6到20个字符之间', trigger: 'blur' }
]
}
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
loading.value = true
const { success, message } = await userStore.login(loginForm.username, loginForm.password)
if (success) {
ElMessage.success('登录成功')
router.push('/home')
} else {
ElMessage.error(message)
}
} catch (error) {
console.error('表单验证失败', error)
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
}
.login-box {
width: 400px;
padding: 30px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.login-title {
text-align: center;
margin-bottom: 30px;
}
.login-title h2 {
font-weight: 600;
font-size: 24px;
color: #409EFF;
}
.login-form {
margin-top: 20px;
}
.login-button {
width: 100%;
}
</style>
3.6 主页实现
src/views/JDWAHome.vue:
<template>
<div class="home-container">
<el-container>
<el-header>
<div class="header-content">
<div class="logo">JDWA系统</div>
<div class="user-info">
<span>欢迎, {{ username }}</span>
<el-button type="text" @click="handleLogout">退出登录</el-button>
</div>
</div>
</el-header>
<el-main>
<el-card class="welcome-card">
<h2>登录成功!</h2>
<p>这是一个基于Vue 3和Spring Boot的简单登录系统示例。</p>
<p>您已成功登录系统。</p>
</el-card>
</el-main>
</el-container>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { useUserStore } from '../store/user'
const router = useRouter()
const userStore = useUserStore()
const username = computed(() => userStore.username)
const handleLogout = () => {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore.logout()
router.push('/login')
}).catch(() => {})
}
</script>
<style scoped>
.home-container {
height: 100vh;
}
.el-header {
background-color: #409EFF;
color: white;
line-height: 60px;
padding: 0 20px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 18px;
font-weight: bold;
}
.user-info {
display: flex;
align-items: center;
}
.user-info span {
margin-right: 10px;
}
.welcome-card {
max-width: 600px;
margin: 20px auto;
text-align: center;
}
.welcome-card h2 {
color: #409EFF;
margin-bottom: 20px;
}
</style>
## 4. 后端开发
### 4.1 创建Spring Boot项目
首先,我们使用Spring Initializr来创建一个基础的Spring Boot项目。
**创建步骤**:
1. 访问 https://start.spring.io/
2. 配置项目参数:
- 项目类型:Maven
- 语言:Java
- Spring Boot版本:2.7.11
- 项目元数据:
- Group: com.jdwa
- Artifact: jdwa-login
- Name: jdwa-login
- Description: Spring Boot登录系统后端
- Package Name: com.jdwa.login
- Packaging: Jar
- Java版本:8
- 依赖选择:
- Spring Web
- Spring Security
- MyBatis Framework
- MySQL Driver
- Lombok
3. 点击"Generate"下载项目压缩包
4. 解压并导入IDE
### 4.2 配置数据库连接
**src/main/resources/application.properties**:
```properties
# 应用配置
spring.application.name=jdwa-login
server.port=8080
# 数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/jdwa_login?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# MyBatis配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.jdwa.login.model
mybatis.configuration.map-underscore-to-camel-case=true
# 日志配置
logging.level.com.jdwa.login=debug
# JWT配置
jdwa.jwt.secret=jdwaSecretKey1234567890abcdefghijklmnopqrstuvwxyz
jdwa.jwt.expiration=86400000
4.3 数据库设计
创建数据库和用户表:
src/main/resources/db/schema.sql:
-- 创建数据库
CREATE DATABASE IF NOT EXISTS jdwa_login DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jdwa_login;
-- 创建用户表
CREATE TABLE IF NOT EXISTS jdwa_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
role VARCHAR(50) NOT NULL DEFAULT 'USER',
status TINYINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
last_login_time DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试用户数据,密码为:123456(BCrypt加密后的值)
INSERT INTO jdwa_user (username, password, email, role, status, create_time, update_time)
VALUES
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'admin@example.com', 'ADMIN', 1, NOW(), NOW()),
('user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'user@example.com', 'USER', 1, NOW(), NOW());
4.4 实体类设计
src/main/java/com/jdwa/login/model/JDWAUser.java:
package com.jdwa.login.model;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class JDWAUser {
private Long id;
private String username;
private String password;
private String email;
private String phone;
private String role;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private LocalDateTime lastLoginTime;
}
4.5 DTO设计
src/main/java/com/jdwa/login/dto/JDWALoginRequest.java:
package com.jdwa.login.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Data
public class JDWALoginRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3到20个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6到20个字符之间")
private String password;
}
src/main/java/com/jdwa/login/dto/JDWALoginResponse.java:
package com.jdwa.login.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JDWALoginResponse {
private String token;
private JDWAUserDTO user;
}
src/main/java/com/jdwa/login/dto/JDWAUserDTO.java:
package com.jdwa.login.dto;
import lombok.Data;
@Data
public class JDWAUserDTO {
private Long id;
private String username;
private String email;
private String role;
}
src/main/java/com/jdwa/login/dto/JDWAResponse.java:
package com.jdwa.login.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JDWAResponse<T> {
private boolean success;
private String message;
private T data;
public static <T> JDWAResponse<T> success(T data) {
return new JDWAResponse<>(true, "操作成功", data);
}
public static <T> JDWAResponse<T> success(String message, T data) {
return new JDWAResponse<>(true, message, data);
}
public static <T> JDWAResponse<T> fail(String message) {
return new JDWAResponse<>(false, message, null);
}
}
4.6 Mapper接口实现
src/main/java/com/jdwa/login/mapper/JDWAUserMapper.java:
package com.jdwa.login.mapper;
import com.jdwa.login.model.JDWAUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface JDWAUserMapper {
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户对象
*/
JDWAUser findByUsername(@Param("username") String username);
/**
* 更新最后登录时间
* @param userId 用户ID
* @return 影响行数
*/
int updateLastLoginTime(@Param("userId") Long userId);
}
src/main/resources/mapper/JDWAUserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jdwa.login.mapper.JDWAUserMapper">
<resultMap id="userMap" type="com.jdwa.login.model.JDWAUser">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="role" column="role"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="lastLoginTime" column="last_login_time"/>
</resultMap>
<select id="findByUsername" resultMap="userMap">
SELECT * FROM jdwa_user WHERE username = #{username}
</select>
<update id="updateLastLoginTime">
UPDATE jdwa_user SET last_login_time = NOW() WHERE id = #{userId}
</update>
</mapper>
4.7 JWT工具类
src/main/java/com/jdwa/login/security/JDWAJwtTokenUtil.java:
package com.jdwa.login.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JDWAJwtTokenUtil {
@Value("${jdwa.jwt.secret}")
private String secret;
@Value("${jdwa.jwt.expiration}")
private Long expiration;
/**
* 从令牌中获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 从令牌中获取过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 从令牌中获取所有声明
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 检查令牌是否过期
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 为用户生成令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
/**
* 生成令牌的具体实现
*/
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 验证令牌
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
4.8 Spring Security配置
src/main/java/com/jdwa/login/security/JDWAUserDetailsService.java:
package com.jdwa.login.security;
import com.jdwa.login.mapper.JDWAUserMapper;
import com.jdwa.login.model.JDWAUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class JDWAUserDetailsService implements UserDetailsService {
@Autowired
private JDWAUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
JDWAUser user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 如果用户被禁用,抛出异常
if (user.getStatus() != 1) {
throw new UsernameNotFoundException("用户已被禁用: " + username);
}
// 授予用户角色权限
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + user.getRole());
return new User(
user.getUsername(),
user.getPassword(),
Collections.singletonList(authority)
);
}
}
src/main/java/com/jdwa/login/security/JDWAAuthenticationFilter.java:
package com.jdwa.login.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JDWAAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JDWAUserDetailsService userDetailsService;
@Autowired
private JDWAJwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// 如果请求头包含JWT Token
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwtToken = authHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (Exception e) {
logger.error("JWT Token解析失败: " + e.getMessage());
}
}
// 一旦我们获取到token,就验证它
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 如果token有效,那么我们手动设置Spring Security的上下文
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("认证用户 " + username + ",设置安全上下文");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
src/main/java/com/jdwa/login/security/JDWASecurityConfig.java:
package com.jdwa.login.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JDWASecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JDWAUserDetailsService userDetailsService;
@Autowired
private JDWAAuthenticationFilter jwtAuthenticationFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// 配置自定义的UserDetailsService和密码加密方式
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 禁用CSRF(跨站请求伪造)
.csrf().disable()
// 启用CORS
.cors().and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 设置权限
.authorizeRequests()
// 允许登录接口匿名访问
.antMatchers("/api/login").permitAll()
// 其他所有请求需要认证
.anyRequest().authenticated();
// 添加JWT token过滤器
httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// 禁用缓存
httpSecurity.headers().cacheControl();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
4.9 服务层实现
src/main/java/com/jdwa/login/service/JDWAUserService.java:
package com.jdwa.login.service;
import com.jdwa.login.dto.JDWALoginRequest;
import com.jdwa.login.dto.JDWALoginResponse;
import com.jdwa.login.dto.JDWAUserDTO;
import com.jdwa.login.mapper.JDWAUserMapper;
import com.jdwa.login.model.JDWAUser;
import com.jdwa.login.security.JDWAJwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
@Service
public class JDWAUserService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JDWAJwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JDWAUserMapper userMapper;
/**
* 用户登录
* @param loginRequest 登录请求
* @return 登录响应(包含token和用户信息)
* @throws Exception 认证异常
*/
public JDWALoginResponse login(JDWALoginRequest loginRequest) throws Exception {
try {
// 认证用户凭据
authenticate(loginRequest.getUsername(), loginRequest.getPassword());
// 加载用户详情
final UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
// 生成JWT令牌
final String token = jwtTokenUtil.generateToken(userDetails);
// 获取用户信息
JDWAUser user = userMapper.findByUsername(loginRequest.getUsername());
// 更新最后登录时间
userMapper.updateLastLoginTime(user.getId());
// 构建返回DTO
JDWAUserDTO userDTO = new JDWAUserDTO();
userDTO.setId(user.getId());
userDTO.setUsername(user.getUsername());
userDTO.setEmail(user.getEmail());
userDTO.setRole(user.getRole());
return new JDWALoginResponse(token, userDTO);
} catch (Exception e) {
throw e;
}
}
/**
* 认证用户
* @param username 用户名
* @param password 密码
* @throws Exception 认证异常
*/
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new Exception("用户已禁用", e);
} catch (BadCredentialsException e) {
throw new Exception("用户名或密码错误", e);
}
}
}
4.10 控制器实现
src/main/java/com/jdwa/login/controller/JDWALoginController.java:
package com.jdwa.login.controller;
import com.jdwa.login.dto.JDWALoginRequest;
import com.jdwa.login.dto.JDWALoginResponse;
import com.jdwa.login.dto.JDWAResponse;
import com.jdwa.login.service.JDWAUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api")
public class JDWALoginController {
@Autowired
private JDWAUserService userService;
/**
* 用户登录接口
* @param loginRequest 登录请求
* @return 登录响应
*/
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody JDWALoginRequest loginRequest) {
try {
JDWALoginResponse response = userService.login(loginRequest);
return ResponseEntity.ok(JDWAResponse.success("登录成功", response));
} catch (Exception e) {
return ResponseEntity.badRequest().body(JDWAResponse.fail(e.getMessage()));
}
}
/**
* 用户信息接口(需要认证)
* @return 用户信息
*/
@GetMapping("/user/info")
public ResponseEntity<?> getUserInfo() {
return ResponseEntity.ok(JDWAResponse.success("获取成功", "当前用户已认证"));
}
}
4.11 应用程序入口类
src/main/java/com/jdwa/login/JDWALoginApplication.java:
package com.jdwa.login;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JDWALoginApplication {
public static void main(String[] args) {
SpringApplication.run(JDWALoginApplication.class, args);
}
}
5. 项目部署
5.1 前端打包部署
前端项目打包步骤:
# 进入前端项目目录
cd frontend
# 安装依赖
npm install
# 构建生产环境版本
npm run build
构建完成后,dist 目录中的文件即为可部署的静态资源。可以使用Nginx或其他Web服务器进行部署。
Nginx配置示例
server {
listen 80;
server_name your-domain.com;
# 前端静态资源
location / {
root /path/to/frontend/dist;
index index.html;
try_files $uri $uri/ /index.html; # 支持Vue Router的history模式
}
# API代理
location /api/ {
proxy_pass http://localhost:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.2 后端打包部署
后端项目打包步骤:
# 进入后端项目目录
cd backend
# 使用Maven打包
mvn clean package -DskipTests
打包完成后,target 目录中的 jdwa-login-0.0.1-SNAPSHOT.jar 即为可部署的JAR包。
运行JAR包
java -jar target/jdwa-login-0.0.1-SNAPSHOT.jar
使用环境变量配置
可以通过环境变量覆盖配置,例如数据库连接信息:
java -jar target/jdwa-login-0.0.1-SNAPSHOT.jar \
--spring.datasource.url=jdbc:mysql://production-db:3306/jdwa_login \
--spring.datasource.username=prod_user \
--spring.datasource.password=prod_password
6. 项目测试
6.1 登录测试
测试登录接口:
curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"123456"}' http://localhost:8080/api/login
预期返回:
{
"success": true,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzUxMiJ9...",
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"role": "ADMIN"
}
}
}
6.2 访问受保护资源测试
测试需要认证的接口:
curl -X GET -H "Authorization: Bearer YOUR_TOKEN_HERE" http://localhost:8080/api/user/info
预期返回:
{
"success": true,
"message": "获取成功",
"data": "当前用户已认证"
}
7. 总结与扩展
7.1 项目总结
本项目展示了如何使用Vue 3和Spring Boot构建一个前后端分离的登录认证系统。主要技术要点包括:
前端方面:
- Vue 3组合式API的使用
- Vue Router实现路由控制和路由守卫
- Pinia管理应用状态
- Axios处理HTTP请求
- Element Plus提供UI组件
后端方面:
- Spring Boot构建RESTful API
- Spring Security实现认证和授权
- JWT实现无状态令牌认证
- MyBatis实现数据库访问
- 统一响应格式和异常处理
7.2 项目扩展
本项目提供了基础的登录功能,可以扩展以下功能:
用户管理功能:
- 用户注册
- 用户信息修改
- 权限管理
安全性增强:
- 密码策略(强度要求、定期更换)
- 登录失败限制(防止暴力破解)
- 验证码/图形验证
- 双因素认证
日志和监控:
- 用户操作日志
- 系统监控和告警
前端体验优化:
- 记住登录状态
- 多主题支持
- 国际化支持
部署和运维:
- Docker容器化
- CI/CD流程
- 自动化测试
7.3 学习建议
对于初学者,建议分步骤学习:
- 先掌握单独的前端或后端技术
- 理解HTTP和RESTful API的基本原理
- 学习认证和授权的基本概念(Cookie、Session、Token、JWT等)
- 分阶段实现功能,从简单到复杂
- 重视安全性和代码质量
通过本项目的学习,可以掌握主流的前后端技术栈和实现方法,为进一步学习和开发打下良好基础。
项目源码
如需参考完整实现或进行实践,请通过以下途径获取源码:
- GitHub:JDWA-vue-spring-boot-login
- Gitee(国内推荐):vue-spring-boot-login
- 蓝奏云(直接下载):vue-spring-boot-login.zip(密码:jdwa)
欢迎Star和Fork,如有问题可在仓库中提交Issue!
8. 常见问题解答(FAQ)
8.1 前后端路径匹配问题
问题:为什么要修改前端请求路径和Vite代理配置?
当在前后端分离项目中遇到路径不匹配问题时,通常需要调整以下配置:
前端请求路径:
// 修改前:直接请求/login const response = await axios.post('/login', { username, password }) // 修改后:请求/api/login const response = await axios.post('/api/login', { username, password })原因:后端在
application.properties中配置了server.servlet.context-path=/api,这意味着所有后端接口的URL都会自动添加/api前缀。如果前端发送请求到/login,后端实际期望的完整路径是/api/login,导致路径不匹配。Vite代理配置:
// 修改前:重写路径,删除/api前缀 proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } // 修改后:保留路径,不做重写 proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path } }原因:原配置中的
rewrite: (path) => path.replace(/^\/api/, '')会将所有以/api开头的请求路径中的/api前缀删除。例如,请求/api/login会被改写为/login然后发送到目标服务器。但由于后端已经配置了server.servlet.context-path=/api,再删除这个前缀会导致请求发送到错误的路径。
实际请求流程:
- 修改前:前端请求
/api/login→ 代理重写为/login→ 发送到后端http://localhost:8080/login(错误路径) - 修改后:前端请求
/api/login→ 代理保持不变 → 发送到后端http://localhost:8080/api/login(正确路径)
8.2 Spring Security工作原理
问题:JDWASecurityConfig如何控制请求访问权限?
Spring Security的工作原理可以类比为一个建筑物的安全系统:
安全系统整体架构:
- 整个Web应用相当于一栋大楼
- 不同的URL路径相当于不同的房间/区域
JDWASecurityConfig相当于大楼的总安保系统- JWT令牌相当于电子门禁卡
权限配置规则:
.antMatchers("/api/login").permitAll() // 登录入口对所有人开放 .antMatchers("/api/admin/**").hasRole("ADMIN") // 管理区域只有管理员可进入 .anyRequest().authenticated() // 其他区域需要有效门禁卡请求处理流程:
- 当请求到达后端时,首先经过Servlet容器
- 由于配置了
server.servlet.context-path=/api,Spring Boot接收到的实际路径会去掉这个前缀 - 请求进入Spring Security过滤器链
- 过滤器链检查请求URL是否在允许访问的列表中
- 对于
/api/login请求,找到匹配的规则permitAll(),允许无需认证访问 - 对于其他路径,检查是否有有效的JWT令牌和所需的角色权限
角色与权限:
.antMatchers("/api/admin/**").hasRole("ADMIN")表示所有以/api/admin/开头的URL路径只有拥有ADMIN角色的用户才能访问- Spring Security会自动将
ADMIN转换为ROLE_ADMIN(内部添加ROLE_前缀) - 即使用户已登录,如果没有所需角色,也会返回403禁止访问错误
自动配置机制:
- Spring Security通过注解自动应用安全配置
@Configuration和@EnableWebSecurity标记配置类- 配置类继承
WebSecurityConfigurerAdapter并重写配置方法 - Spring Boot在启动时自动构建安全过滤器链
- 过滤器链拦截所有HTTP请求并应用安全规则
通过正确配置前后端的路径和安全规则,可以确保请求正确到达后端接口,并实现精细的权限控制。
本文档由记得晚安(JDWA)创建于2025-05-08,最后更新于2025-05-10。