Springboot+vue博客管理系统
项目是前后端分离的一个项目,文章总体分为java后端接口和vue前端页面。
Java后端接口
1.前言
编译器使用的IDEA2021,一开始先创建新的项目骨架,直接选择Spring Initializr创建。数据层一开始学习springboot的时候用的mybatis,易上手也很方便,但是编写SQL语句时工作量很大,尤其是字段多、关联表多时,更是如此,所以用的mybatis-plus(https://baomidou.com/guide/),为简化而生只…,启动自动注入基本CRUD。性能基本无损耗,直接面向对象操作,还有分页插件等等。然后同时权限也是需要注意的,所以用了Shiro的配置,直接使用,节约时间。其次考虑项目可能要部署多台所以一些会话信息等需要共享,然后使用了redis,因为是前后端分离,因此使用jwt作为用户身份验证,简介完开始搭建项目脚手架。
使用技术:
- Springboot
- Mybatis plus
- Shiro
- lombok
- redis
- hibernate validatior
- jwt
项目目录概览:
2.新建Springboot项目
用IDEA开发项目,创建项目步骤比较简单就直接跳过了。
开发工具与环境:
- IDEA
- mysql
- jdk8
- maven3.6.3
pom的依赖引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
devtools
:项目加热部署lombok
:简化代码的工具例如用@Data
注解省略getter setter ToString等
3.整合mybatis plus
Mybatis-Puls官方文档
第一步:引入mybastis-puls依赖、以及模板引擎依赖后续还有使用代码生成
<!--mp-->
<dependency>
<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>
第二步:写yml配置文件
# DataSource Config
server:
port: 8082
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
这里配置了基本的接口、数据库信息以及还要配置mybatis-plus的mapper的xml文件的扫描路径
第三步:开启mapper接口扫描,添加分页插件
新建一个包:通过@mapperScan
注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。PaginationInterceptor
是一个分页插件。
@Configuration
@EnableTransactionManagement
@MapperScan("com.vueblog.mapper")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
-
@MapperScan
注解之前是,直接在Mapper类上面添加注解
@Mapper
,这种方式要求每一个mapper类都需要添加此注解,麻烦,通过使用@MapperScan
可以指定要扫描的Mapper类的包的路径 -
@EnableTransactionManagement
@EnableTransactionManagement表示开启事务支持,在springboot项目中一般配置在启动类上,效果等同于xml配置的<tx:annotation-driven />。开启事务支持后,然后在访问数据库的Service方法上添加注解
@Transactional
便可。
第四步:代码生成
官方文档有提供代码生成器,直接使用即可,写上了自己的参数后,就可以根据数据表信息生成entity层、service层、mapper层等
大致生成如下:
第五步:测试
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
public Object test(@PathVariable("id") Long id) {
return userService.getById(id);
}
}
数据库信息
postman检验:
4.统一结果封装
目前的前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式有利于前后端的交互与UI的展示。
创建一个Rusult的类,用来做异步统一返回的将结果封装主要是几点
- code-响应状态码(200表示成功 400表示异常)
- msg-响应信息
- data-响应数据
也可以是通过一般形式
- success-是否响应成功
- code-响应状态码
- message-状态码描述
- data-响应数据
- 其他标识符
@Data
public 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;
}
}
5.整合Shiro+jwt
Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多,1.Shiro官方文档2.shiro官方教程里面有教程如何上手shiro
JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,用于在各方之间以JSON对象安全传输信息,这些信息可以通过数字签名进行验证和信任。 可以使用RSA的公钥/私钥对对JWT进行签名。这里是通过采用token或者jwt作为跨域身份验证解决方案,来在登录环节进行用户身份验证,以获取资源。是目前最流行的跨域认证解决方案。
- 首先梳理一下shiro在项目里是怎么样的一个流程,以及用户登录,登录完成后这个jwt的校验逻辑是怎么样的,结合一下流程图:
- 首先用户通过用户名密码登录
- 后端进行校验,成功将生成jwt并且返回jwt给用户。如果在校验中出现密码错误,用户不存在等,将抛出一处。
- 再当用户拿到jwt凭证时,再去访问其他后端接口或者受限资源时,需要带上jwt,这是它的身份凭证,这样才能知道访问这个接口的用户到底是谁,结合流程图再梳理这个逻辑过程:
- 在进入接口之前,访问可能有分为用户登录和用户未登录状态,没登录前是没jwt身份凭证,登录了才有jwt身份凭证,有jwt的话在项目里有全局的处理逻辑,在接口之前都会有拦截或者过滤,在Shiro中就是一个过滤的逻辑,例如一个
JwtFilter
。 - 有jwt的话,交给shiro登录处理,调用shiro的Filter,内置的一些登录的逻辑,比如将jwt封装成token再使用shiro的
SecurityManager
进行一个登录,登录完成就能识别了当前用户是谁。 - 随即检验是正常或者异常(jwt过期、密钥不对)状态,如果是异常状态那么将抛出异常,之后将有全局异常处理器就行一个拦截,再返回一个
json
数据给前端。 - 正常或者无jwt的访问我们的接口,shiro除了需要对登录就行一个处理之外,还需要去判断资源的一个权限的情况,不如xxxcontroller需要一个admin管理员的角色进行一个管理,就需要在controller接口的前置设置一个拦截,可以通过一个注解拦截过滤例如controller前置一个
@RequiresRoles("admin")
注解就是需要admin用户权限才可以访问该接口。 - 经过注解过滤判断是否有权限,无权限的话同样抛出异常,交由全局异常处理器进行拦截;有权限就访问controller。
第一步:接下来先引入shiro-redis依赖、jwt依赖、还有hutool工具包
先简介一下hutool:
hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,Hutool是项目中“util”包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug。
以计算MD5为例:
👴【以前】打开搜索引擎 -> 搜“Java MD5加密” -> 打开某篇博客-> 复制粘贴 -> 改改好用
👦【现在】引入Hutool -> SecureUtil.md5()
Hutool的存在就是为了减少代码搜索成本,避免网络上参差不齐的代码出现导致的bug。
接着按官方文档引入依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
第二步:编写配置(直接套用官方文档)
先简述Shiro的工作流程:
Shiro进行认证的本质还是通过过滤器进行拦截,过滤器拦截后判断是否需要进行认证,如果需要,取出”token”并交给SecurityManager
进行认证,认证通过后放行,如果不需要认证则直接放行。
为什么token打上引号,是因为token并不一定是普遍意义上的JWT(json web token),也可以是基于BASIC HTTP的token,还可以是表单中的用户名和密码。
所以Shiro中就内置了一些常用的Filter,比如内置的AuthenticatingFilter
类,有基于表单认证的FormAuthenticationFilter
类和基于BASIC HTTP的BasicHttpAuthenticationFilter
类,还有不需要认证直接放行的AnonymousFilter
类,也有一些用于检查Roles
和Permissions
的过滤器,具体的可以去DefaultFilter
中看看。
- ShiroConfig:
按官方文档:
重写sessionManage和SessionsSecurityManager
/**
* shiro启用注解拦截控制器
*/
@Configuration
public 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