原文网址:前后端分离简单项目--蚂蚁博客--后端部分_IT利刃出鞘的博客-CSDN博客

简介

说明

        本文介绍我从0开发的前后端分离的简单项目--蚂蚁博客。本博文介绍后端部分。

本项目是一个全栈项目,使用主流、前沿的技术栈开发,项目虽小,五脏俱全。

后期我会出一个视频,详细讲解本项目。视频录完后会将链接贴到本文。

项目介绍

见:前后端分离简单项目--蚂蚁博客--简介_IT利刃出鞘的博客-CSDN博客

项目源码

gitee地址:https://gitee.com/knifeedge/ant_blog

ant_backend目录是后端部分的代码。

技术栈

  1. SpringBoot(spring-boot-starter-parent:2.4.2)
  2. MyBatis-Plus(mybatis-plus-boot-starter:3.4.3.1)
  3. MySQL(8.0.21)
  4. Shiro(shiro-redis-spring-boot-starter:3.3.1)
  5. JWT(java-jwt:3.18.1)
  6. knife4j(swagger的升级版)(knife4j-spring-boot-starter:3.0.3)
  7. hutool(hutool-all:5.5.7)
  8. lombok(lombok(版本由spring-boot-starter-parent指定))

技术栈简介

  1. MyBatis-Plus

    1. 主流、开发效率最高的持久层框架。秒杀Mybatis、JPA
    2. 相关教程:
      1. MyBatis-Plus--使用_IT利刃出鞘的博客-CSDN博客
      2. MyBatis-Plus--分页--方法/教程/实例_IT利刃出鞘的博客-CSDN博客
  2. MySQL
  3. Shiro
    1. 主流、便利的权限管理框架
    2. 相关教程。 shiro用法我写了一个系列,有如下文章:
      1. Shiro--用Session控制权限--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
      2. Shiro--整合shiro-redis--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
      3. Shiro--整合jwt--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
      4. Shiro--整合jwt--通过url路径控制权限--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
  4. JWT
    1. 主流的token生成、校验工具
    2. 相关教程:
      1. JWT--使用/教程/实例_IT利刃出鞘的博客-CSDN博客_jwt使用教程
  5. knife4j(swagger的升级版)
    1. 主流的接口管理工具
    2. 相关教程:
      1. Knife4j--使用/教程/实例/配置_IT利刃出鞘的博客-CSDN博客_knife4j
  6. hutool
    1. 主流的工具包
  7. lombok

项目结构

概述

项目结构清晰。

  1. 业务部分(business包):按模块进行划分;

    1. 这样的结构是最好的,单个模块的代码都在一处,便于查找,也便于模块化。
    2. 不推荐的做法:所有业务的controller放一个包,所有业务的service放一个包...。原因:不利于模块化;而且一旦模块很多,开发过程中我只关注某个模块,但显示时会很长,影响开发效率
  2. 公共部分(common包):全局处理、常量、工具类
  3. 配置部分(config包):配置类

结构概览

建库建表

DROP DATABASE IF EXISTS blog;
CREATE DATABASE blog DEFAULT CHARACTER SET utf8;
USE blog;DROP TABLE IF EXISTS `t_blog`;
DROP TABLE IF EXISTS `t_user`;
SET NAMES utf8mb4;CREATE TABLE `t_blog`
(`id`           BIGINT(0) NOT NULL AUTO_INCREMENT,`user_id`      BIGINT(0) NOT NULL,`user_name`    VARCHAR(64) NOT NULL,`title`        VARCHAR(256) CHARACTER SET utf8mb4 NOT NULL COMMENT '标题',`description`  VARCHAR(256) CHARACTER SET utf8mb4 NOT NULL COMMENT '摘要',`content`      LONGTEXT CHARACTER SET utf8mb4 NOT NULL COMMENT '内容',`status`       INT(0) NOT NULL DEFAULT 0 COMMENT '0:正常。 1:正在审核。2:已删除',`create_time`  DATETIME(0),`update_time`  DATETIME(0),`deleted_flag` BIGINT(0) NOT NULL DEFAULT 0 COMMENT '0:未删除 其他:已删除',PRIMARY KEY (`id`) USING BTREE,KEY `index_user_id`(`user_id`),KEY `index_user_name`(`user_name`),KEY `index_create_time`(`create_time`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '博客';CREATE TABLE `t_user`
(`id`              BIGINT(0) NOT NULL AUTO_INCREMENT,`user_name`       VARCHAR(64) NOT NULL,`password`        VARCHAR(64) NOT NULL,`salt`            VARCHAR(64) NOT NULL,`nick_name`       VARCHAR(64) NOT NULL,`avatar_url`      VARCHAR(256) NOT NULL,`email`           VARCHAR(64),`status`          INT(0) NOT NULL DEFAULT 0 COMMENT '0:正常 1:被锁定',`last_login_time` DATETIME(0),`create_time`     DATETIME(0) NOT NULL,`update_time`     DATETIME(0) NOT NULL,PRIMARY KEY (`id`) USING BTREE,KEY `index_user_name`(`user_name`),KEY `index_create_time`(`create_time`)
) ENGINE = InnoDB COMMENT = '用户';-- 账号:knife,密码:222333   密码会加盐,两次md5之后进行保存
INSERT INTO `t_user` VALUES (1, 'knife', 'e4b8c8e43f8fabbe08d5aa67d58068ac', 'sCPKiMOoEl2ecPsfFhClcg==','刀刃', 'https://i.postimg.cc/T2Cn6r8y/IOHWn0.png',NULL, 0, NULL, '2021-01-23 09:33:36', '2021-01-23 09:33:36');-- 账号:sky,密码:123456    密码会加盐,两次md5之后进行保存
INSERT INTO `t_user` VALUES (2, 'sky', '49b3146badc6479f5d6c306994a4a33e','+SyMx8kT2CnKf6K2l3IH8g==','天蓝', 'https://i.postimg.cc/Hn91nMcj/image.jpg',NULL, 0, NULL, '2021-01-10 22:01:05', '2021-01-10 22:01:05');INSERT INTO `t_blog` VALUES (1, 1, 'knife', 'Java中枚举的用法','本文介绍Java的枚举类的使用,枚举一般用于定义一些常量。枚举类完全单例、线程安全;性能高,常量值地址唯一,可以用==直接对比;不需重新编译引用类。枚举类编译时,没有把常量值编译到代码里,即使常量的值发生变化,也不会影响引用常量的类。','本文介绍Java的枚举类的使用,枚举一般用于定义一些常量。枚举类完全单例、线程安全;性能高,常量值地址唯一,可以用==直接对比;不需重新编译引用类。枚举类编译时,没有把常量值编译到代码里,即使常量的值发生变化,也不会影响引用常量的类。',0, '2021-01-23 11:33:36', '2021-01-23 11:33:36', 0);
INSERT INTO `t_blog` VALUES (2, 1, 'knife', 'Java中泛型的用法','本文介绍Java的泛型的使用。一些框架的源码中经常看到泛型,学习泛型可以帮助我们更好的阅读框架源码、理解框架,也可以提高编程水平','本文介绍Java的泛型的使用。一些框架的源码中经常看到泛型,学习泛型可以帮助我们更好的阅读框架源码、理解框架,也可以提高编程水平',0, '2021-01-28 23:37:37', '2021-01-28 23:37:37', 0);
INSERT INTO `t_blog` VALUES (3, 1, 'knife', 'Java的HashMap的原理','本文介绍Java的HashMap的原理。HashMap实际上是一个大的数组,key是数组的下标,value是数组的值。如果产生了哈希冲突,HashMap使用链地址法解决。JDK8中引入了红黑树,当同一个key上边大于8个元素时,链表会转化为红黑树,提高性能','本文介绍Java的HashMap的原理。HashMap实际上是一个大的数组,key是数组的下标,value是数组的值。如果产生了哈希冲突,HashMap使用链地址法解决。JDK8中引入了红黑树,当同一个key上边大于8个元素时,链表会转化为红黑树,提高性能',0, '2021-05-28 09:06:06', '2021-05-28 09:06:06', 0);
INSERT INTO `t_blog` VALUES (4, 1, 'knife', 'Java中BigDecimal的用法','本文介绍Java的BigDecimal的使用。BigDecimal处理数字很精确,而且可以表示大于16位的数。相比而言,double只能处理16位以内的数而且计算时不精确。比较经典的使用场景是金额','本文介绍Java的BigDecimal的使用。BigDecimal处理数字很精确,而且可以表示大于16位的数。相比而言,double只能处理16位以内的数而且计算时不精确。比较经典的使用场景是金额',0, '2021-06-24 20:36:54', '2021-06-24 20:36:54', 0);
INSERT INTO `t_blog` VALUES (5, 1, 'knife', 'Java中反射的用法','本文介绍Java的反射的使用。反射一般用于通过Class获得实例、调用方法等','本文介绍Java的反射的使用。反射一般用于通过Class获得实例、调用方法等',0, '2021-10-28 22:24:18', '2021-10-28 22:24:18', 0);
INSERT INTO `t_blog` VALUES (6, 1, 'knife', 'Java的ArrayList保证线程安全的方法','ArrayList不是线程安全的,也就是说:多个线程操作同一个ArrayList的时候会出现问题。有多种方法可以保证线程安全:Collections.synchronizedList(List)、JUC中的CopyOnWriteArrayList、Vector。推荐使用前两种,第三种性能很差,不推荐使用','ArrayList不是线程安全的,也就是说:多个线程操作同一个ArrayList的时候会出现问题。有多种方法可以保证线程安全:Collections.synchronizedList(List)、JUC中的CopyOnWriteArrayList、Vector。推荐使用前两种,第三种性能很差,不推荐使用',0, '2021-08-28 21:31:20', '2021-08-28 21:31:20', 0);
INSERT INTO `t_blog` VALUES (7, 1, 'knife', 'SpringBoot启动的流程','本文介绍SpringBoot启动的流程。分析Spring的启动流程有多种方法:1.构造一个AnnotationConfigApplicationContext对象,调用它的getBean(xxx.class)方法; 2.直接分析SpringBoot的启动流程。本文直接分析SpringBoot的启动流程。本文分析的版本:SpringBoot版本:2.3.0.RELEASE(其对应Spring:5.2.6.RELEASE)。','本文介绍SpringBoot启动的流程。分析Spring的启动流程有多种方法:1.构造一个AnnotationConfigApplicationContext对象,调用它的getBean(xxx.class)方法; 2.直接分析SpringBoot的启动流程。本文直接分析SpringBoot的启动流程。本文分析的版本:SpringBoot版本:2.3.0.RELEASE(其对应Spring:5.2.6.RELEASE)。',0, '2021-09-25 19:02:55', '2021-09-25 19:02:55', 0);
INSERT INTO `t_blog` VALUES (8, 1, 'knife', 'ArrayList扩容原理','本文介绍Java的ArrayList扩容的原理。直接new 一个ArrayList对象时(未指定初始容量大小)是一个空的数组,容量大小为零。当第一次调用ArrayList对象的add方法时,分配容量大小','本文介绍Java的ArrayList扩容的原理。直接new 一个ArrayList对象时(未指定初始容量大小)是一个空的数组,容量大小为零。当第一次调用ArrayList对象的add方法时,分配容量大小',0, '2021-10-29 22:30:32','2021-10-29 22:30:32', 0);
INSERT INTO `t_blog` VALUES (9, 1, 'knife', 'Java的类加载流程','本文介绍Java的类加载流程。Java的类加载流程为:加载=> 链接(验证+准备+解析)=> 初始化=> 使用=> 卸载。加载:通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存(不一定在堆,对于HotSpot在方法区)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。','本文介绍Java的类加载流程。Java的类加载流程为:加载=> 链接(验证+准备+解析)=> 初始化=> 使用=> 卸载。加载:通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存(不一定在堆,对于HotSpot在方法区)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。',0, '2021-10-29 23:03:05', '2021-10-29 23:03:05', 0);
INSERT INTO `t_blog` VALUES (10, 1, 'knife', 'SpringBoot整合RabbitMQ','SpringBoot引入RabbitMQ依赖,生产者调用发送方法,消费者进行订阅','SpringBoot引入RabbitMQ依赖,生产者调用发送方法,消费者进行订阅',0, '2021-10-15 21:38:59', '2021-10-15 21:38:59', 0);
INSERT INTO `t_blog` VALUES (11, 1, 'knife', 'ElasticSearch复杂查询的方法','本文介绍如何使用ElasticSearch进行复杂查询。ElasticSearch使用布尔查询进行复杂查询。布尔查询的所有子查询之间的逻辑关系是与(and):只有当一个文档满足布尔查询中的所有子查询条件时,ElasticSearch引擎才认为该文档满足查询条件。','本文介绍如何使用ElasticSearch进行复杂查询。ElasticSearch使用布尔查询进行复杂查询。布尔查询的所有子查询之间的逻辑关系是与(and):只有当一个文档满足布尔查询中的所有子查询条件时,ElasticSearch引擎才认为该文档满足查询条件。对于单个子句,只要一个文档满足该子句的查询条件,返回的逻辑结果就是true。对于should子句,它一般包含多个子查询条件,参数 minimum_should_match 控制文档必须满足should子句中的子查询条件的数量,只有当文档满足 minimum_should_match 时,should子句返回的逻辑结果才是true。',0, '2021-03-28 18:55:01', '2021-03-28 18:55:01', 0);
INSERT INTO `t_blog` VALUES (12, 1, 'knife', 'Kafka如何保证消息不丢失','本文介绍保证Kafka消息不丢失的方案。Kafka在生产者、服务器、消费者三个地方都可能导致消息丢失。','本文介绍保证Kafka消息不丢失的方案。Kafka在生产者、服务器、消费者三个地方都可能导致消息丢失。',0, '2021-10-18 23:00:08', '2021-10-18 23:00:08', 0);
INSERT INTO `t_blog` VALUES (13, 1, 'knife', 'Java的CAS的原理','本文介绍Java的CAS的原理。CAS是多线程的基础,含义是:Compare And SetCAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用CAS技术鬼斧神工地实现了Java多线程的并发操作,可以说CAS是整个JUC(java.util.concurrent)的基石。CAS性能很高,适合于高并发场景。','本文介绍Java的CAS的原理。CAS是多线程的基础,含义是:Compare And SetCAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用CAS技术鬼斧神工地实现了Java多线程的并发操作,可以说CAS是整个JUC(java.util.concurrent)的基石。CAS性能很高,适合于高并发场景。',0, '2021-07-26 19:59:10', '2021-07-26 19:59:10', 0);
INSERT INTO `t_blog` VALUES (14, 1, 'knife', 'Spring的AOP的原理','本文介绍Spring的AOP的原理。Spring的AOP是通过动态代理来实现的。','本文介绍Spring的AOP的原理。Spring的AOP是通过动态代理来实现的。',0, '2021-08-28 20:59:58', '2021-08-28 20:59:58', 0);INSERT INTO `t_blog` VALUES (15, 2, 'sky', 'Vue-cli的使用','Vue-cli是Vue的一个脚手架工具','Vue-cli可以用来创建vue项目',0, '2021-02-23 11:34:36', '2021-02-25 14:33:36', 0);
INSERT INTO `t_blog` VALUES (16, 2, 'sky', 'Vuex的用法','Vuex是vue用于共享变量的插件','一般使用vuex来共享变量',0, '2021-03-28 23:37:37', '2021-03-28 23:37:37', 0);

依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>blog</artifactId><version>0.0.1-SNAPSHOT</version><name>blog</name><description>Blog project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3.1</version></dependency><dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis-spring-boot-starter</artifactId><version>3.3.1</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.5.7</version></dependency><!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.18.1</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.3</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

配置文件

application.yml

server:port: 9000spring:application:name: blogdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/blog?useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 222333#mybatis-plus配置控制台打印完整带参数SQL语句
#mybatis-plus:
#  configuration:
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImplshiro:enabled: true       # 开启 shiro,默认为 trueweb:enabled: true     # 开启 shiro Web,默认为 truecustom:jwt:# 加密秘钥secret: f4e2e52034348f86b67cde581c0f9eb5# token有效时长,7天。单位:秒expire: 604800user:defaultAvatarUrl: https://i.postimg.cc/m2wGXNXk/image.png

业务部分

本处展示博客模块、登录模块、登出模块。其余模块省略,详见文章开头的gitee地址。

博客模块

controller

package com.example.demo.business.blog.controller;import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.business.blog.entity.Blog;
import com.example.demo.business.blog.service.BlogService;
import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import com.example.demo.common.util.auth.ShiroUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;@Api(tags = "博客")
@RestController
@RequestMapping("blog")
public class BlogController {@AutowiredBlogService blogService;@ApiOperation("创建博客")@RequiresAuthentication@PostMapping("/add")public Result<Blog> add(@RequestBody Blog blog) {Assert.hasText(blog.getTitle(), "标题不能为空");Assert.hasText(blog.getDescription(), "摘要不能为空");Assert.hasText(blog.getContent(), "内容不能为空");Blog temp = new Blog();temp.setUserId(Long.parseLong(ShiroUtil.getProfile().getUserId()));temp.setUpdateTime(LocalDateTime.now());temp.setStatus(0);blogService.save(temp);return new Result<Blog>().data(temp);}@ApiOperation("编辑博客")@RequiresAuthentication@PostMapping("/edit")public Result<Blog> edit(@RequestBody Blog blog) {Assert.isTrue(blog.getId() != null, "id不能为空");Assert.hasText(blog.getTitle(), "标题不能为空");Assert.hasText(blog.getDescription(), "摘要不能为空");Assert.hasText(blog.getContent(), "内容不能为空");Blog temp = blogService.getById(blog.getId());// 只能编辑自己的文章Assert.isTrue(temp.getUserId().equals(Long.parseLong(ShiroUtil.getProfile().getUserId())), "没有权限编辑");BeanUtil.copyProperties(blog, temp, "id", "userId", "updateTime", "status");blogService.updateById(temp);return new Result<Blog>().data(temp);}@ApiOperation("博客分页")@GetMapping("/page")public Result<IPage<Blog>> list(Page<Blog> page, @RequestParam String userName) {if (!StringUtils.hasText(userName)) {throw new BusinessException("用户名不能为空");}IPage<Blog> pageData = blogService.lambdaQuery().eq(Blog::getUserName, userName).orderByDesc(Blog::getCreateTime).page(page);return new Result<IPage<Blog>>().data(pageData);}@ApiOperation("查看博客")@GetMapping("/getThis")public Result<Blog> detail(@RequestParam Long id) {Blog blog = blogService.getById(id);Assert.notNull(blog, "该博客已被删除");return new Result<Blog>().data(blog);}@ApiOperation("删除博客")@RequiresAuthentication@PostMapping("/delete")public Result delete(@RequestParam Long[] ids) {Assert.notNull(ids, "博客id不能为空");List<Blog> blogList = blogService.lambdaQuery().in(Blog::getId, Arrays.asList(ids)).list();// 只能删除自己的文章for (Blog blog : blogList) {if (!ShiroUtil.getProfile().getUserId().equals(blog.getUserId().toString())) {throw new BusinessException("您无权删除其他人的文章");}}blogService.deleteBlog(Arrays.asList(ids));return new Result();}
}

service

接口

package com.example.demo.business.blog.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.business.blog.entity.Blog;import java.sql.Wrapper;
import java.util.List;public interface BlogService extends IService<Blog> {int deleteBlog(List<Long> blogIds);Integer blogCount(Long userId);
}

实现类

package com.example.demo.business.blog.service.impl;import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.business.blog.entity.Blog;
import com.example.demo.business.blog.mapper.BlogMapper;
import com.example.demo.business.blog.service.BlogService;
import com.example.demo.common.exception.BusinessException;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements BlogService {@Overridepublic int deleteBlog(List<Long> ids) {if (ids == null || ids.isEmpty()) {throw new BusinessException("博客id不能为空");}LambdaQueryChainWrapper<Blog> wrapper = lambdaQuery().in(Blog::getId, ids);return this.getBaseMapper().deleteBlog(wrapper);}@Overridepublic Integer blogCount(Long userId) {return lambdaQuery().eq(Blog::getUserId, userId).count();}
}

mapper

package com.example.demo.business.blog.mapper;import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.business.blog.entity.Blog;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;@Repository
public interface BlogMapper extends BaseMapper<Blog> {@Update("UPDATE t_blog SET deleted_flag = id ${ew.customSqlSegment}")int deleteBlog(@Param("ew") Wrapper<Blog> wrapper);
}

entity

package com.example.demo.business.blog.entity;import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;import java.time.LocalDateTime;@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_blog")
public class Blog {@TableId(value = "id", type = IdType.AUTO)private Long id;private Long userId;private String userName;private String title;// 摘要private String description;private String content;private Integer status;@TableField(fill = FieldFill.INSERT)@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private LocalDateTime updateTime;@TableLogicprivate Long deletedFlag;}

登录模块

package com.example.demo.business.login;import com.example.demo.business.user.entity.User;
import com.example.demo.business.user.entity.UserVO;
import com.example.demo.business.user.service.UserService;
import com.example.demo.common.constant.AuthConstant;
import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.properties.UserProperty;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletResponse;@Api(tags = "登录/注册")
@RestController
@RequestMapping("/login")
public class LoginController {@Autowiredprivate UserService userService;@Autowiredprivate UserProperty userProperty;/*** 若未注册则自动注册,若已注册则登录*/@ApiOperation("登录/注册")@PostMappingpublic Result<UserVO> login(@RequestBody User user, HttpServletResponse response) {Assert.hasLength(user.getUserName(), "用户名不能为空");Assert.hasLength(user.getPassword(), "密码不能为空");User userFromDB = userService.lambdaQuery().eq(User::getUserName, user.getUserName()).one();if (userFromDB == null) {userFromDB = register(user.getUserName(), user.getPassword());} else {String calculatedPassword = new SimpleHash(AuthConstant.ALGORITHM_TYPE,user.getPassword(), userFromDB.getSalt(), AuthConstant.HASH_ITERATIONS).toString();if (!userFromDB.getPassword().equals(calculatedPassword)) {throw new BusinessException("用户名或密码不正确");}}String jwt = JwtUtil.createToken(userFromDB.getId().toString());response.setHeader(AuthConstant.AUTHENTICATION_HEADER, jwt);// response.setHeader("Access-Control-Expose-Headers", TOKEN_HEADER);   // 后端配置跨域时使用UserVO userVO = new UserVO();userVO.setId(userFromDB.getId());userVO.setUserName(userFromDB.getUserName());userVO.setNickName(userFromDB.getNickName());userVO.setAvatarUrl(userFromDB.getAvatarUrl());userVO.setEmail(userFromDB.getEmail());return new Result<UserVO>().success().data(userVO);}private User register(String userName, String password) {User user = new User();user.setUserName(userName);user.setNickName(userName);String salt = new SecureRandomNumberGenerator().nextBytes().toString();String calculatedPassword = new SimpleHash(AuthConstant.ALGORITHM_TYPE,password, salt, AuthConstant.HASH_ITERATIONS).toString();user.setPassword(calculatedPassword);user.setSalt(salt);user.setAvatarUrl(userProperty.getDefaultAvatarUrl());userService.save(user);return user;}
}

登出模块

package com.example.demo.business.logout;import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@Api(tags = "退出")
@RestController
@RequestMapping("/logout")
public class LogoutController {@ApiOperation("退出登录")@RequiresAuthentication@PostMappingpublic Result logout() {SecurityUtils.getSubject().logout();return new Result();}
}

公共部分

全局处理

全局异常处理

package com.example.demo.common.advice;import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
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 GlobalExceptionAdvice {@ExceptionHandler(Exception.class)public Result<Object> handleException(Exception e) throws Exception {log.error(e.getMessage(), e);// 如果某个自定义异常有@ResponseStatus注解,就继续抛出if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {throw e;}// 实际项目中应该这样写,防止用户看到详细的异常信息// return new Result().failure().message.message("操作失败");return new Result<>().failure().message(e.getMessage());}@ResponseStatus(HttpStatus.UNAUTHORIZED)@ExceptionHandler(UnauthenticatedException.class)public Result<Object> handleUnauthenticatedException(Exception e) {log.error(e.getMessage(), e);return new Result<>().failure().message(e.getMessage());}@ResponseStatus(HttpStatus.FORBIDDEN)@ExceptionHandler(UnauthorizedException.class)public Result<Object> handleUnauthorizedException(Exception e) {log.error(e.getMessage(), e);return new Result<>().failure().message(e.getMessage());}@ExceptionHandler(BusinessException.class)public Result<Object> handleBusinessException(Exception e) throws Exception {log.error(e.getMessage(), e);// 如果某个自定义异常有@ResponseStatus注解,就继续抛出if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {throw e;}// 实际项目中应该这样写,防止用户看到详细的异常信息// return new Result<>().failure().message("操作失败");return new Result<>().failure().message(e.getMessage());}
}

全局响应处理

package com.example.demo.common.advice;import com.example.demo.common.constant.WhiteList;
import com.example.demo.common.entity.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;@Slf4j
@ControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {@Overridepublic boolean supports(MethodParameter returnType,Class<? extends HttpMessageConverter<?>> converterType) {// 若接口返回的类型本身就是ResultWrapper,则无需操作,返回false// return !returnType.getParameterType().equals(ResultWrapper.class);return true;}@Override@ResponseBodypublic Object beforeBodyWrite(Object body, MethodParameter returnType,MediaType selectedContentType,Class<? extends HttpMessageConverter<?>> selectedConverterType,ServerHttpRequest request, ServerHttpResponse response) {if (body instanceof String) {// 若返回值为String类型,需要包装为String类型返回。否则会报错try {ObjectMapper objectMapper = new ObjectMapper();Result<Object> result = new Result<>().data(body);return objectMapper.writeValueAsString(result);} catch (JsonProcessingException e) {throw new RuntimeException("序列化String错误");}} else if (body instanceof Result) {return body;} else if (isKnife4jUrl(request.getURI().getPath())) {// 如果是接口文档uri,直接跳过return body;}return new Result<>().data(body);}private boolean isKnife4jUrl(String uri) {AntPathMatcher pathMatcher = new AntPathMatcher();for (String s : WhiteList.KNIFE4J) {if (pathMatcher.match(s, uri)) {return true;}}return false;}
}

工具

JWT工具

package com.example.demo.common.util.auth;import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.example.demo.common.util.ApplicationContextHolder;
import com.example.demo.config.properties.JwtProperty;import java.util.Date;public class JwtUtil {private static final JwtProperty JWT_PROPERTY;static {JWT_PROPERTY = ApplicationContextHolder.getContext().getBean(JwtProperty.class);}// 创建jwt tokenpublic static String createToken(String userId) {try {Date date = new Date(System.currentTimeMillis() + JWT_PROPERTY.getExpire() * 1000);Algorithm algorithm = Algorithm.HMAC512(JWT_PROPERTY.getSecret());return JWT.create()// 自定义私有的payload的key-value。比如:.withClaim("userName", "Tony")// .withClaim("key1", "value1").withAudience(userId)  // 将 user id 保存到 token 里面.withExpiresAt(date)   // date之后,token过期.sign(algorithm);      // token 的密钥} catch (Exception e) {return null;}}/*** 校验token* 若校验失败,会抛出异常:{@link JWTVerificationException}* 失败情况(按先后顺序):* - 算法不匹配:{@link com.auth0.jwt.exceptions.AlgorithmMismatchException}* - 签名验证失败:{@link com.auth0.jwt.exceptions.SignatureVerificationException}* - Claim无效:{@link com.auth0.jwt.exceptions.InvalidClaimException}* - token超期:{@link com.auth0.jwt.exceptions.TokenExpiredException}*/public static void verifyToken(String token) {Algorithm algorithm = Algorithm.HMAC512(JWT_PROPERTY.getSecret());JWTVerifier jwtVerifier = JWT.require(algorithm)// .withIssuer("auth0")// .withClaim("userName", userName).build();DecodedJWT jwt = jwtVerifier.verify(token);}public static String getUserIdByToken(String token) {try {return JWT.decode(token).getAudience().get(0);} catch (JWTDecodeException e) {return null;}}public static boolean isTokenExpired(String token) {DecodedJWT decodedJWT = JWT.decode(token);return decodedJWT.getExpiresAt().before(new Date());}}

ApplicationContextHolder

package com.example.demo.common.util;import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;@Component
public class ApplicationContextHolder implements ApplicationContextAware {private static ApplicationContext context;public void setApplicationContext(ApplicationContext context) throws BeansException {ApplicationContextHolder.context = context;}public static ApplicationContext getContext() {return context;}
}  

配置部分

Shiro

总配置

package com.example.demo.config.shiro;import com.example.demo.common.constant.WhiteList;
import com.example.demo.config.shiro.filter.JwtFilter;
import com.example.demo.config.shiro.realm.AccountRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
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.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;
import java.util.Map;@Configuration
public class ShiroConfig {@Beanpublic ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();chainDefinition.addPathDefinition("/login", "anon");chainDefinition.addPathDefinition("/blog/page", "anon");chainDefinition.addPathDefinition("/blog/getThis", "anon");chainDefinition.addPathDefinition("/user/page", "anon");chainDefinition.addPathDefinition("/user/profile", "anon");WhiteList.ALL.forEach(str -> {chainDefinition.addPathDefinition(str, "anon");});// all other paths require a logged in userchainDefinition.addPathDefinition("/**", "jwt");return chainDefinition;}// 这样是不行的,会导致标记了anon的路径也会走到JwtFilter。// 也就是说:不能将自定义的filter注册成bean。// @Bean("authc")// public AuthenticatingFilter authenticatingFilter() {//     return new JwtFilter();// }/*** 设置过滤器,将自定义的Filter加入*/@Bean("shiroFilterFactoryBean")public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();// 登录的地址factoryBean.setLoginUrl("/login");// 登录成功后要跳转的地址// factoryBean.setSuccessUrl("/index");// 未授权地址// factoryBean.setUnauthorizedUrl("/unauthorized");factoryBean.setSecurityManager(securityManager);Map<String, Filter> filterMap = factoryBean.getFilters();filterMap.put("jwt", new JwtFilter());factoryBean.setFilters(filterMap);factoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());return factoryBean;}@Beanpublic DefaultWebSecurityManager securityManager() {DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();// 关闭shiro自带的session。这样不能通过session登录shiro,后面将采用jwt凭证登录。// 见:http://shiro.apache.org/session-management.html#SessionManagement-DisablingSubjectStateSessionStoragedefaultSessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(getDatabaseRealm());securityManager.setSubjectDAO(subjectDAO);return securityManager;}@Beanpublic AccountRealm getDatabaseRealm() {return new AccountRealm();}/*** setUsePrefix(true)用于解决一个奇怪的bug。如下:*  在引入spring aop的情况下,在@Controller注解的类的方法中加入@RequiresRole等*  shiro注解,会导致该方法无法映射请求,导致返回404。加入这项配置能解决这个bug。*/@Beanpublic static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();defaultAdvisorAutoProxyCreator.setUsePrefix(true);return defaultAdvisorAutoProxyCreator;}/*** 开启shiro 注解。比如:@RequiresRole* 本处不用此方法开启注解,使用引入spring aop依赖的方式。原因见:application.yml里的注释*//*@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}*//*** 此种配置方法在本项目中跑不通。*//* @Bean("shiroFilterFactoryBean")public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 认证失败要跳转的地址。// shiroFilterFactoryBean.setLoginUrl("/login");// // 登录成功后要跳转的链接// shiroFilterFactoryBean.setSuccessUrl("/index");// // 未授权界面;// shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();filterChainDefinitionMap.put("/login", "anon");WhiteList.ALL.forEach(str -> {filterChainDefinitionMap.put(str, "anon");});// filterChainDefinitionMap.put("/logout", "logout");filterChainDefinitionMap.put("/**", "jwtAuthc");Map<String, Filter> customisedFilters = new LinkedHashMap<>();// 不能用注入来设置过滤器。若用注入,则本过滤器优先级会最高(/**优先级最高,导致前边所有请求都无效)。// springboot会扫描所有实现了javax.servlet.Filter接口的类,无需加@Component也会扫描到。customisedFilters.put("jwtAuthc", new JwtFilter());shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);shiroFilterFactoryBean.setFilters(customisedFilters);return shiroFilterFactoryBean;}*/
}

Filter

package com.example.demo.config.shiro.filter;import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.demo.common.constant.AuthConstant;
import com.example.demo.common.util.ResponseUtil;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.JwtToken;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class JwtFilter extends AuthenticatingFilter {/*** 所有请求都会到这里来(无论是不是anon)。* 返回true:表示允许向下走。* 返回false:表示不允许向下走。*/@Overrideprotected boolean onAccessDenied(ServletRequest servletRequest,ServletResponse servletResponse) throws Exception {HttpServletRequest request = (HttpServletRequest) servletRequest;String token = request.getHeader(AuthConstant.AUTHENTICATION_HEADER);if (!StringUtils.hasText(token)) {ResponseUtil.jsonResponse((HttpServletResponse) servletResponse,HttpStatus.UNAUTHORIZED.value(), "授权信息不能为空");return false;} else {try {JwtUtil.verifyToken(token);} catch (TokenExpiredException e) {ResponseUtil.jsonResponse((HttpServletResponse) servletResponse,HttpStatus.UNAUTHORIZED.value(), "授权信息已过期,请重新登录");return false;} catch (Exception e) {ResponseUtil.jsonResponse((HttpServletResponse) servletResponse,HttpStatus.UNAUTHORIZED.value(), "授权信息校验失败");return false;}}// 此登录并非调用login接口,而是shiro层面的登录。// 里边会调用下边的createToken方法return executeLogin(servletRequest, servletResponse);}/*** 这里的token会传给AuthorizingRealm子类(本处是AccountRealm)的doGetAuthenticationInfo方法作为参数*/@Overrideprotected AuthenticationToken createToken(ServletRequest servletRequest,ServletResponse servletResponse) {HttpServletRequest request = (HttpServletRequest) servletRequest;String token = request.getHeader(AuthConstant.AUTHENTICATION_HEADER);if (!StringUtils.hasText(token)) {return null;}return new JwtToken(token);}
}

Realm

package com.example.demo.config.shiro.realm;import com.example.demo.business.user.entity.User;
import com.example.demo.business.user.service.UserService;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.AccountProfile;
import com.example.demo.config.shiro.entity.JwtToken;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;import java.util.HashSet;
import java.util.Set;public class AccountRealm extends AuthorizingRealm {@Lazy@Autowiredprivate UserService userService;//使realm支持jwt的认证方案@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JwtToken;}// 登录认证// 此处的 SimpleAuthenticationInfo 可返回任意值,密码校验时不会用到它。@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {JwtToken jwtToken = (JwtToken) token;String userId = JwtUtil.getUserIdByToken((String) jwtToken.getPrincipal());if (userId == null) {throw new UnknownAccountException("token为空,请重新登录");}// 获取数据库中的密码User user = userService.getById(userId);if (user == null) {throw new UnknownAccountException("token为空,请重新登录");}AccountProfile accountProfile = new AccountProfile();accountProfile.setId(userId);accountProfile.setUserName(user.getUserName());String salt = user.getSalt();// 认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :accountRealm// 盐也放进去,通过ShiroConfig里配置的 HashedCredentialsMatcher 进行自动校验SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(accountProfile, jwtToken.getCredentials(), ByteSource.Util.bytes(salt), getName());return authenticationInfo;}// 权限验证// 只有用到org.apache.shiro.web.filter.authz包里默认的过滤器才会走到这里。@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {// 能进入到这里,表示账号已经通过认证了AccountProfile profile = (AccountProfile) principalCollection.getPrimaryPrincipal();// 通过service获取角色和权限// Set<String> permissions = permissionService//         .getPermissionsByUserId(Long.parseLong(profile.getId()));// Set<String> roles = roleService.getRolesByUserId(profile.getId());Set<String> permissions = new HashSet<>();Set<String> roles = new HashSet<>();// 授权对象SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();// 把通过service获取到的角色和权限放进去s.setStringPermissions(permissions);s.setRoles(roles);return s;}
}

实体类

package com.example.demo.config.shiro.entity;import lombok.Data;@Data
public class AccountProfile {private String id;private String userName;private String avatar;private String email;}
package com.example.demo.config.shiro.entity;import org.apache.shiro.authc.AuthenticationToken;/*** JwtToken代替官方的UsernamePasswordToken,是Shiro用户名、密码等信息的载体,* 前后端分离,服务器不保存用户状态,所以不需要RememberMe等功能。*/
public class JwtToken implements AuthenticationToken {private final String token;public JwtToken(String jwt) {this.token = jwt;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}

前后端分离简单项目--蚂蚁博客--后端部分相关推荐

  1. 前后端分离简单项目--蚂蚁博客--简介

    原文网址:前后端分离简单项目--蚂蚁博客--简介_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍我从0开发的前后端分离的简单项目--蚂蚁博客. 本项目是一个全栈项目,使用主流.前沿的技术栈开发 ...

  2. SpringSecurity - 前后端分离简单实战 - 环境准备

    SpringSecurity 学习指南大全 文章目录 SpringSecurity - 前后端分离简单实战 - 环境准备 最好的理解方法 环境准备 技术前提 项目架构 项目创建 项目配置 数据库配置 ...

  3. 视频教程-SpringBoot2+Vue+AntV前后端分离开发项目实战-Java

    SpringBoot2+Vue+AntV前后端分离开发项目实战 10多年互联网一线实战经验,现就职于大型知名互联网企业,架构师, 有丰富实战经验和企业面试经验:曾就职于某上市培训机构数年,独特的培训思 ...

  4. laravel + Vue 前后端分离 之 项目配置 - 开发环境

    既然你来到查看这篇文章,那么你应该知道PHP 版本 >=7. 接着看一下目录 |-blog |-- api |-- front 目录外部很简单,,不多说了,赶紧看重点 配置Laravel 作为前 ...

  5. 基于Springboot+vue前后端分离的项目--后端笔记

    效果图: 1.前言  从零开始搭建一个项目最重要的是选择一个自己熟悉的框架,此项目使用Springboot框架来构建后端结构,使用vue来构建前端页面.数据层我们常用的是Mybatis,这里我大部分使 ...

  6. 视频教程-Angular+Django前后端分离实战项目开发教程-AngularJS

    Angular+Django前后端分离实战项目开发教程 胜蓝博创(韬略课堂)创始人,IT培训讲师,先后在蓝港在线,热酷,乐元素等大型游戏公司任职,参与过多款大型网游.手游的设计和开发,精通页游.手游前 ...

  7. JAVA外卖项目第九天 前后端分离和项目部署优化

    瑞吉外卖项目优化-Day03 课程内容 前后端分离开发 Yapi Swagger 项目部署 前言 当前项目中,前端代码和后端代码混合在一起,是存在问题的,存在什么问题呢? 主要存在以下几点问题: 1) ...

  8. 前后端分离的项目部署到tomcat_如何在开发时部署和运行前后端分离的JavaWeb项目...

    在开发中大型的JavaEE项目时,前后端分离的框架逐渐成为业界的主流,传统的单机部署前后端在同一个项目中的工程项目越来越少.这类JavaWeb项目的后端通常都采用微服务的架构,后端会被分解为诸多个小项 ...

  9. Element-UI + Vue.js + SpringBoot 实现前后端分离入门项目

    Element UI 项目简介与演示 刷新页面时让组件默认为当前路由路径 删除时确认 分页功能的实现 el-date-picke 日期少一天 前端部分源码:https://github.com/szl ...

最新文章

  1. java获取jndi密码_用JAVA代码获取Weblogic配置的JNDI 数据源连接
  2. python中的字符串是什么,一文秒懂Python中的字符串
  3. 《leetcode》longest-substring-without-repeating
  4. 在墙上找垂直线_墙上如何快速找水平线
  5. 寻找第k大的元素Java,java – 支持快速第k个最大元素查找的队列数据结构
  6. 【Flink】Flink 报错 Hash join exceeded Too many duplicate keys
  7. echarts折线图怎么从y轴开始_基于echarts的双y轴实时更新折线图
  8. CSS 相对定位与绝对定位
  9. 输入框中有内容才激活组件
  10. Excel:VBA编程入门(一)
  11. android 声纹识别 开源,声纹识别开源工具ASV-Subtools.pdf
  12. (二)市场调查大赛系列——市场调查问卷设计
  13. EAS开发单据分录和序时簿添加合计
  14. Fovea Box阅读学习笔记
  15. 手机测试软件对手机有影响吗,智能手机测试软件的猫腻
  16. 鸿蒙智慧电视,鸿蒙带来的超强多屏互动 荣耀智慧屏与普通电视的不同
  17. recycler上下拉刷新view
  18. 是不是可以赚钱的APP越来越多
  19. STM32之STM Studio使用
  20. Hive的图形化工具HWI

热门文章

  1. vim加载systemverilog语法高亮
  2. Markdown 前言
  3. 05-【VirtualBox】VirtualBox 导入、导出虚拟机,实现从一台电脑中的虚拟机复制到另一台电脑
  4. 全向移动机器人运动参数校准
  5. 采取HEXO+NexT主题+github.io的方式建立自己的个人主页
  6. 微信公众号赞赏账户头像在哪里修改?附详细图文教程
  7. 注册表(regedit)
  8. 下载数据库名前加“#”方法
  9. ArcGIS 制图搭配:文字、比例尺等大小
  10. 淘宝x-sign, x-mini-wua, x-sgext, x-umt挂unidbg分析