本项目是一个结合springboot+jwt+shiro+vue+elementUI+axios+redis+mysql这些技术的一个练手的简单的博客系统
/*
Navicat MySQL Data Transfer
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : vueblog
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 08/02/2023 08:53:31
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for m_user
-- ----------------------------
DROP TABLE IF EXISTS `m_user`;
CREATE TABLE `m_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `UK_USERNAME`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_user
-- ----------------------------
INSERT INTO `m_user` VALUES (1, 'admin', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', '96e79218965eb72c92a549dd5a330112');
INSERT INTO `m_user` VALUES (14, '123', 'https://pic4.zhimg.com/v2-797973e16edcd0ccaab44cfbfa08d2d3_r.jpg', '202cb962ac59075b964b07152d234b70');
INSERT INTO `m_user` VALUES (15, '123456', 'https://p0.ssl.img.360kuai.com/t01948ff2341a5d1ac3.jpg', 'e10adc3949ba59abbe56e057f20f883e');
SET FOREIGN_KEY_CHECKS = 1;
/*
Navicat MySQL Data Transfer
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : vueblog
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 08/02/2023 08:53:40
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for m_blog
-- ----------------------------
DROP TABLE IF EXISTS `m_blog`;
CREATE TABLE `m_blog` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`created` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
`status` tinyint NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_blog
-- ----------------------------
INSERT INTO `m_blog` VALUES (1, 1, '生活就像海洋,只有意志坚强的人才能到达彼岸', '这里是摘要哈哈哈', '内容???', '2020-05-21 22:08:42', 0);
INSERT INTO `m_blog` VALUES (2, 1, '最值得学习的博客项目eblog', 'eblog是一个基于Springboot2.1.2开发的博客学习项目,为了让项目融合更多的知识点,达到学习目的,编写了详细的从0到1开发文档。主要学习包括:自定义Freemarker标签,使用shiro+redis完成了会话共享,redis的zset结构完成本周热议排行榜,t-io+websocket完成即时消息通知和群聊,rabbitmq+elasticsearch完成博客内容搜索引擎等。值得学习的地方很多!', '**推荐阅读:**\r\n\r\n[分享一套SpringBoot开发博客系统源码,以及完整开发文档!速度保存!](https://mp.weixin.qq.com/s/jz6e977xP-OyaAKNjNca8w)\r\n\r\n[Github上最值得学习的100个Java开源项目,涵盖各种技术栈!](https://mp.weixin.qq.com/s/N-U0TaEUXnBFfBsmt_OESQ)\r\n\r\n[2020年最新的常问企业面试题大全以及答案](https://mp.weixin.qq.com/s/lR5LC5GnD2Gs59ecV5R0XA)', '2020-05-28 09:36:38', 0);
INSERT INTO `m_blog` VALUES (3, 1, '关注公众号JavaCat,回复xshell或navicat获取破解对应工具', '视频中所用到的xshell和navicat直接获取哈!', '### 工具获取\r\n\r\n* xshell 6 绿色破解版:关注公众号:JavaCat,回复 xshell 获取\r\n* Navicat 11 简体中文版:关注公众号:JavaCat,回复 navicat 获取\r\n\r\n公众号二维码:\r\n\r\n[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QzWS88Se-1675817747671)(//image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20201020/7fa16a1f957f4cfebe7be1f6675f6f36.png \"JavaCat\")]\r\n\r\n直接扫码回复对应关键字\r\n\r\n**推荐阅读:**\r\n\r\n[B站86K播放量,SpringBoot+Vue前后端分离完整入门教程!](https://mp.weixin.qq.com/s/jGEkHTf2X8l-wUenc-PpEw)\r\n\r\n[分享一套SpringBoot开发博客系统源码,以及完整开发文档!速度保存!](https://mp.weixin.qq.com/s/jz6e977xP-OyaAKNjNca8w)\r\n\r\n[Github上最值得学习的100个Java开源项目,涵盖各种技术栈!](https://mp.weixin.qq.com/s/N-U0TaEUXnBFfBsmt_OESQ)\r\n\r\n[2020年最新的常问企业面试题大全以及答案](https://mp.weixin.qq.com/s/lR5LC5GnD2Gs59ecV5R0XA)', '2020-10-20 05:05:31', 0);
INSERT INTO `m_blog` VALUES (7, 1, '你真的会写单例模式吗?', '单例模式可能是代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对 Java 中常见的单例模式写法做了一个总结,如有错漏之处,恳请读者指正。', '> 作者:吃桔子的攻城狮 来源:http://www.tekbroaden.com/singleton-java.html\n\n\n单例模式可能是代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对 Java 中常见的单例模式写法做了一个总结,如有错漏之处,恳请读者指正。\n\n饿汉法\n===\n\n顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。代码如下:\n\n```\npublic class Singleton { \n private static Singleton = new Singleton();\n private Singleton() {}\n public static getSignleton(){\n return singleton;\n }\n}\n\n```\n\n这样做的好处是编写简单,但是无法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载,所以就需要下面的懒汉法:\n', '2020-05-22 00:42:44', 0);
INSERT INTO `m_blog` VALUES (9, 1, '真正理解Mysql的四种隔离级别@', '事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。\n\n事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所以操作。', '### 什么是事务 \n\n> 事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。\n> \n> 事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所以操作。\n\n**事务的 ACID**\n\n事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。\n\n> 1 、原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做\n> \n> 2 、一致性。事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。', '2020-05-22 22:04:46', 0);
INSERT INTO `m_blog` VALUES (10, 1, '博客项目eblog讲解视频上线啦,长达17个小时!!', '1. 慕课网免费资源好久都没更新了,新教程大都付费\n2. B站上的视频繁多,通过收藏和弹幕数量通常很容易判断出视频是否优质\n3. 讲真,B站的弹幕文化,让我觉得,我不是一个在学习,自古人才出评论。哈哈哈\n4. B站视频通常广告少,up主的用心录制,通常只为了你关注他', 'ok,再回到我们的eblog项目,源码、文档、视频我都开源出来了。来些基本操作:github上给个star,B站视频给个三连支持咧。\n\neblog源码:https://github.com/MarkerHub/eblog\n\n点击这里:[10+篇完整开发文档](https://mp.weixin.qq.com/mp/homepage?__biz=MzIwODkzOTc1MQ==&hid=1&sn=8e512316c3dfe140e636d0c996951166)\n\n[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-us5jzoND-1675817747672)(//image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20200508/c290d945b7d24c79b172759bdb5b94e0.png)]\n\n视频讲解:(记得关注我噢!)\n\nhttps://www.bilibili.com/video/BV1ri4y1x71A\n\n[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K10Mp6cC-1675817747672)(//image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20200508/983b5abc1c934360a1a1362347a275f7.png)]\n\n项目其实还很多bug的,哈哈,我还需要进行二次迭代,到时候再发迭代文档出来。\n\n关注下我的B站,作为一个自媒体的自由职业者,没有什么比涨粉更让我开心的了,嘻嘻。\n\n近期即将推出的视频教程:\n\n1. 搭建脚手架,前后端分离首秀\n2. Shiro入门到精通教程\n3. SpringBoot2.2.6最新入门教程', '2020-05-22 22:05:49', 0);
INSERT INTO `m_blog` VALUES (15, 1, '测试标题1', '描述2', '内容3', '2023-02-07 14:48:57', 0);
SET FOREIGN_KEY_CHECKS = 1;
运行结果
这两个表的关系为,m_user是用户表,然后了这个m_blog是博客表,通过博客表的user_id进行关联
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.8version>
<relativePath/>
parent>
<groupId>com.markerhubgroupId>
<artifactId>vueblogartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>vueblogname>
<description>vueblogdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.mysqlgroupId>
<artifactId>mysql-connector-jartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redis-spring-boot-starterartifactId>
<version>3.2.1version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.3.3version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>org.hibernate.validatorgroupId>
<artifactId>hibernate-validatorartifactId>
<version>6.0.0.Finalversion>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
server:
port: 8082
shiro-redis:
enabled: true
redis-manager:
host: 127.0.0.1:6379
markerhub:
jwt:
secret: f4e2e52034348f86b67cde581c0f9eb5
expire: 604800
header: Authorization
package com.markerhub.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@MapperScan("com.markerhub.mapper")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
修改对应的数据库:账号密码、数据库名、包配置(ctrl+f可找到对应位置)
代码生成器代码
package com.markerhub;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
*
* 读取控制台内容
*
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
// gc.setOutputDir("D:\\test");
gc.setAuthor("极客李华");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(null);
pc.setParent("com.markerhub");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/"
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("m_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
当前的UserController
完整代码
package com.markerhub.controller;
import cn.hutool.core.lang.Assert;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.markerhub.common.lang.Result;
import com.markerhub.entity.User;
import com.markerhub.service.UserService;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Random;
/**
*
* 前端控制器
*
*
* @author 极客李华
* @since 2023-02-07
*/
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@ResponseBody
@GetMapping("/index")
public Object index(){
User user = userService.getById(1L);
return Result.succ(200, "操作成功", user);
}
}
通过postman进行测试
这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的
package com.markerhub.common.lang;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result implements Serializable {
private int code; // 200是正常,非200表示异常
private String msg;
private Object data;
public static Result succ(Object data) {
return succ(200, "操作成功", data);
}
public static Result succ(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
public static Result fail(String msg) {
return fail(400, msg, null);
}
public static Result fail(String msg, Object data) {
return fail(400, msg, data);
}
public static Result fail(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
}
考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis。
这里使用shiro-redis来进行操作,配置简单,这里也推荐大家使用。
而因为我们需要做的是前后端分离项目的骨架,所以一般我们会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro的过程中,我们需要引入jwt的身份验证过程。
在最上面的pom.xml文件中把所有的依赖都导入了,这里再提一下是哪几个
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redis-spring-boot-starterartifactId>
<version>3.2.1version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.3.3version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
package com.markerhub.config;
import com.markerhub.shiro.AccountRealm;
import com.markerhub.shiro.JwtFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// inject redisSessionDAO
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
SessionManager sessionManager,
RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/**", "jwt");
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilter.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
路径: com.vueblog.shiro
这里填写这个自定义过滤器的条件
package com.markerhub.shiro;
import cn.hutool.core.bean.BeanUtil;
import com.markerhub.entity.User;
import com.markerhub.service.UserService;
import com.markerhub.util.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountRealm extends AuthorizingRealm {
//为了让realm支持jwt的凭证校验
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//权限校验
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//登录认证校验
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) token;
return null;
}
}
@SpringBootApplication
@MapperScan(basePackages = "com.markerhub.mapper")
public class VueblogApplication {
public static void main(String[] args) {
SpringApplication.run(VueblogApplication.class, args);
}
}
package com.markerhub.shiro;
import cn.hutool.json.JSONUtil;
import com.markerhub.common.lang.Result;
import com.markerhub.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if(StringUtils.isEmpty(jwt)) {
return null;
}
return new JwtToken(jwt);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if(StringUtils.isEmpty(jwt)) {
return true;
} else {
// 校验jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
throw new ExpiredCredentialsException("token已失效,请重新登录");
}
// 执行登录
return executeLogin(servletRequest, servletResponse);
}
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result result = Result.fail(throwable.getMessage());
String json = JSONUtil.toJsonStr(result);
try {
httpServletResponse.getWriter().print(json);
} catch (IOException ioException) {
}
return false;
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
package com.markerhub.shiro;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String jwt) {
this.token = jwt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
package com.markerhub.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* jwt工具类
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "markerhub.jwt")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
里面的内容为restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
创建类AccountProfile 用于传递数据
package com.markerhub.shiro;
import lombok.Data;
import java.io.Serializable;
@Data
public class AccountProfile implements Serializable {
private Long id;
private String username;
private String avatar;
private String email;
}
package com.markerhub.shiro;
import cn.hutool.core.bean.BeanUtil;
import com.markerhub.entity.User;
import com.markerhub.service.UserService;
import com.markerhub.util.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) token;
String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
User user = userService.getById(Long.valueOf(userId));
if (user == null) {
throw new UnknownAccountException("账户不存在");
}
AccountProfile profile = new AccountProfile();
BeanUtil.copyProperties(user, profile);
return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
}
}
创建GlobalExceptionHandler 类
捕获全局异常
package com.markerhub.common.exception;
import com.markerhub.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(value = ShiroException.class)
public Result handler(ShiroException e) {
log.error("运行时异常:----------------{}", e);
return Result.fail(401, e.getMessage(), null);
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handler(MethodArgumentNotValidException e) {
log.error("实体校验异常:----------------{}", e);
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
return Result.fail(objectError.getDefaultMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) {
log.error("Assert异常:----------------{}", e);
return Result.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handler(RuntimeException e) {
log.error("运行时异常:----------------{}", e);
return Result.fail(e.getMessage());
}
}
package com.markerhub.controller;
import cn.hutool.core.lang.Assert;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.markerhub.common.lang.Result;
import com.markerhub.entity.User;
import com.markerhub.service.UserService;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Random;
/**
*
* 前端控制器
*
*
* @author 极客李华
* @since 2023-02-07
*/
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@ResponseBody
@RequiresAuthentication
@GetMapping("/index")
public Object index(){
User user = userService.getById(1L);
return Result.succ(200, "操作成功", user);
}
}
当我们表单数据提交的时候,前端的校验我们可以使用一些类似于jQuery Validate等js插件实现,而后端我们可以使用Hibernate validatior来做校验。
我们使用springboot框架作为基础,那么就已经自动集成了Hibernate validatior。
package com.markerhub.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("m_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@NotBlank(message = "昵称不能为空")
private String username;
private String avatar;
@NotBlank(message = "密码不能为空")
private String password;
}
在UserController中写测试方法
@PostMapping("/save")
public Result save(@Validated @RequestBody User user){
return Result.succ(user);
}
因为是前后端分析,所以跨域问题是避免不了的,我们直接在后台进行全局跨域处理:
package com.markerhub.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 解决跨域问题
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
/**
* 对跨域提供支持
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
package com.markerhub.common.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
@Data
public class LoginDto implements Serializable {
@NotBlank(message = "昵称不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
/**
* 断言异常
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e){
log.error("Assert异常:------------------>{}",e);
return Result.fail(e.getMessage());
}
package com.markerhub.controller;
import cn.hutool.core.map.MapUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.markerhub.common.dto.LoginDto;
import com.markerhub.common.lang.Result;
import com.markerhub.entity.User;
import com.markerhub.service.UserService;
import com.markerhub.util.JwtUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Random;
@RestController
public class AccountController {
@Autowired
UserService userService;
@Autowired
JwtUtils jwtUtils;
// 登录处理
@PostMapping("/login")
public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {
User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
// 通过断点处理是否用户名为空
Assert.notNull(user, "用户不存在");
if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
return Result.fail("密码不正确");
}
// 获取当前user的jwt令牌
String jwt = jwtUtils.generateToken(user.getId());
// 后面返回的时候把这个令牌也返回 让令牌延期
response.setHeader("Authorization", jwt);
response.setHeader("Access-control-Expose-Headers", "Authorization");
return Result.succ(MapUtil.builder()
.put("id", user.getId())
.put("username", user.getUsername())
.put("avatar", user.getAvatar())
.map()
);
}
@RequestMapping(value = "/register",method = RequestMethod.POST)
@ResponseBody
public Result save(@RequestBody User user){
if (userService.getOne(new QueryWrapper<User>().eq("username", user.getUsername())) != null){
cn.hutool.core.lang.Assert.isNull(user, "用户已存在");
}
// 随机生成一个头像
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("https://pic3.zhimg.com/v2-d6ddf0128212235dddf76df7f4383f53.jpg");
arrayList.add("https://p0.ssl.img.360kuai.com/t01948ff2341a5d1ac3.jpg");
arrayList.add("https://pic3.zhimg.com/v2-65020b1231ba55d55ee3d6a29ff3df26_r.jpg");
arrayList.add("https://pic2.zhimg.com/v2-fc348d5e926116782149d2151dc09834.jpg");
arrayList.add("https://pic4.zhimg.com/v2-797973e16edcd0ccaab44cfbfa08d2d3_r.jpg");
Random random = new Random();
user.setAvatar(arrayList.get(random.nextInt(arrayList.size())));
System.out.println(user);
user.setPassword(SecureUtil.md5(user.getPassword()));
userService.save(user);
return Result.succ(user);
}
// 登出处理
@RequiresAuthentication
@GetMapping("/logout")
public Result logout() {
SecurityUtils.getSubject().logout();
return Result.succ(null);
}
}
package com.markerhub.util;
import com.markerhub.shiro.AccountProfile;
import org.apache.shiro.SecurityUtils;
public class ShiroUtil {
public static AccountProfile getProfile() {
return (AccountProfile) SecurityUtils.getSubject().getPrincipal();
}
}
package com.markerhub.controller;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.markerhub.common.lang.Result;
import com.markerhub.entity.Blog;
import com.markerhub.service.BlogService;
import com.markerhub.util.ShiroUtil;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
/**
*
* 前端控制器
*
*
* @author 极客李华
* @since 2023-02-07
*/
@RestController
public class BlogController {
@Autowired
BlogService blogService;
// 查询文章
// 最开始查询的时候默认值是1 防止没有数据的情况
@GetMapping("/blogs")
public Result list(@RequestParam(defaultValue = "1") Integer currentPage) {
Page page = new Page(currentPage, 5);
// 查询条件 分页的基础上 再按照创建时间排序
IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));
// 如果成功了返回分页查询的数据
return Result.succ(pageData);
}
// 根据id进行查询
@GetMapping("/blog/{id}")
public Result detail(@PathVariable(name = "id") Long id) {
Blog blog = blogService.getById(id);
// 判断文章是否被删除了
Assert.notNull(blog, "该博客已被删除");
return Result.succ(blog);
}
// 编辑博客
@RequiresAuthentication
@PostMapping("/blog/edit")
public Result edit(@Validated @RequestBody Blog blog) {
// Assert.isTrue(false, "公开版不能任意编辑!");
Blog temp = null;
// 如果博客id不为空
if(blog.getId() != null) {
temp = blogService.getById(blog.getId());
// 只能编辑自己的文章
System.out.println(ShiroUtil.getProfile().getId());
// 且是自己的文章 才可以编辑
Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(), "没有权限编辑");
} else {
// 是空的话 创建一篇
temp = new Blog();
temp.setUserId(ShiroUtil.getProfile().getId());
temp.setCreated(LocalDateTime.now());
temp.setStatus(0);
}
// 创建的时候把temp copy到 blog里面 然后忽略掉后面的那些属性
BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");
blogService.saveOrUpdate(temp);
return Result.succ(null);
}
// 删除文章
//@PathVariable动态路由
@RequiresAuthentication //需要认证之后才能操作
@PostMapping("/blogdel/{id}")
public Result del(@PathVariable Long id){
boolean b = blogService.removeById(id);
//判断是否为空 为空则断言异常
if(b==true){
return Result.succ("文章删除成功");
}else{
return Result.fail("文章删除失败");
}
}
}
代码测试
打开vue ui开始创建项目
手动创建项目
目前大概这样选择
选择vue2的版本
选择less
项目架构
├── README.md 项目介绍
├── index.html 入口页面
├── build 构建脚本目录
│ ├── build-server.js 运行本地构建服务器,可以访问构建后的页面
│ ├── build.js 生产环境构建脚本
│ ├── dev-client.js 开发服务器热重载脚本,主要用来实现开发阶段的页面自动刷新
│ ├── dev-server.js 运行本地开发服务器
│ ├── utils.js 构建相关工具方法
│ ├── webpack.base.conf.js wabpack基础配置
│ ├── webpack.dev.conf.js wabpack开发环境配置
│ └── webpack.prod.conf.js wabpack生产环境配置
├── config 项目配置
│ ├── dev.env.js 开发环境变量
│ ├── index.js 项目配置文件
│ ├── prod.env.js 生产环境变量
│ └── test.env.js 测试环境变量
├── mock mock数据目录
│ └── hello.js
├── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息
├── src 源码目录
│ ├── main.js 入口js文件
│ ├── app.vue 根组件
│ ├── components 公共组件目录
│ │ └── title.vue
│ ├── assets 资源目录,这里的资源会被wabpack构建
│ │ └── images
│ │ └── logo.png
│ ├── routes 前端路由
│ │ └── index.js
│ ├── store 应用级数据(state)状态管理
│ │ └── index.js
│ └── views 页面目录
│ ├── hello.vue
│ └── notfound.vue
├── static 纯静态资源,不会被wabpack构建。
└── test 测试文件目录(unit&e2e)
└── unit 单元测试
├── index.js 入口脚本
├── karma.conf.js karma配置文件
└── specs 单测case目录
└── Hello.spec.js
在终端中运行下面命令
cnpm install element-ui --save
然后我们打开项目src目录下的main.js,引入element-ui依赖。
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
Vue.use(Element)
然后运行测试
<div id="nav">
<router-link to="/demo">Demo页面router-link>
div>
autoOpenBrowser:true
安装命令:
cnpm install axios --save
import axios from 'axios'
//引用全局
Vue.prototype.$axios = axios
下载开始配置下面几个页面的路由
我们在views文件夹下定义几个页面:
BlogDetail.vue(博客详情页)
BlogEdit.vue(编辑博客)
Blogs.vue(博客列表)
Login.vue(登录页面)
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import Blogs from '../views/Blogs.vue'
import BlogEdit from '../views/BlogEdit.vue'
import BlogDetail from '../views/BlogDetail.vue'
import Register from "../views/Register.vue";
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Index',
redirect: {name: "Blogs"}
},
{
path: '/blogs',
name: 'Blogs',
component: Blogs
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/blog/add',
name: 'BlogAdd',
component: BlogEdit,
meta: {
requireAuth: true
}
},
{
path: '/blog/:blogId',
name: 'BlogDetail',
component: BlogDetail
},
{
path: '/blog/:blogId/edit',
name: 'BlogEdit',
component: BlogEdit,
meta: {
requireAuth: true
}
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
<template>
<div>
<el-container>
<el-header>
<img class="mlogo" src="https://pic2.zhimg.com/v2-5e049963b2180c98d5016490b85d0768_xs.jpg" alt="">
el-header>
<el-main>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username">el-input>
el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password">el-input>
el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">登录el-button>
<el-button @click="register">注册el-button>
el-form-item>
el-form>
el-main>
el-container>
div>
template>
<script>
export default {
name: "Login",
data() {
return {
ruleForm: {
username: 'admin',
password: '111111'
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请选择密码', trigger: 'change' }
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios.post('/login', this.ruleForm).then(res => {
console.log(res.data)
const jwt = res.headers['authorization']
const userInfo = res.data.data
// 把数据共享出去
_this.$store.commit("SET_TOKEN", jwt)
_this.$store.commit("SET_USERINFO", userInfo)
// 获取
console.log(_this.$store.getters.getUser)
_this.$router.push("/blogs")
})
} else {
console.log('error submit!!');
return false;
}
});
},
register(){
location.href="/register";
}
}
}
script>
<style scoped>
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
/*background-color: #E9EEF3;*/
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
.mlogo {
height: 60%;
margin-top: 10px;
}
.demo-ruleForm {
max-width: 500px;
margin: 0 auto;
}
style>
<template>
<div>
<el-container>
<el-header>
<img class="mlogo" src="https://pic2.zhimg.com/v2-5e049963b2180c98d5016490b85d0768_xs.jpg" alt="">
el-header>
<el-main>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username">el-input>
el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password">el-input>
el-form-item>
<el-form-item>
<el-button @click="submitForm('ruleForm')">注册el-button>
el-form-item>
el-form>
el-main>
el-container>
div>
template>
<script>
export default {
name: "Login",
data() {
return {
ruleForm: {
username: '',
password: ''
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请选择密码', trigger: 'change' }
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios({
method: "post",
url: "/register",
data: this.ruleForm
}).then(function (resp) {
console.log(resp.data)
const jwt = resp.headers['authorization']
const userInfo = resp.data.data
// 把数据共享出去
_this.$store.commit("SET_TOKEN", jwt)
_this.$store.commit("SET_USERINFO", userInfo)
// 获取
console.log(_this.$store.getters.getUser)
_this.$router.push("/login")
})
}
});
}}
}
script>
<style scoped>
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
/*background-color: #E9EEF3;*/
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
.mlogo {
height: 60%;
margin-top: 10px;
}
.demo-ruleForm {
max-width: 500px;
margin: 0 auto;
}
style>
<template>
<div class="m-content">
<h3>欢迎来到极客李华的博客h3>
<div class="block">
<el-avatar :size="50" :src="user.avatar">el-avatar>
<div>{{ user.username }}div>
div>
<div class="maction">
<span><el-link href="/blogs">主页el-link>span>
<el-divider direction="vertical">el-divider>
<span><el-link type="success" href="/blog/add">发表博客el-link>span>
<el-divider direction="vertical">el-divider>
<span v-show="!hasLogin"><el-link type="primary" href="/login">登录el-link>span>
<span v-show="hasLogin"><el-link type="danger" @click="logout">退出el-link>span>
div>
div>
template>
<script>
export default {
name: "Header",
data() {
return {
user: {
username: '请先登录',
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
},
hasLogin: false
}
},
methods: {
logout() {
const _this = this
_this.$axios.get("/logout", {
headers: {
"Authorization": localStorage.getItem("token")
}
}).then(res => {
_this.$store.commit("REMOVE_INFO")
_this.$router.push("/login")
})
}
},
created() {
if(this.$store.getters.getUser.username) {
this.user.username = this.$store.getters.getUser.username
this.user.avatar = this.$store.getters.getUser.avatar
this.hasLogin = true
}
}
}
script>
<style scoped>
.m-content {
max-width: 960px;
margin: 0 auto;
text-align: center;
}
.maction {
margin: 10px 0;
}
style>
cnpm install mavon-editor --save
然后在main.js中全局注册:
// 全局注册
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)
<template>
<div class="mcontaner">
<Header>Header>
<div class="block">
<el-timeline>
<el-timeline-item :timestamp="blog.created" placement="top" v-for="blog in blogs">
<el-card>
<h4>
<router-link :to="{name: 'BlogDetail', params: {blogId: blog.id}}">
{{blog.title}}
router-link>
h4>
<p>{{blog.description}}p>
el-card>
el-timeline-item>
el-timeline>
<el-pagination class="mpage"
background
layout="prev, pager, next"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change=page>
el-pagination>
div>
div>
template>
<script>
import Header from "../components/Header";
export default {
name: "Blogs.vue",
components: {Header},
data() {
return {
blogs: {},
currentPage: 1,
total: 0,
pageSize: 5
}
},
methods: {
page(currentPage) {
const _this = this
_this.$axios.get("/blogs?currentPage=" + currentPage).then(res => {
console.log(res)
_this.blogs = res.data.data.records
_this.currentPage = res.data.data.current
_this.total = res.data.data.total
_this.pageSize = res.data.data.size
})
}
},
created() {
this.page(1)
}
}
script>
<style scoped>
.mpage {
margin: 0 auto;
text-align: center;
}
style>
<template>
<div>
<Header>Header>
<div class="m-content">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="标题" prop="title">
<el-input v-model="ruleForm.title">el-input>
el-form-item>
<el-form-item label="摘要" prop="description">
<el-input type="textarea" v-model="ruleForm.description">el-input>
el-form-item>
<el-form-item label="内容" prop="content">
<mavon-editor v-model="ruleForm.content">mavon-editor>
el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">立即创建el-button>
<el-button @click="resetForm('ruleForm')">重置el-button>
el-form-item>
el-form>
div>
div>
template>
<script>
import Header from "../components/Header";
export default {
name: "BlogEdit.vue",
components: {Header},
data() {
return {
ruleForm: {
id: '',
title: '',
description: '',
content: ''
},
rules: {
title: [
{ required: true, message: '请输入标题', trigger: 'blur' },
{ min: 3, max: 25, message: '长度在 3 到 25 个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入摘要', trigger: 'blur' }
],
content: [
{ trequired: true, message: '请输入内容', trigger: 'blur' }
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios.post('/blog/edit', this.ruleForm, {
headers: {
"Authorization": localStorage.getItem("token")
}
}).then(res => {
console.log(res)
_this.$alert('操作成功', '提示', {
confirmButtonText: '确定',
callback: action => {
_this.$router.push("/blogs")
}
});
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
created() {
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this
if(blogId) {
this.$axios.get('/blog/' + blogId).then(res => {
const blog = res.data.data
_this.ruleForm.id = blog.id
_this.ruleForm.title = blog.title
_this.ruleForm.description = blog.description
_this.ruleForm.content = blog.content
})
}
}
}
script>
<style scoped>
.m-content {
text-align: center;
}
style>
# 用于解析md文档
cnpm install markdown-it --save
# md样式
cnpm install github-markdown-css
<template>
<div>
<Header>Header>
<div class="mblog">
<h2> {{ blog.title }}h2>
<el-link icon="el-icon-edit" v-if="ownBlog">
<router-link :to="{name: 'BlogEdit', params: {blogId: blog.id}}" >
编辑
router-link>
el-link>
<el-link icon="el-icon-delete" v-if="ownBlog" class="linklist">
<el-button type="danger" round @click="delblog(blog.id)">删除el-button>
el-link>
<el-divider>el-divider>
<div class="markdown-body" v-html="blog.content">div>
div>
div>
template>
<script>
import 'github-markdown-css'
import Header from "../components/Header";
export default {
name: "BlogDetail.vue",
components: {Header},
data() {
return {
blog: {
id: "",
title: "",
content: ""
},
ownBlog: false
}
},
methods: {
delblog (id) {
console.log("删除")
const blogId = id
const _this = this
console.log(blogId)
if (blogId) {
this.$confirm('此操作将永久删除该文章, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
_this.$axios.post(`/blogdel/${blogId}`, null, {
headers: {
"Authorization": localStorage.getItem("token")
}
}).then(res => {
this.$message({
type: 'success',
message: res.data.data
});
_this.$router.push("/blogs")
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
}
},
created() {
const blogId = this.$route.params.blogId
console.log(blogId)
const _this = this
this.$axios.get('/blog/' + blogId).then(res => {
const blog = res.data.data
_this.blog.id = blog.id
_this.blog.title = blog.title
var MardownIt = require("markdown-it")
var md = new MardownIt()
var result = md.render(blog.content)
_this.blog.content = result
// 是自己的博客的时候才可以编辑
_this.ownBlog = (blog.userId === _this.$store.getters.getUser.id)
})
}
}
script>
<style scoped>
.mblog {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
width: 100%;
min-height: 700px;
padding: 20px 15px;
}
.linklist {
margin: 5px;
}
style>
页面已经开发完毕之后,我们来控制一下哪些页面是需要登录之后才能跳转的,如果未登录访问就直接重定向到登录页面,因此我们在src目录下定义一个js文件:
import router from "./router";
// 路由判断登录 根据路由配置文件的参数
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)) { // 判断该路由是否需要登录权限
const token = localStorage.getItem("token")
console.log("------------" + token)
if (token) { // 判断当前的token是否存在 ; 登录存入的token
if (to.path === '/login') {
} else {
next()
}
} else {
next({
path: '/login'
})
}
} else {
next()
}
})
通过之前我们再定义页面路由时候的的meta信息,指定requireAuth: true,需要登录才能访问,因此这里我们在每次路由之前(router.beforeEach)判断token的状态,觉得是否需要跳转到登录页面。
{
path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
name: 'BlogAdd',
meta: {
requireAuth: true
},
component: BlogEdit
},{
path: '/blog/:blogid/edit',
name: 'BlogEdit',
component: BlogEdit,
meta: {
requireAuth: true
}
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Element from 'element-ui'
import axios from 'axios'
//引用全局
Vue.prototype.$axios = axios
import mavonEditor from 'mavon-editor'
import "element-ui/lib/theme-chalk/index.css"
import 'mavon-editor/dist/css/index.css'
import "./axios"
import "./permission"
Vue.use(Element)
Vue.use(mavonEditor)
Vue.config.productionTip = false
Vue.prototype.$axios = axios
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
— 项目完结 —