目录
- 任务描述 3
1.1课题的基本内容 3
1.2 项目整体技术架构 3
1.3主要技术栈: 3
1.4 模块划分 4
1.5 容器集群化部署的任务内容 5
1.6 项目容器化部署的目的 6 - 总体结构 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.提升系统的可维护性和高可用性:
容器化部署使得每个模块都在独立的容器中运行,提高了系统的稳定性。
容器可以在失败时快速重启,保证系统的高可用性。
- 总体结构
本项目的总体结构分为前端、后端、数据库、缓存和容器化部署几个部分。具体来说:
前端 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 实现访问控制,确保不同角色有不同的访问权限。
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());
}
}
- 处理Assert的异常
上面捕捉了几个异常:
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)