基于Dockerfile的博客管理系统的容器化部署

目录

  1. 任务描述 3
    1.1课题的基本内容 3
    1.2 项目整体技术架构 3
    1.3主要技术栈: 3
    1.4 模块划分 4
    1.5 容器集群化部署的任务内容 5
    1.6 项目容器化部署的目的 6
  2. 总体结构 7
    2.1 容器角色和功能 7
    2.2 容器之间的关联关系 8
    2.3 数据流动示例 8
    3.详细设计 9
    3.1 设计思路 9
    3.2Java后端接口开发 11
    1、前言 11
    2、新建Springboot项目 12
    3、整合mybatis plus 13
    4、整合shiro+jwt,并会话共享 18
    5、异常处理 29
    6、实体校验 31
    7、跨域问题 32
    8、登录接口开发 33
    9、博客接口开发 35
    3.3前端代码展示与部分后端代码展示 37
    3.3.1.前言 37
    3.3.2.项目演示 37
    3.3.4 源码展示 39
    4.实现与测试 42
    4.1构建镜像 42
    4.2启动服务 43
    4.2.1启动docker-compose 43
    4.2.2 启动vue容器及其其他服务 43
    4.3 成功进入前端页面 44
    4.4 Mysql数据库连接成功 45
    4.5文章管理界面 46
    5.结束语 46
    6.参考文献 47

1.任务描述

1.1课题的基本内容

一个基于SpringBoot + Vue开发的前后端分离博客项目,后端涵盖新建项目、整合 Mybatis Plus、Shiro + JWT 等技术,还包括异常处理、实体校验等。前端涉及环境准备、组件安装、页面路由、登录页面等开发,如安装 element-ui、axios 等,还实现了博客列表、编辑、详情等页面及路由权限拦截。

1.2 项目整体技术架构

该博客管理系统的技术架构遵循现代分布式系统设计原则,采用了前后端分离的开发模式。前端使用 Vue.js 来开发单页面应用,后端则使用 Spring Boot 框架,结合 Mybatis Plus 和其他技术栈实现完整的博客管理系统。

整体架构如下图所示:

[前端 Vue.js (页面展示, 路由控制)] ←→ [后端 Spring Boot (API接口, 业务逻辑)]

[数据库 MySQL]

[缓存 Redis]

1.3主要技术栈:

前端:

Vue.js:负责构建博客的用户界面,使用 Vue Router 管理页面路由,Vuex 进行状态管理。

Element UI:作为 Vue.js 的 UI 组件库,提供丰富的 UI 组件,方便开发快速构建前端界面。

Axios:用于前端和后端之间的 HTTP 通信,执行数据请求。

后端:

Spring Boot:作为主框架,快速搭建 Web 应用。

Mybatis Plus:基于 Mybatis 的增强工具,简化了数据库操作,减少了开发人员的工作量。

Shiro + JWT:使用 Shiro 实现安全框架进行身份认证,结合 JWT 进行用户的 Token 验证。

Redis:用于缓存、会话存储等操作,提升系统的性能。

Hibernate Validator:实现后台的实体校验,确保数据输入的有效性。

Lombok:简化 Java 类的编写,减少 boilerplate 代码。

1.4 模块划分

本项目可以划分为以下几个模块:

1.前端模块:

首页模块:展示博客文章列表,分页展示。

博客详情模块:查看单篇博客的详情内容。

博客编辑模块:管理员可以编辑、删除博客文章。

用户模块:用户注册、登录、权限控制等。

路由管理模块:管理前端页面的路由,处理权限拦截。

2.后端模块:

用户管理模块:包括用户的注册、登录、权限校验、JWT 生成和解析。

博客管理模块:包括博客的创建、更新、删除、查询等操作。

权限模块:通过 Shiro 实现角色和权限控制,配合 JWT 实现 Token 身份认证。

数据缓存模块:使用 Redis 缓存博客数据、用户信息等,减少数据库访问频率。

异常处理模块:全局异常处理类,返回统一格式的错误信息。

输入校验模块:使用 Hibernate Validator 实现用户输入的校验,确保数据合法。

3.数据库模块:

MySQL:存储博客文章数据、用户信息、角色权限等。

Redis:缓存热点数据,如用户会话、博客列表等,提高系统响应速度。

1.5 容器集群化部署的任务内容

容器集群化部署任务内容主要涉及以下几个方面:

1.Docker 容器化部署:

通过 Dockerfile 构建前后端服务的镜像。

配置 Docker Compose 实现多容器管理(前端、后端、数据库等)。

构建和部署到 Docker 容器中,实现容器化部署。

2.容器集群化管理:

使用 Docker Swarm 或 Kubernetes 来管理容器集群,实现负载均衡、服务发现等。

配置 Nginx 作为反向代理,将请求路由到后端 API。

配置 Redis 和 MySQL 为容器服务,实现高可用部署。

3.日志和监控:

配置容器内日志输出,结合 ELK(Elasticsearch, Logstash, Kibana)或 Prometheus + Grafana 实现日志管理和监控。

配置容器的健康检查,确保系统的高可用性。

4.CI/CD 集成:

使用 Jenkins 或 GitLab CI 实现自动化构建和部署,确保每次代码提交后可以自动构建 Docker 镜像并推送到容器集群中。

1.6 项目容器化部署的目的

容器化部署的目的包括以下几点:

1.简化部署过程:

通过 Docker 容器将应用打包,减少部署时环境配置的差异问题。

在任何环境中(本地开发、测试、生产)都能保持一致的运行效果。

2.提高资源利用率:

使用 Docker 容器技术,多个容器可以共享同一台物理主机的资源,从而提高资源的利用率。

容器能够在不同环境中快速启动,节省时间和资源。

3.易于扩展:

容器化的应用可以方便地进行水平扩展,容器集群能够应对更高的流量需求。

通过集群管理工具(如 Kubernetes)实现自动伸缩,满足业务需求。

4.提升系统的可维护性和高可用性:

容器化部署使得每个模块都在独立的容器中运行,提高了系统的稳定性。

容器可以在失败时快速重启,保证系统的高可用性。

  1. 总体结构

本项目的总体结构分为前端、后端、数据库、缓存和容器化部署几个部分。具体来说:

前端 Vue.js 应用:展示用户界面,接收用户请求,发送 API 请求至后端。

后端 Spring Boot 应用:处理前端请求,执行业务逻辑,访问数据库,返回数据。

数据库 MySQL:存储用户、博客、评论等数据。

缓存 Redis:缓存热点数据,减少数据库压力。

容器化部署:前端、后端、数据库等都被部署在 Docker 容器中,Docker Compose 管理这些服务的启动、停止和扩展。

2.1 容器角色和功能

1.前端容器:

负责启动 Vue.js 应用,提供浏览器访问接口。

与后端容器进行通信,显示动态内容。

2.后端容器:

启动 Spring Boot 应用,提供 RESTful API 接口。

处理用户请求,进行权限验证,访问数据库。

3.数据库容器:

存储 MySQL 数据库,提供数据持久化。

数据库容器与后端容器之间通过网络连接,进行数据存储和读取操作。

4.缓存容器:

启动 Redis 容器,提供缓存服务。

后端容器使用 Redis 存储会话数据、博客列表等高频数据。

2.2 容器之间的关联关系

前端容器与后端容器:前端通过 HTTP 请求与后端容器进行通信。前端容器通过配置 Docker 网络访问后端 API,后端容器暴露端口供前端调用。

后端容器与数据库容器:后端通过 JDBC 连接数据库容器。数据库容器内部运行 MySQL 服务,后端通过容器内网络访问数据库。

后端容器与缓存容器:后端容器通过 Redis 客户端(如 Jedis 或 Lettuce)与 Redis 容器进行通信,缓存热点数据,减少数据库访问。

容器网络:所有容器都运行在同一个 Docker 网络内,确保容器之间可以通过服务名称进行互相访问。Docker Compose 会根据服务名称自动管理容器之间的网络连接。

2.3 数据流动示例

在本系统中,数据流动贯穿前端、后端、数据库和缓存之间,以下是一个具体的数据流动示例:假设用户访问博客列表页面,系统的处理过程如下:

1.前端请求:

1.用户访问博客列表页面,前端 Vue.js 应用通过 Axios 向后端 API 发起 GET 请求,获取博客文章列表。

2.请求路径类似:GET /api/blogs/list

2.后端接收请求:

1.Spring Boot 后端接收到请求后,首先会通过 Shiro + JWT 进行用户身份验证,确保请求合法。

2.如果验证通过,后端会查询缓存 Redis 中是否已存有博客列表数据(用于优化性能,减少数据库查询次数)。

3.如果缓存中有数据,则直接从 Redis 中获取,返回给前端。

3.数据库查询:

1.如果 Redis 中没有缓存数据,后端会通过 Mybatis Plus 访问 MySQL 数据库,查询所有博客文章列表。

2.数据库返回包含博客信息的列表。

4.缓存更新:

1.后端会将从数据库中查询到的数据缓存到 Redis 中,以便下次访问时直接从缓存获取,减少数据库负载。

5.响应前端:

1.后端将博客列表数据返回给前端,格式为 JSON。

2.前端 Vue.js 应用接收到数据后,通过 Vuex 或组件直接渲染博客列表页面。

6.前端显示:

1.用户可以在博客列表页面上查看所有博客文章的标题、摘要、发布时间等基本信息。

2.用户可以点击某篇文章的标题,进入博客详情页面。

3.详细设计

3.1 设计思路

本项目的设计思路是基于前后端分离、微服务架构和容器化部署,确保系统的灵活性、可扩展性和高可用性。详细设计考虑到系统的各个功能模块,优化了数据流动、接口设计和权限管理等方面。

以下是主要模块的设计思路:

1.前端设计:

1.前端使用 Vue.js 构建,确保用户交互流畅。

2.使用 Vue Router 管理路由,支持多页面导航,确保不同功能页面的独立性。

3.在路由控制中,前端实现了权限拦截,确保只有登录用户能访问某些页面。

2.后端设计:

1.后端采用 Spring Boot 架构,通过 RESTful API 提供服务。所有请求返回 JSON 格式的数据,方便前端处理。

2.权限控制:通过 Shiro 实现身份认证和权限管理,结合 JWT 生成 Token,保证用户的安全性。

3.异常处理:使用全局异常处理,统一返回错误信息,保证 API 的健壮性。

4.数据校验:后端使用 Hibernate Validator 进行数据校验,确保数据合法。

5.Mybatis Plus:通过 Mybatis Plus 实现对数据库的高效操作,减少 SQL 编写量。

3.数据库设计:

1.数据库采用 MySQL 存储博客文章、用户信息、评论等数据。每个表都设计了必要的索引,提高查询效率。

2.用户表设计了角色字段,支持不同角色的权限区分(如管理员、普通用户等)。

4.缓存设计:

1.使用 Redis 进行数据缓存,缓存博客列表、热门文章、用户会话等数据,提高访问速度,减少数据库压力。

5.容器化设计:

1.使用 Docker 进行项目容器化,前端、后端、数据库、缓存等服务都运行在不同的 Docker 容器中,互相之间通过 Docker 网络进行通信。

2.使用 Docker Compose 管理服务,确保服务之间的协调和自动化部署。

6.安全设计:

1.前后端通过 JWT 进行用户身份验证,确保数据传输过程中的安全性。

2.使用 Shiro 实现访问控制,确保不同角色有不同的访问权限。

7.CI/CD设计:

1.使用 Jenkins 或 GitLab CI 实现自动化构建和部署,确保代码修改后能快速构建 Docker 镜像并部署到生产环境。

3.2Java后端接口开发

1、前言

从零开始搭建一个项目骨架,最好选择合适,熟悉的技术,并且在未来易拓展,适合微服务化体系等。所以一般以Springboot作为框架基础,这是离不开的了。

然后数据层,常用的是Mybatis,易上手,方便维护。但是单表操作比较困难,特别是添加字段或减少字段的时候,比较繁琐,所以这里使用Mybatis Plus CRUD 操作,从而节省大量时间。

作为一个项目骨架,权限也是不能忽略的,Shiro配置简单,使用也简单,所以使用Shiro作为的的权限。考虑到项目可能需要部署多台,这时候的会话等信息需要共享,Redis是现在主流的缓存中间件,也适合项目。然后因为前后端分离,所以使用jwt作为用户身份凭证。

技术栈:

SpringBoot

mybatis plus

shiro

lombok

redis

hibernate validatior

jwt

2、新建Springboot项目

开发工具与环境:

idea

mysql

jdk 8

maven3.3.9

新建好的项目结构如下,SpringBoot版本使用的目前最新的2.2.6.RELEASE版本

图3-2

pom的jar包导入如下:

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-devtools

runtime

true

org.projectlombok

lombok

true

devtools:项目的热加载重启插件

lombok:简化代码的工具

3、整合mybatis plus

第一步:导入jar包

pom中导入mybatis plus的jar包,因为后面会涉及到代码生成,导入页面模板引擎,使用freemarker。

<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version></dependency><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope></dependency><!--mp代码生成器--><dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version></dependency>

第二步:然后去写配置文件

DataSource Configspring:

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: admin

mybatis-plus:

mapper-locations: classpath*:/mapper/**Mapper.xml

上面除了配置数据库的信息,还配置了myabtis plus的mapper的xml文件的扫描路径。

第三步:开启mapper接口扫描,添加分页插件

新建一个包:通过@mapperScan注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。PaginationInterceptor是一个分页插件。

com.markerhub.config.MybatisPlusConfig

@Configuration@EnableTransactionManagement@MapperScan("com.markerhub.mapper")public class MybatisPlusConfig {

@Bean

public PaginationInterceptor paginationInterceptor() {

PaginationInterceptor paginationInterceptor = new PaginationInterceptor();

return paginationInterceptor;

}

}

第四步:代码生成

使用mybatis plus了,官方给提供了一个代码生成器,然后我写上自己的参数之后,就可以直接根据数据库表信息生成entity、service、mapper等接口和实现类。

com.markerhub.CodeGenerator

首先在数据库中新建了一个user表:

CREATE TABLE m_user (
id bigint(20) NOT NULL AUTO_INCREMENT,
username varchar(64) DEFAULT NULL,
avatar varchar(255) DEFAULT NULL,
email varchar(64) DEFAULT NULL,
password varchar(64) DEFAULT NULL,
status int(5) NOT NULL,
created datetime DEFAULT NULL,
last_login datetime DEFAULT NULL,

PRIMARY KEY (id),

KEY UK_USERNAME (username) USING BTREE

) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE m_blog (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
title varchar(255) NOT NULL,
description varchar(255) NOT NULL,
content longtext,
created datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
status tinyint(4) DEFAULT NULL,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;INSERT INTO vueblog.m_user (id, username, avatar, email, password, status, created, last_login) VALUES ('1', 'markerhub', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', NULL, '96e79218965eb72c92a549dd5a330112', '0', '2020-04-20 10:44:01', NULL);

运行CodeGenerator的main方法,输入表名:m_user,生成结果如下:

图3-2-3

得到:

图3-2-4

在UserController中写个测试:

@RestController@RequestMapping("/user")

public class UserController {

@Autowired

UserService userService;

@GetMapping("/{id}")

public Object test(@PathVariable("id") Long id) {

return userService.getById(id);

}

}

访问:http://localhost:8080/user/1 获得结果如下,整合成功!

图3-2-5

3.1、统一结果封装

这里用到了一个Result的类,这个用于异步统一返回的结果封装。一般来说,结果里面有几个要素必要的

是否成功,可用code表示(如200表示成功,400表示异常)

结果消息

结果数据

所以可得到封装如下:

com.markerhub.common.lang.Result

@Datapublic class Result implements Serializable {

private String code;

private String msg;

private Object data;

public static Result succ(Object data) {

Result m = new Result();

m.setCode("0");

m.setData(data);

m.setMsg("操作成功");

return m;

}

public static Result succ(String mess, Object data) {

Result m = new Result();

m.setCode("0");

m.setData(data);

m.setMsg(mess);

return m;

}

public static Result fail(String mess) {

Result m = new Result();

m.setCode("-1");

m.setData(null);

m.setMsg(mess);

return m;

}

public static Result fail(String mess, Object data) {

Result m = new Result();

m.setCode("-1");

m.setData(data);

m.setMsg(mess);

return m;

}

}

4、整合shiro+jwt,并会话共享

考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,一般考虑使用redis来存储这些数据,所以,不仅仅需要整合shiro,同时也需要整合redis。在开源的项目中,找到了一个starter可以快速整合shiro-redis,配置简单。

而因为需要做的是前后端分离项目的骨架,所以一般会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro的过程中,需要引入jwt的身份验证过程。

那么就开始整合:

使用一个shiro-redis-spring-boot-starter的jar包,具体教程可以看官方文档:github.com/alexxiyang/...

第一步:导入shiro-redis的starter包:还有jwt的工具包,以及为了简化开发,引入了hutool工具包。

org.crazycake

shiro-redis-spring-boot-starter

3.2.1

cn.hutool

hutool-all

5.3.3

io.jsonwebtoken

jjwt

0.9.1

第二步:编写配置:

ShiroConfig

com.markerhub.config.ShiroConfig

/**

  • shiro启用注解拦截控制器
    /@Configurationpublic class ShiroConfig {
    @Autowired
    JwtFilter jwtFilter;
    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionDAO(redisSessionDAO);
    return sessionManager;
    }
    @Bean
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
    SessionManager sessionManager,
    RedisCacheManager redisCacheManager) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
    securityManager.setSessionManager(sessionManager);
    securityManager.setCacheManager(redisCacheManager);
    /

    * 关闭shiro自带的session,详情见文档

    */

    DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();

    DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();

    defaultSessionStorageEvaluator.setSessionStorageEnabled(false);

    subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);

    securityManager.setSubjectDAO(subjectDAO);

    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;

    }

    // 开启注解代理(默认好像已经开启,可以不要)

    @Bean

    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){

    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();

    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);

    return authorizationAttributeSourceAdvisor;

    }

    @Bean

    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {

    DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();

    return creator;

    }

    }

上面ShiroConfig,主要做了几件事情:

1.引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。

2.重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。

3.在ShiroFilterChainDefinition中,不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。

那么,接下来,聊聊ShiroConfig中出现的AccountRealm,还有JwtFilter。

AccountRealm

AccountRealm是shiro进行登录或者权限校验的逻辑所在,需要重写3个方法,分别是

supports:为了让realm支持jwt的凭证校验

doGetAuthorizationInfo:权限校验

doGetAuthenticationInfo:登录认证校验

总体看看AccountRealm的代码,然后逐个分析:

com.markerhub.shiro.AccountRealm

@Slf4j@Componentpublic 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 jwt = (JwtToken) token;

log.info("jwt----------------->{}", jwt);

String userId = jwtUtils.getClaimByToken((String) jwt.getPrincipal()).getSubject();

User user = userService.getById(Long.parseLong(userId));

if(user == null) {

throw new UnknownAccountException("账户不存在!");

}

if(user.getStatus() == -1) {

throw new LockedAccountException("账户已被锁定!");

}

AccountProfile profile = new AccountProfile();

BeanUtil.copyProperties(user, profile);

log.info("profile----------------->{}", profile.toString());

return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());

}

}

其实主要就是doGetAuthenticationInfo登录认证这个方法,可以看到通过jwt获取到用户信息,判断用户的状态,最后异常就抛出对应的异常信息,否者封装成SimpleAuthenticationInfo返回给shiro。 接下来逐步分析里面出现的新类:

1、shiro默认supports的是UsernamePasswordToken,而现在采用了jwt的方式,所以这里自定义一个JwtToken,来完成shiro的supports方法。

JwtToken

com.markerhub.shiro.JwtToken

public class JwtToken implements AuthenticationToken {

private String token;

public JwtToken(String token) {

this.token = token;

}

@Override

public Object getPrincipal() {

return token;

}

@Override

public Object getCredentials() {

return token;

}

}

2、JwtUtils是个生成和校验jwt的工具类,其中有些jwt相关的密钥信息是从项目配置文件中配置的:

@Component@ConfigurationProperties(prefix = "markerhub.jwt")public class JwtUtils {

private String secret;

private long expire;

private String header;

/**

* 生成jwt token

*/

public String generateToken(long userId) {

...

}

// 获取jwt的信息
public Claims getClaimByToken(String token) {
...
}

/**
 * token是否过期
 * @return  true:过期
 */
public boolean isTokenExpired(Date expiration) {
    return expiration.before(new Date());
}

}

3、而在AccountRealm还用到了AccountProfile,这是为了登录成功之后返回的一个用户信息的载体,

AccountProfile

com.markerhub.shiro.AccountProfile

@Datapublic class AccountProfile implements Serializable {

private Long id;

private String username;

private String avatar;

}

第三步,基本的校验的路线完成之后,需要少量的基本信息配置:

shiro-redis:

enabled: true

redis-manager:

host: 127.0.0.1:6379markerhub:

jwt:

加密秘钥

secret: f4e2e52034348f86b67cde581c0f9eb5

token有效时长,7天,单位秒

expire: 604800

header: token

第四步:另外,如果项目有使用spring-boot-devtools,需要添加一个配置文件,在resources目录下新建文件夹META-INF,然后新建文件spring-devtools.properties,这样热重启时候才不会报错。

resources/META-INF/spring-devtools.properties

restart.include.shiro-redis=/shiro-[\w-\.]+jar

图3-2-5

JwtFilter

第五步:定义jwt的过滤器JwtFilter。

这个过滤器是的重点,这里继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,有些继承BasicHttpAuthenticationFilter也是可以的。

需要重写几个方法:

1.createToken:实现登录,需要生成自定义支持的JwtToken

2.onAccessDenied:拦截校验,当头部没有Authorization时候,直接通过,不需要自动登录;当带有的时候,首先校验jwt的有效性,没问题就直接执行executeLogin方法实现自动登录

3.onLoginFailure:登录异常时候进入的方法,直接把异常信息封装然后抛出

4.preHandle:拦截器的前置拦截,因为是前后端分析项目,项目中除了需要跨域全局配置之外,再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。

下面看看总体的代码:

com.markerhub.shiro.JwtFilter

@Componentpublic class JwtFilter extends AuthenticatingFilter {

@Autowired

JwtUtils jwtUtils;

@Override

protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

// 获取 token

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 token = request.getHeader("Authorization");

if(StringUtils.isEmpty(token)) {

return true;

} else {

// 判断是否已过期

Claims claim = jwtUtils.getClaimByToken(token);

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 httpResponse = (HttpServletResponse) response;

try {

//处理登录失败的异常

Throwable throwable = e.getCause() == null ? e : e.getCause();

Result r = Result.fail(throwable.getMessage());

String json = JSONUtil.toJsonStr®;

httpResponse.getWriter().print(json);

} catch (IOException e1) {

}

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);

}

}

那么到这里,的shiro就已经完成整合进来了,并且使用了jwt进行身份校验。

5、异常处理

有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要程序员设计返回一个友好简单的格式给前端。

处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。

com.markerhub.common.exception.GlobalExceptionHandler

步骤二、定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。

/**

  • 全局异常处理
    /@Slf4j@RestControllerAdvicepublic class GlobalExcepitonHandler {
    // 捕捉shiro的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public Result handle401(ShiroException e) {
    return Result.fail(401, e.getMessage(), null);
    }
    /
    *

    • 处理Assert的异常
      /
      @ResponseStatus(HttpStatus.BAD_REQUEST)
      @ExceptionHandler(value = IllegalArgumentException.class)
      public Result handler(IllegalArgumentException e) throws IOException {
      log.error("Assert异常:-------------->{}",e.getMessage());
      return Result.fail(e.getMessage());
      }
      /
      *
    • @Validated 校验错误异常处理
      */
      @ResponseStatus(HttpStatus.BAD_REQUEST)
      @ExceptionHandler(value = MethodArgumentNotValidException.class)
      public Result handler(MethodArgumentNotValidException e) throws IOException {
      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 = RuntimeException.class)

    public Result handler(RuntimeException e) throws IOException {

    log.error("运行时异常:-------------->",e);

    return Result.fail(e.getMessage());

    }

    }

上面捕捉了几个异常:

ShiroException:shiro抛出的异常,比如没有权限,用户登录异常

IllegalArgumentException:处理Assert的异常

MethodArgumentNotValidException:处理实体校验的异常

RuntimeException:捕捉其他异常

6、实体校验

当表单数据提交的时候,前端的校验可以使用一些类似于jQuery Validate等js插件实现,而后端可以使用Hibernate validatior来做校验。

使用springboot框架作为基础,那么就已经自动集成了Hibernate validatior。

第一步:首先在实体的属性上添加对应的校验规则,比如:

@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;

@NotBlank(message = "邮箱不能为空")

@Email(message = "邮箱格式不正确")

private String email;

...

}

第二步 :这里使用@Validated注解方式,如果实体不符合要求,系统会抛出异常,那么的异常处理中就捕获到MethodArgumentNotValidException。

com.markerhub.controller.UserController

/**

  • 测试实体校验
  • @param user
  • @return
    */@PostMapping("/save")
    public Object testUser(@Validated @RequestBody User user) {
    return user.toString();
    }

7、跨域问题

因为是前后端分析,所以跨域问题是避免不了的,直接在后台进行全局跨域处理:

com.markerhub.config.CorsConfig

/**

  • 解决跨域问题
    /@Configurationpublic 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("*");
    }
    }

8、登录接口开发

登录的逻辑其实很简答,只需要接受账号密码,然后把用户的id生成jwt,返回给前段,为了后续的jwt的延期,所以把jwt放在header上。具体代码如下:

com.markerhub.controller.AccountController

@RestControllerpublic class AccountController {

@Autowired

JwtUtils jwtUtils;

@Autowired

UserService userService;

/**

* 默认账号密码:markerhub / 111111

*

*/

@CrossOrigin

@PostMapping("/login")

public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {

User user = userService.getOne(new QueryWrapper().eq("username", loginDto.getUsername()));

Assert.notNull(user, "用户不存在");

if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {

return Result.fail("密码错误!");

}

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())

.put("email", user.getEmail())

.map()

);

}

// 退出
@GetMapping("/logout")
@RequiresAuthentication
public Result logout() {
    SecurityUtils.getSubject().logout();
    return Result.succ(null);
}

}

接口测试:

图3-2-8

9、博客接口开发

的骨架已经完成,接下来,就可以添加的业务接口了,下面我以一个简单的博客列表、博客详情页为例子开发:

com.markerhub.controller.BlogController

@RestControllerpublic class BlogController {

@Autowired

BlogService blogService;

@GetMapping("/blogs")

public Result blogs(Integer currentPage) {

if(currentPage == null || currentPage < 1) currentPage = 1;

Page page = new Page(currentPage, 5)

IPage pageData = blogService.page(page, new QueryWrapper().orderByDesc("created"));

return Result.succ(pageData);

}

@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) {
System.out.println(blog.toString());
Blog temp = null;
if(blog.getId() != null) {
    temp = blogService.getById(blog.getId());
    Assert.isTrue(temp.getUserId() == ShiroUtil.getProfile().getId(), "没有权限编辑");
} else {
    temp = new Blog();
    temp.setUserId(ShiroUtil.getProfile().getId());
    temp.setCreated(LocalDateTime.now());
    temp.setStatus(0);
}
BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");
blogService.saveOrUpdate(temp);
return Result.succ("操作成功", null);

}

}

注意@RequiresAuthentication说明需要登录之后才能访问的接口,其他需要权限的接口可以添加shiro的相关注解。 接口比较简单,就不多说了,基本增删改查而已。注意的是edit方法是需要登录才能操作的受限资源。

接口测试:

图3-2-9

至此,后端开发完成。

3.3前端代码展示与部分后端代码展示

3.3.1.前言

前端使用的到技术如下:

element-ui

axios

mavon-editor

markdown-it

github-markdown-css

3.3.2.项目演示

图3-3-1

图3-3-2

图3-3-3

3.3.4 源码展示

图3-3-4

Mysql配置以及相关内容

图3-3-5

Dockerfile相关配置文件

图3-3-6

网站页面配置

图3-3-7

前端源码展示

图3-3-8

后端项目maven打包

图3-3-9

将前端build项目传入nignx中

4.实现与测试

4.1构建镜像

使用Docker Compose 构建所有服务的镜像:

图4-1

4.2启动服务

4.2.1启动docker-compose

图4-2-1

4.2.2 启动vue容器及其其他服务

图4-2-2

图4-2-3

4.3 成功进入前端页面

图4-3-1

图4-3-2

图4-3-3

4.4 Mysql数据库连接成功

图4-4

4.5文章管理界面

图4-5

5.结束语

本项目通过整合前后端分离、容器化部署以及多种现代化技术栈(如 Vue.js、Spring Boot、Mybatis Plus、JWT 等)构建了一个高效、可维护且具备高可用性的博客管理系统。通过权限控制、数据缓存、异常处理等一系列设计,确保系统能够提供流畅的用户体验和稳定的后端支持。容器化部署的引入不仅简化了部署过程,还提高了系统的可扩展性和容错性。

通过这次项目的实施,不仅帮助开发人员提高了前后端分离的开发能力,还深入了解了容器化技术的应用和微服务架构的实践,为今后的大规模分布式系统开发和维护奠定了坚实的基础。

6.参考文献

1.Spring Documentation

"Spring Framework Documentation." Spring.io. Available at: https://spring.io/docs

(Accessed: 2024)

2.Vue.js Documentation

"Vue.js --- The Progressive JavaScript Framework." Vue.js. Available at: https://vuejs.org/(Accessed: 2024)

3.Mybatis-Plus Documentation

"MyBatis-Plus." Mybatis-Plus. Available at: https://mybatis.plus/

(Accessed: 2024)

4.Redis Documentation

"Redis - In-memory data structure store." Redis.io. Available at: https://redis.io/documentation

(Accessed: 2024)

5.Shiro Documentation

"Apache Shiro --- A Powerful and Easy-to-Use Security Framework." Apache Shiro. Available at: https://shiro.apache.org/

(Accessed: 2024)

6.Docker Documentation

"Docker Docs." Docker.com. Available at: https://docs.docker.com/

(Accessed: 2024)

7.JWT.io Documentation

"JWT (JSON Web Tokens) Introduction." JWT.io. Available at: https://jwt.io/introduction/

(Accessed: 2024)

8.Hibernate Validator Documentation

"Hibernate Validator - Reference Documentation." Hibernate.org. Available at: https://hibernate.org/validator/

(Accessed: 2024)

相关推荐
PersistJiao39 分钟前
使用 Temporal 管理和调度 Couchbase SQL 脚本的实际例子
数据库·sql
dz88i81 小时前
关于Idea中database按钮不显示的问题
java·ide·intellij-idea
IT-民工211101 小时前
K8s部署MySQL
mysql·容器·kubernetes
Ian10251 小时前
postgresql使用 ST_PointFromText(“POINT()“, 4326)时字段 出现“POINT()“ 不存在SQL 状态: 42703
服务器·前端·sql
JhonKI1 小时前
【MySQL】表的约束(上)详解
android·数据库·mysql
水w2 小时前
微服务之间的相互调用的几种常见实现方式对比 2
java·开发语言·后端·微服务·架构
java菜鸡加油2 小时前
代码随想录-算法训练营day56(动态规划17:回文子串,最长回文子序列,动态规划总结篇)
java·算法·leetcode·动态规划·力扣
潘多编程2 小时前
SpringBoot 3.2:CRaC技术助力启动速度飞跃
java·spring boot·后端
牛奶2 小时前
SQL学习-增删改数据
前端·后端·mysql
「QT(C++)开发工程师」2 小时前
Qt | 开发工具(top1)
java·开发语言·qt