视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在前三篇中,我们已经完成了:
- ✅后端 API(Spring Boot)
- ✅前台展示(Vue 3)
- ✅全栈整合与部署(Nginx + HTTPS)
但作为一个真正的“个人博客”,你肯定不希望每次发文章都要手动插数据库!
现在,是时候打造一个专属后台管理系统——让你能登录、写文章、管理分类,像 WordPress 一样丝滑操作!
本篇将手把手教你实现:用户登录 + JWT 鉴权 + 富文本编辑 + 文章管理,真正掌控你的博客内容。
一、为什么需要后台管理?
| 场景 | 痛点 | 解决方案 |
|---|---|---|
| 想发新文章 | 要连数据库写 SQL | 后台表单提交 |
| 修改错别字 | 无法在线编辑 | 富文本编辑器 |
| 分类混乱 | 手动改 category_id | 分类下拉选择 |
| 安全风险 | 任何人都能删文章 | 登录 + 权限控制 |
💡 目标:只有你自己能进后台,其他人只能看前台!
二、功能需求
- 管理员登录/登出
- 文章列表页(带分页、搜索、删除)
- 新增/编辑文章(支持 Markdown 或富文本)
- 分类管理(增删改查)
- JWT Token 鉴权(保护所有后台接口)
三、技术选型升级
| 模块 | 技术 |
|---|---|
| 后端鉴权 | Spring Security + JWT |
| 前端 UI | Element Plus(Vue 3 官方合作组件库) |
| 富文本 | @wangeditor/editor-for-vue(轻量、国产、好用) |
| 表单验证 | VeeValidate 或手动校验 |
四、后端改造:添加用户与鉴权
1. 新增管理员表(简单版,仅1个账号)
CREATE TABLE blog_admin ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存储 BCrypt 加密后的密码 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 插入初始管理员(密码:123456,加密后) INSERT INTO blog_admin (username, password) VALUES ('admin', '$2a$10$DdFvR7sZ9KzXqQlLbUeBFeuT8rWJvOyYkGzVxHmIjKlMnOpQrStUv');🔐 密码生成方式(Java 测试类):
System.out.println(new BCryptPasswordEncoder().encode("123456"));
2. 添加 JWT 工具类
// src/main/java/com/example/blog/util/JwtUtil.java @Component public class JwtUtil { private String secret = "myBlogSecretKey2026"; // 实际项目建议从配置读取 private long expire = 24 * 60 * 60 * 1000; // 24小时 public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public String getUsernameFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } }⚠️ 依赖:需引入
io.jsonwebtoken:jjwt
3. 登录接口
@RestController @RequestMapping("/api/admin") public class AdminController { @Autowired private AdminService adminService; @PostMapping("/login") public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials) { String username = credentials.get("username"); String password = credentials.get("password"); if (adminService.authenticate(username, password)) { String token = adminService.generateToken(username); Map<String, Object> resp = new HashMap<>(); resp.put("token", token); resp.put("message", "登录成功"); return ResponseEntity.ok(resp); } return ResponseEntity.status(401).body(Map.of("error", "用户名或密码错误")); } }4. 鉴权拦截器(保护后台接口)
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); if (jwtUtil.validateToken(token)) { return true; } } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } } // 注册拦截器 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/admin/**") // 拦截所有后台接口 .excludePathPatterns("/api/admin/login"); } }✅ 所有
/api/admin/**接口(除登录外)都需携带Authorization: Bearer <token>。
五、前端改造:后台管理页面
1. 安装依赖
npm install element-plus @wangeditor/editor @wangeditor/editor-for-vue axios2. 创建后台布局(AdminLayout.vue)
<!-- src/views/admin/AdminLayout.vue --> <template> <el-container style="height: 100vh"> <el-aside width="200px" style="background: #2c3e50; color: white;"> <div style="padding: 20px; font-size: 18px;">博客后台</div> <el-menu background-color="#2c3e50" text-color="#fff" active-text-color="#ffd04b" router > <el-menu-item index="/admin/posts">文章管理</el-menu-item> <el-menu-item index="/admin/categories">分类管理</el-menu-item> <el-menu-item index="/admin/logout" @click="logout">退出登录</el-menu-item> </el-menu> </el-aside> <el-main> <router-view /> </el-main> </el-container> </template> <script setup> import { useRouter } from 'vue-router'; const router = useRouter(); function logout() { localStorage.removeItem('admin_token'); router.push('/admin/login'); } </script>3. 登录页(AdminLogin.vue)
<template> <div class="login-container"> <el-card style="width: 400px;"> <h2>管理员登录</h2> <el-form @submit.prevent="handleLogin"> <el-form-item> <el-input v-model="form.username" placeholder="用户名" /> </el-form-item> <el-form-item> <el-input v-model="form.password" type="password" placeholder="密码" /> </el-form-item> <el-button type="primary" native-type="submit" :loading="loading">登录</el-button> </el-form> </el-card> </div> </template> <script setup> import { ref } from 'vue'; import { useRouter } from 'vue-router'; import api from '@/api'; const form = ref({ username: 'admin', password: '' }); const loading = ref(false); const router = useRouter(); async function handleLogin() { loading.value = true; try { const res = await api.post('/admin/login', form.value); localStorage.setItem('admin_token', res.data.token); router.push('/admin/posts'); } catch (err) { ElMessage.error('登录失败'); } finally { loading.value = false; } } </script> <style scoped> .login-container { display: flex; justify-content: center; align-items: center; height: 100vh; background: #f5f5f5; } </style>4. 请求拦截器:自动携带 Token
// src/api/index.js(补充) api.interceptors.request.use(config => { const token = localStorage.getItem('admin_token'); if (token && config.url?.startsWith('/admin')) { config.headers.Authorization = `Bearer ${token}`; } return config; });5. 文章编辑页(使用 WangEditor)
<template> <el-card> <el-form :model="post" label-width="80px"> <el-form-item label="标题"> <el-input v-model="post.title" /> </el-form-item> <el-form-item label="分类"> <el-select v-model="post.categoryId" placeholder="请选择"> <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" /> </el-select> </el-form-item> <el-form-item label="内容"> <div style="border: 1px solid #ccc;"> <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" /> <Editor v-model="post.content" :defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" /> </div> </el-form-item> <el-button type="primary" @click="savePost">保存</el-button> </el-form> </el-card> </template> <script setup> import { onMounted, ref, shallowRef } from 'vue'; import { Editor, Toolbar } from '@wangeditor/editor-for-vue'; import api from '@/api'; const props = defineProps({ id: [String, Number] }); const post = ref({ title: '', content: '', categoryId: null }); const categories = ref([]); const editorRef = shallowRef(); const mode = 'default'; const toolbarConfig = {}; const editorConfig = { placeholder: '请输入内容...' }; onMounted(async () => { // 加载分类 const catRes = await api.get('/categories'); // 假设你已提供分类接口 categories.value = catRes.data; // 如果是编辑,加载文章 if (props.id) { const res = await api.get(`/admin/posts/${props.id}`); post.value = res.data; } }); function handleCreated(editor) { editorRef.value = editor; } async function savePost() { if (props.id) { await api.put(`/admin/posts/${props.id}`, post.value); } else { await api.post('/admin/posts', post.value); } ElMessage.success('保存成功'); } </script>📌 注意:你需要在后端新增
/api/admin/posts的增删改接口,并加上@PreAuthorize("hasRole('ADMIN')")或拦截器保护。
六、反例 & 注意事项
❌ 反例:Token 存在 Cookie 且未设 HttpOnly
- 容易被 XSS 窃取。
- ✅ 正确做法:存 localStorage + HTTPS + 短期 Token。
❌ 反例:富文本内容直接v-html渲染(前台)
- 若内容来自后台(可信),可接受;
- 若未来开放评论,则必须过滤(如
DOMPurify.sanitize())。
⚠️ 注意事项
- 密码安全:永远不要明文存储,用 BCrypt;
- Token 刷新:本例 Token 24 小时过期,简单场景够用;
- 权限粒度:目前只有一个管理员,无需复杂 RBAC;
- CSRF:因使用 Token 而非 Cookie,天然免疫 CSRF。
七、最终效果
- 访问
https://your-domain.com/admin/login - 输入账号密码 → 进入后台
- 点击“新建文章” → 使用富文本编辑器写作
- 保存后,前台首页立即更新!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!