目录
一、前言
突然想搭建一个前后端的框架,以后在次基础上扩展、学习其它技术也方便点,也给自己一个学习、查漏补缺的过程吧。
此项目是在我上一个文章:Spring Boot整合MyBatis+MySQL教程(附详细代码)的基础上开发的,有需要的同学可以关注查看一下。
(注:源码我会在文章结尾提供gitee连接,需要的同学可以去自行下载)
二、后端搭建
1.实现JwtUtil类
1.什么是jwt?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用之间安全地传递声明(claims)。它以一种紧凑且自包含的方式,将用户信息、权限或其他数据通过数字签名或加密进行传输。
核心作用:
- 身份验证:用户登录后,服务器返回一个 JWT,客户端在后续请求中携带该 token。
- 信息交换:安全地在各方之间传递结构化数据(如用户信息、权限等)。
- 无状态认证:服务端无需保存 session,所有认证信息都包含在 token 中
2.具体实现
暂时只实现了生成Jwt令牌和从Jwt令牌中解析出用户名两个方法。
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
/**
* JwtUtil 是一个工具类,用于生成和解析 JWT(JSON Web Token)。
* 主要功能包括:
* - 生成带有用户名和过期时间的 JWT 令牌
* - 从 JWT 令牌中解析出用户名
*
* 注意事项:
* - 密钥(SECRET_KEY)应通过配置文件管理,避免硬编码
* - 过期时间(EXPIRATION)可按业务需求调整
*/
public class JwtUtil {
/**
* JWT 签名所使用的密钥。
* 在生产环境中建议使用更安全的方式存储,如配置中心或环境变量。
*/
public static final Key SIGNING_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512); /**
* JWT 令牌的有效期,单位为毫秒。
* 当前设置为 24 小时(86,400,000 毫秒)。
*/
private static final long EXPIRATION = 86400000; // 24小时
/**
* 生成 JWT 令牌。
*
* @param username 用户名,作为 JWT 的 subject 字段
* @return 返回生成的 JWT 字符串
*/
public static String generateToken(String username) {
return Jwts.builder()
// 设置 JWT 的主题(通常为用户标识)
.setSubject(username)
// 设置 JWT 的过期时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
// 使用 HS512 算法签名,并指定密钥
.signWith(SIGNING_KEY)
// 构建并返回紧凑格式的 JWT 字符串
.compact();
}
/**
* 从 JWT 令牌中解析出用户名。
*
* @param token 需要解析的 JWT 字符串
* @return 解析出的用户名(subject)
* @throws JwtException 如果 token 无效或签名不匹配会抛出异常
*/
public static String parseUsername(String token) {
return Jwts.parser()
// 设置签名验证所使用的密钥
.setSigningKey(SIGNING_KEY)
// 解析并验证 JWT 令牌
.parseClaimsJws(token)
// 获取 JWT 中的负载(claims),并提取 subject(用户名)
.getBody()
.getSubject();
}
}
2.配置跨域类
1.什么是跨域?为什么需要跨域?
跨域(Cross-Origin)是浏览器的一种安全机制,称为 同源策略(Same-Origin Policy)。它限制了不同源之间的资源访问,防止恶意网站通过脚本访问其他网站的敏感数据。两个 URL 的协议、域名、端口三个部分完全相同,才被认为是同源。
2.具体实现
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置类
*/
@Configuration
@EnableWebMvc
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 匹配所有以 /api 开头的接口
.allowedOrigins("http://localhost:9527") // 前端地址
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")//允许前端发送哪些请求头
.allowCredentials(true);//是否允许发送 Cookie 或认证信息(如 session、token)。
}
}
3.登录接口实现
1.LoginController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.wal.userdemo.DTO.req.LoginReq;
import org.wal.userdemo.service.UserService;
import org.wal.userdemo.utils.JwtUtil;
import org.wal.userdemo.utils.Result;
@RestController
@RequestMapping("/api/auth")
public class LoginController {
@Autowired
private UserService userService;
@PostMapping("/login")
public Result<?> login(@RequestBody LoginReq request) {
// 验证账号密码
Integer result =userService.checkUser(request.getUsername(), request.getPassword());
if (result == 1) {
String token = JwtUtil.generateToken(request.getUsername());
return Result.success(token);
} else {
return Result.error("用户名或密码错误");
}
}
}
2.UserService.java
import org.wal.userdemo.entity.UserEntity;
import java.util.List;
public interface UserService {
/**
* 登录校验
* @param username
* @param password
* @return
*/
Integer checkUser(String username, String password);
//省略其他接口信息。。。。。。
}
3.UserServiceImpl.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.mapper.UserMapper;
import org.wal.userdemo.service.UserService;
import java.util.Collections;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public Integer checkUser(String username, String password) {
return userMapper.checkUser(username, password);
}
//省略其他接口实现。。。。。。
}
4.mapper.java
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.wal.userdemo.entity.UserEntity;
import java.util.List;
@Mapper
public interface UserMapper {
/**
* 登录校验
* @param username
* @param password
* @return
*/
Integer checkUser(@Param("username") String username, @Param("password") String password);
//省略其他mapper接口。。。。。。
}
5.mapper.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="org.wal.userdemo.mapper.UserMapper">
<resultMap id="BaseResultMap" type="org.wal.userdemo.entity.UserEntity">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="age" property="age" />
</resultMap>
<select id="checkUser" resultType="java.lang.Integer" parameterType="String">
select count(*) from user where name = #{username} and password = #{password} and del_flag = 0;
</select>
//省略其他mapper.xml的代码。。。。。。
</mapper>
6.校准application.yml
server:
# 端口
port: 8080
spring:
datasource:
# 数据库连接地址
url: jdbc:mysql://127.0.0.1:3306/wal?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
# mybatis 配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.wal.userdemo.entity
# 日志
logging:
level:
org.wal.userdemo.mapper: debug
7.后端架构示例
UserDemo/
├── src/
│ └── main/
│ ├── java/
│ │ └── org.wal.userdemo/ # 主包
│ │ ├── UserDemoApplication.java # 启动类
│ │ │
│ │ ├── config/ # 配置类
│ │ │ └── CorsConfig.java # 跨域配置
│ │ │
│ │ ├── controller/ # 控制器层(接收 HTTP 请求)
│ │ │ ├── LoginController.java
│ │ │ └── UserController.java # 用户相关接口
│ │ │
│ │ ├── service/ # 业务逻辑层
│ │ │ ├── UserService.java # 接口
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ │
│ │ ├── mapper/ # 数据访问层(MyBatis Mapper)
│ │ │ └── UserMapper.java
│ │ │
│ │ ├── entity/ # 实体类(与数据库表对应)
│ │ │ └── User.java
│ │ │
│ │ ├── dto/
│ │ │ └── req/ # 请求参数 DTO 目录
│ │ │ └── LoginReq.java # 登录请求参数对象
│ │ │
│ │ └── utils/ # 工具类和通用组件
│ │ ├── JwtUtil.java # JWT 工具类
│ │ └── Result.java # 统一返回结果封装类
│ │
│ └── resources/
│ ├── application.yml # 配置文件
│ ├── mapper/
│ │ └── UserMapper.xml # MyBatis XML 映射文件
│ └── logback-spring.xml # 日志配置(可选)
│
└── pom.xml # Maven 项目配置
到这里后端就告一段落了,暂时就这些,后续有需要会再添加和配置。
三、前端搭建
1.初始化vue
1.安装 Vue CLI
1.打开终端,切换到userdemo项目目录下执行以下命令:
npm install -g @vue/cli
2.创建新项目,执行以下命令:
vue create userdemo-vue
选择默认配置即可,或者手动选择 Babel、Router 等功能。
(稍等片刻等待加载依赖、目录)
这是 Vue CLI 提供的标准命令,用于快速搭建基于 Webpack、Babel、Vue Router 等的现代前端开发环境。使用这个命令:
- 会自动为你配置好开发服务器、构建工具、ESLint、TypeScript 支持等;
- 可以选择默认配置或手动选择功能(如 Babel、Router、Vuex、CSS 预处理器等);
- 保证项目结构标准化,方便后续维护和部署。
2.安装 Element UI
1.安装依赖
npm install element-ui --save
2.在 main.js 中引入 Element UI:
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
3.安装 Axios(用于调用后端 API),执行以下命令:
npm install axios --save
(注:安装过程中的npm版本问题,可以更换npm的版本后再执行命令)
2.配置
1.配置端口、跨域
1.打开vue.config.js,配置端口、跨域代理信息
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 9527,
proxy: { // 配置代理,以api为前缀的请求会转发到 target 属性配置的代理服务器上
'/api': {
target: 'http://localhost:8080', // 后端服务地址和端口
changeOrigin: true, // 是否跨域
}
}
}
})
2.配置挂载、请求拦截器
1.打开main.js,配置挂载全局的示例、路由、拦截器信息等
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import router from './router'
import axios from 'axios'
Vue.use(ElementUI);
Vue.config.productionTip = false
new Vue({
router, // 必须挂载 router 实例
render: h => h(App),
}).$mount('#app')
// 创建一个 axios 实例
const apiClient = axios.create({
baseURL: '/api',
})
// 请求拦截器
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {//token为登录接口返回,如果token不存在说明此次请求为登录请求或者外部的非法请求
config.headers['Authorization'] = 'Bearer ' + token
}
return config
})
export default apiClient
3.页面实现
1.改造项目入口页面App.vue
App.vue是vue初始化生成页面,在main.js中已经引入并且挂载初始化。
1.稍微改造入口页面(和初始化区别不大,有需要就自行修改)
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
body {
margin: 0 !important;
}
</style>
2.初始化登录、首页、用户页面
1.在src下创建view目录用来存放页面(结构可自由选择)
2.在view目录下创建login.vue
<template>
<el-container style="height: 100vh;">
<el-main class="login-main">
<el-row type="flex" justify="center" align="middle" style="height: 100%;">
<el-col :xs="20" :sm="12" :md="8" :lg="6" :xl="4">
<el-card class="login-card">
<div slot="header" class="login-header">
<h2>用户管理平台</h2>
</div>
<el-form ref="form" :model="formData" label-width="80px" :rules="rules">
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" show-password placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="login" style="width: 100%;">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script>
import axios from 'axios'
export default {
name: 'UserLogin',
data() {
return {
formData: {
username: '',
password: ''
},
rules: {
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
],
}
};
},
methods: {
async login() {
this.$refs.form.validate(async valid => {
if (valid) {
try {
const res = await axios.post('/api/auth/login', this.formData)
console.table(res.data);
if (res.data.code === 200) {
const token = res.data.data
localStorage.setItem('token', token)
this.$router.push('/')
this.$message.success('登录成功!');
} else {
this.$message.error(res.data.message);
}
} catch (error) {
this.$message.error('请求失败,请检查网络或服务端状态')
}
} else {
return false;
}
});
}
}
};
</script>
<style scoped>
.login-main {
background: linear-gradient(to right, #e0f7fa, #fffde7);
/* 柔和渐变背景 */
}
.login-card {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
.login-header {
text-align: center;
margin-bottom: 20px;
}
</style>
3.在view下创建index.vue(首页兼主要前端布局)
<template>
<el-container class="home-container">
<!-- 左侧区域 -->
<el-aside class="left-section" :width="'12%'">
<!-- 左上部分:logo + 标题 -->
<div class="top-left">
<div class="logo-container">
<img src="../assets/logo.png" alt="logo">
</div>
<h1>我的管理系统</h1>
</div>
<!-- 左下部分:菜单 -->
<el-menu
default-active="1"
class="sidebar-menu"
:collapse="isCollapse"
:collapse-transition="false"
@open="handleOpen"
@close="handleClose"
background-color="#304156"
text-color="#fff"
active-text-color="#ffd04b"
>
<el-submenu index="1">
<template #title>
<i class="el-icon-document"></i>
<span>内容管理</span>
</template>
<el-menu-item index="1-1">文章列表</el-menu-item>
<el-menu-item index="1-2">新增文章</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template #title>
<i class="el-icon-user"></i>
<span>用户管理</span>
</template>
<el-menu-item index="2-1">用户列表</el-menu-item>
</el-submenu>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">导航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">导航四</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 右侧区域 -->
<el-container class="right-section">
<!-- 右上部分:顶部导航 -->
<el-header class="top-right-header">
<div class="header-right">
<span>欢迎,Admin</span>
<el-button type="text" @click="logout">退出</el-button>
</div>
</el-header>
<!-- 右下部分:主内容区域 -->
<el-main class="main-content">
<!-- <router-view /> -->
<user/>
</el-main>
</el-container>
</el-container>
</template>
<script>
import user from './user.vue'
export default {
name: 'userIndex',
components: { user },
data() {
return {
isCollapse: false, // 默认展开
};
},
methods: {
logout() {
localStorage.removeItem('token') // 清除 token
this.$router.push('/login');
},
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
},
};
</script>
<style scoped>
.home-container {
height: 100vh;
}
/* 左侧整体样式 */
.left-section {
display: flex;
flex-direction: column;
background-color: #304156;
color: white;
padding: 10px;
width: 50px;
}
/* 左上角 logo 和标题 */
.top-left {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.logo-container {
margin-right: 10px;
}
.logo-container img {
height: 36px;
width: auto;
object-fit: contain;
}
.top-left h1 {
font-size: 18px;
margin: 0;
color: white;
}
/* 菜单样式 */
.sidebar-menu {
flex: 1;
border-right: none;
}
/* 右侧整体样式 */
.right-section {
display: flex;
flex-direction: column;
}
/* 右上角导航栏 */
.top-right-header {
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #ffffff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 0 20px;
}
.header-right {
display: flex;
align-items: center;
}
/* 主内容区域 */
.main-content {
padding: 20px;
}
</style>
4.在view目录下创建user.vue
<template>
<div>user页面开发中。。。。。。</div>
</template>
<script>
export default {
name: 'userView',
}
</script>
<style scoped>
</style>
(注:用户页面后续文章会具体讲解,需要的同学可以关注我)
3.配置路由信息
1.打开src下router下的index.js(已经在main.js中挂载)
2.配置路由、路由守卫信息等(根据需要自行配置)
import Vue from 'vue'
import Router from 'vue-router'
import Login from '../view/login.vue' // 替换为你自己的登录页组件
import Index from '../view/index.vue'
import user from '../view/user.vue'
Vue.use(Router)
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Index',
component: Index,
// 如果你想让首页重定向到其他路径,可以加 redirect
},
{
path: '/index',
redirect: '/'
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/user',
name: 'user',
component: user,
meta: { requiresAuth: true } // 表示该页面需要登录才能访问
}
]
})
// 全局前置守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = !!localStorage.getItem('token') // 假设你用 token 判断登录状态
console.log("to.name:" + to.name);
console.log("isAuthenticated:" + isAuthenticated)
console.log("to.path" + to.path);
if (to.path === '/'|| to.path === '/index' || to.name === 'Index'
|| to.matched.some(record => record.meta.requiresAuth)) {
// 如果目标路由是 Index 或者需要登录才能访问
if (isAuthenticated) {
next() // 已登录,允许访问
} else {
next({ name: 'Login' }) // 未登录,跳转到登录页
}
} else {
console.log("不需要验证的页面")
next() // 不需要登录验证的页面直接放行
}
})
export default router//导出,让项目中其他地方可以使用
4.前端架构示例
userdemo-vue/
├── public/ # 静态资源(不会被 webpack 处理)
│ └── favicon.ico
├── src/ # 源码目录
│ ├── assets/ # 图片、字体等静态资源(会被 webpack 处理)
│ │ └── logo.png
│ ├── components/ # 可复用的组件(如公共头部、按钮等)
│ │ └── ...
│ ├── view/ # 页面视图组件
│ │ ├── login.vue # 登录页
│ │ ├── index.vue # 首页
│ │ └── user.vue # 用户页
│ ├── router/ # 路由配置
│ │ └── index.js # 路由表及导航守卫
│ ├── App.vue # 根组件
│ └── main.js # 入口文件(初始化 Vue 实例)
├── vue.config.js # Vue CLI 配置(代理、端口等)
├── package.json # 项目描述和依赖
└── README.md # 项目说明文档
四、附:源码
1.源码下载地址
https://gitee.com/wangaolin/user-demo.git
同学们有需要可以自行下载查看,此文章是dev-vue分支。
五、结语
通过搭建这个前后端框架,不仅巩固了Spring Boot、MyBatis和MySQL的技术栈,也为后续扩展和学习新技术提供了基础。项目源码已上传至Gitee,有需要的同学可以自行下载参考。
希望这个框架能帮助大家更快上手开发,也欢迎一起交流改进。
持续学习,不断进步,共勉!