# vueadmin-api
**Repository Path**: hhgs_admin/vueadmin-api
## Basic Information
- **Project Name**: vueadmin-api
- **Description**: vueadmin后台api第一版环境
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2022-04-14
- **Last Updated**: 2024-05-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: 前后台分离管理系统
## README
##
项目参考文档地址:https://www.zhuawaba.com/post/19#1.%20%E5%89%8D%E8%A8%80
项目参考视频地址:https://www.bilibili.com/video/BV1af4y1s7Wh?p=54
一,新建springBoot项目
##### 方式一:用mybatis-spring-boot-starter集成
###### pom.xml
```xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.4.0
com.huihui
vueadmin-api
0.0.1-SNAPSHOT
vueadmin-api
vueadmin后台api
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
com.github.xiaoymin
knife4j-spring-boot-starter
2.0.8
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
org.springframework.boot
spring-boot-maven-plugin
```
###### application.yml
######
```yml
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.54:3307/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
mapper-locations: classpath*:/mapper/**Mapper.xml
```
###### 启动类增加注解:
```
@MapperScan("com.huihui.vueadminapi.mapper")
```
##### 方式二:用mybatis-plus-boot-starter集成
###### 1,pom.xml
这里加的是mybatis-plus-generator依赖。
```xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.4.0
com.huihui
vueadmin-api
0.0.1-SNAPSHOT
vueadmin-api
vueadmin后台api
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
com.github.xiaoymin
knife4j-spring-boot-starter
2.0.8
com.baomidou
mybatis-plus-boot-starter
3.4.1
org.springframework.boot
spring-boot-maven-plugin
```
###### 2,application.yml
```yml
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.54:3307/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
#mybatis:
# mapper-locations: classpath*:/mapper/**Mapper.xml
```
###### 启动类增加注解:
```java
@MapperScan("com.huihui.vueadminapi.mapper")
```
## 二:配置easycode模板:
#### 方式一:对应easycode模板:
新增模板生成映射文件前记得在数据库字段类型映射位置(File->Settings-->Other Settings-->EasyCode-->Type Mapper)位置增加tinyint的对应关系:tinyint((\d+))?
#### easycode模板
###### entity.java
```java
##引入宏定义
$!{define.vm}
##使用宏定义设置回调(保存位置与文件后缀)
#save("/entity", ".java")
##使用宏定义设置包后缀
#setPackageSuffix("entity")
##使用全局变量实现默认包导入
$!{autoImport.vm}
import java.io.Serializable;
import io.swagger.annotations.*;
import lombok.Data;
##使用宏定义实现类注释信息
#tableComment("实体类")
@Data
@ApiModel("$tableInfo.comment")
public class $!{tableInfo.name} implements Serializable {
private static final long serialVersionUID = $!tool.serial();
#foreach($column in $tableInfo.fullColumn)
#if(${column.comment})/**
* ${column.comment}
*/#end
@ApiModelProperty("$column.comment")
private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end
}
```
###### mapper.java
```java
##定义初始变量
#set($tableName = $tool.append($tableInfo.name, "Mapper"))
##设置回调
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/mapper"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}mapper;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表数据库访问层
*
* @author $!author
* @since $!time.currTime()
*/
@Mapper
@Repository
public interface $!{tableName} {
/**
* 通过ID查询单条数据
*
* @param $!pk.name 主键
* @return 实例对象
*/
$!{tableInfo.name} queryById($!pk.shortType $!pk.name);
/**
* 查询指定行数据
*
* @param offset 查询起始位置
* @param limit 查询条数
* @return 对象列表
*/
List<$!{tableInfo.name}> queryAllByLimit(@Param("offset") int offset, @Param("limit") int limit);
/**
* 通过实体作为筛选条件查询
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象
* @return 对象列表
*/
List<$!{tableInfo.name}> queryAll($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}));
/**
* 新增数据
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象
* @return 影响行数
*/
int insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}));
/**
* 修改数据
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象
* @return 影响行数
*/
int update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}));
/**
* 通过主键删除数据
*
* @param $!pk.name 主键
* @return 影响行数
*/
int deleteById($!pk.shortType $!pk.name);
}
```
###### mapper.xml
```java
##引入mybatis支持
$!{mybatisSupport.vm}
##设置保存名称与保存位置
$!callback.setFileName($tool.append($!{tableInfo.name}, "Mapper.xml"))
$!callback.setSavePath($tool.append($modulePath, "/src/main/resources/mapper"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#foreach($column in $tableInfo.fullColumn)
#end
insert into $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name}(#foreach($column in $tableInfo.otherColumn)$!column.obj.name#if($velocityHasNext), #end#end)
values (#foreach($column in $tableInfo.otherColumn)#{$!{column.name}}#if($velocityHasNext), #end#end)
update $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name}
#foreach($column in $tableInfo.otherColumn)
$!column.obj.name = #{$!column.name},
#end
where $!pk.obj.name = #{$!pk.name}
delete from $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name} where $!pk.obj.name = #{$!pk.name}
```
###### service.java
```java
##定义初始变量
#set($tableName = $tool.append($tableInfo.name, "Service"))
##设置回调
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/service"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}service;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import $!{tableInfo.savePackageName}.mapper.$!{tableInfo.name}Mapper;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表服务实现类
*
* @author $!author
* @since $!time.currTime()
*/
@Service("$!tool.firstLowerCase($!{tableInfo.name})Service")
public class $!{tableName} {
@Autowired
private $!{tableInfo.name}Mapper $!tool.firstLowerCase($!{tableInfo.name})Mapper;
/**
* 通过ID查询单条数据
*
* @param $!pk.name 主键
* @return 实例对象
*/
public $!{tableInfo.name} queryById($!pk.shortType $!pk.name) {
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.queryById($!pk.name);
}
/**
* 查询多条数据
*
* @param offset 查询起始位置
* @param limit 查询条数
* @return 对象列表
*/
public List<$!{tableInfo.name}> queryAllByLimit(int offset, int limit) {
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.queryAllByLimit(offset, limit);
}
/**
* 新增数据
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象
* @return 实例对象
*/
public $!{tableInfo.name} insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) {
this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.insert($!tool.firstLowerCase($!{tableInfo.name}));
return $!tool.firstLowerCase($!{tableInfo.name});
}
/**
* 修改数据
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象
* @return 实例对象
*/
public $!{tableInfo.name} update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) {
this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.update($!tool.firstLowerCase($!{tableInfo.name}));
return this.queryById($!{tool.firstLowerCase($!{tableInfo.name})}.get$!tool.firstUpperCase($pk.name)());
}
/**
* 通过主键删除数据
*
* @param $!pk.name 主键
* @return 是否成功
*/
public boolean deleteById($!pk.shortType $!pk.name) {
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.deleteById($!pk.name) > 0;
}
}
```
###### controller.java
```java
##定义初始变量
#set($tableName = $tool.append($tableInfo.name, "Controller"))
##设置回调
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/controller"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}controller;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service;
import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表控制层
*
* @author $!author
* @since $!time.currTime()
*/
@Api(tags = "$!{tableInfo.comment}($!{tableInfo.name})")
@RestController
@RequestMapping("$!tool.firstLowerCase($tableInfo.name)")
public class $!{tableName} {
/**
* 服务对象
*/
@Autowired
private $!{tableInfo.name}Service $!tool.firstLowerCase($tableInfo.name)Service;
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@ApiOperation(value = "根据id查询 $!{tableInfo.comment}")
@GetMapping("selectOne/{id}")
public $!{tableInfo.name} selectOne(@ApiParam(value = "$!pk.comment ID") @PathVariable("id") $!pk.shortType id) {
return this.$!{tool.firstLowerCase($tableInfo.name)}Service.queryById(id);
}
}
```
####
##
#### 方式二:对应easycode模板:
新增模板生成映射文件前记得在数据库字段类型映射位置(File->Settings-->Other Settings-->EasyCode-->Type Mapper)位置增加tinyint的对应关系:tinyint((\d+))?
#### easycode模板
###### entity.java.vm
```java
##导入宏定义
$!{define.vm}
##保存文件(宏定义)
#save("/entity", ".java")
##包路径(宏定义)
#setPackageSuffix("entity")
##自动导入包(全局变量)
$!{autoImport.vm}
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
##表注释(宏定义)
##tableComment("表实体类")
/**
* $!{tableInfo.comment}($!{tableInfo.name})表实体类
*
* @author $!author
* @since $!time.currTime()
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "")
@SuppressWarnings("serial")
public class $!{tableInfo.name} extends Model<$!{tableInfo.name}> implements Serializable {
private static final long serialVersionUID = $!tool.serial();
#foreach($column in $tableInfo.fullColumn)
##if(${column.comment})/**
##* ${column.comment}
##*/#end
@ApiModelProperty("$column.comment")
private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end
}
```
###### mapper.java.vm
```java
##定义初始变量
#set($tableName = $tool.append($tableInfo.name, "Mapper"))
##设置回调
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/mapper"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}mapper;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表数据库访问层
*
* @author $!author
* @since $!time.currTime()
*/
public interface $!{tableName} extends BaseMapper<$!{tableInfo.name}>{
}
```
###### service.java.vm
```java
##定义初始变量
#set($tableName = $tool.append($tableInfo.name, "Service"))
##设置回调
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/service"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}service;
import com.baomidou.mybatisplus.extension.service.IService;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
/**
* $!{tableInfo.comment}($!{tableInfo.name})表服务接口层
*
* @author $!author
* @since $!time.currTime()
*/
public interface $!{tableInfo.name}Service extends IService<$!{tableInfo.name}>{
}
```
###### impl.java.vm
```java
##导入宏定义
$!{define.vm}
##设置表后缀(宏定义)
#setTableSuffix("ServiceImpl")
##保存文件(宏定义)
#save("/service/impl", "ServiceImpl.java")
##包路径(宏定义)
#setPackageSuffix("service.impl")
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import $!{tableInfo.savePackageName}.mapper.$!{tableInfo.name}Mapper;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service;
import org.springframework.stereotype.Service;
##表注释(宏定义)
##tableComment("表服务实现类")
/**
* $!{tableInfo.comment}($!{tableInfo.name})表服务实现类
*
* @author $!author
* @since $!time.currTime()
*/
@Service
public class $!{tableInfo.name}ServiceImpl extends ServiceImpl<$!{tableInfo.name}Mapper, $!{tableInfo.name}> implements $!{tableInfo.name}Service {
}
```
###### controller.java.vm
```java
##定义初始变量
#set($tableName = $tool.append($tableInfo.name, "Controller"))
##设置回调
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/controller"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}controller;
import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表服务控制层
*
* @author $!author
* @since $!time.currTime()
*/
@Api(tags = "$!{tableInfo.comment}($!{tableInfo.name})")
@Validated
@RestController
@AllArgsConstructor
@RequestMapping("$!tool.firstLowerCase($tableInfo.name)")
public class $!{tableName} {
@Resource
private final $!{tableInfo.name}Service $!tool.firstLowerCase($tableInfo.name)Service;
}
```
###### mapper.xml.vm
```xml
##引入mybatis支持
$!{mybatisSupport.vm}
##设置保存名称与保存位置
$!callback.setFileName($tool.append($!{tableInfo.name}, "Mapper.xml"))
$!callback.setSavePath($tool.append($modulePath, "/src/main/resources/mapper"))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#foreach($column in $tableInfo.fullColumn)
#end
```
## 三:初始化数据库
- 新建数据库:vueadmin
- 导入vueadmin.sql文件
## 四:结果封装
因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。
这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的
- 是否成功,可用code表示(如200表示成功,400表示异常)
- 结果消息
- 结果数据
所以可得到封装如下:
```java
package com.huihui.vueadminapi.common.lang;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result implements Serializable {
private int code;
private String msg;
private Object data;
public static Result succ(Object data) {
return succ(200, "操作成功", data);
}
public static Result succ(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
public static Result fail(String msg) {
return fail(400, msg, null);
}
public static Result fail(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
}
```
另外出了在结果封装类上的code可以提现数据是否正常,我们还可以通过http的状态码来提现访问是否遇到了异常,比如401表示五权限拒绝访问等,注意灵活使用。
## 五:全局异常处理
有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。
处理办法如下:通过使用[@ControllerAdvice](https://github.com/ControllerAdvice)来进行统一异常处理,[@ExceptionHandler](https://github.com/ExceptionHandler)(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。
步骤二、定义全局异常处理,[@ControllerAdvice](https://github.com/ControllerAdvice)表示定义全局控制器异常处理,[@ExceptionHandler](https://github.com/ExceptionHandler)表示针对性异常处理,可对每种异常针对性处理。
```java
package com.huihui.vueadminapi.common.exception;
import com.huihui.vueadminapi.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 实体校验异常捕获
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handler(MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
ObjectError objectError = result.getAllErrors().stream().findFirst().get();
log.error("实体校验异常:----------------{}", objectError.getDefaultMessage());
return Result.fail(objectError.getDefaultMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) {
log.error("Assert异常:----------------{}", e.getMessage());
return Result.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handler(RuntimeException e) {
log.error("运行时异常:----------------{}", e.getMessage());
return Result.fail(e.getMessage());
}
}
```
上面我们捕捉了几个异常:
- ShiroException:shiro抛出的异常,比如没有权限,用户登录异常
- IllegalArgumentException:处理Assert的异常
- MethodArgumentNotValidException:处理实体校验的异常
- RuntimeException:捕捉其他异常
## 六:整合Security
很多人不懂spring security,觉得这个框架比shiro要难,的确,security更加复杂一点,同时功能也更加强大,我们首先来看一下security的原理,这里我们引用一张来自江南一点雨大佬画的一张原理图([https://blog.csdn.net/u012702547/article/details/89629415](https://blog.csdn.net/u012702547/article/details/89629415?fileGuid=OnZDwoxFFL8bnP1c)):

(引自江南一点雨的博客)
上面这张图一定要好好看,特别清晰,毕竟security是责任链的设计模式,是一堆过滤器链的组合,如果对于这个流程都不清楚,那么你就谈不上理解security。那么针对我们现在的这个系统,我们可以自己设计一个security的认证方案,结合江南一点雨大佬的博客,我们得到这样一套流程:
https://www.processon.com/view/link/606b0b5307912932d09adcb3?fileGuid=OnZDwoxFFL8bnP1c

流程说明:
1. 客户端发起一个请求,进入 Security 过滤器链。
2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。
3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
4. 进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
5. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
Spring Security 实战干货:必须掌握的一些内置 Filter:[https://blog.csdn.net/qq_35067322/article/details/102690579](https://blog.csdn.net/qq_35067322/article/details/102690579?fileGuid=OnZDwoxFFL8bnP1c)
ok,上面我们说的流程中涉及到几个组件,有些是我们需要根据实际情况来重写的。因为我们是使用json数据进行前后端数据交互,并且我们返回结果也是特定封装的。我们先再总结一下我们需要了解的几个组件:
- LogoutFilter - 登出过滤器
- logoutSuccessHandler - 登出成功之后的操作类
- UsernamePasswordAuthenticationFilter - from提交用户名密码登录认证过滤器
- AuthenticationFailureHandler - 登录失败操作类
- AuthenticationSuccessHandler - 登录成功操作类
- BasicAuthenticationFilter - Basic身份认证过滤器
- SecurityContextHolder - 安全上下文静态工具类
- AuthenticationEntryPoint - 认证失败入口
- ExceptionTranslationFilter - 异常处理过滤器
- AccessDeniedHandler - 权限不足操作类
- FilterSecurityInterceptor - 权限判断拦截器、出口
有了上面的组件,那么认证与授权两个问题我们就已经接近啦,我们现在需要做的就是去重写我们的一些关键类。
#### 引入Security与jwt
首先我们导入security包,因为我们前后端交互用户凭证用的是JWT,所以我们也导入jwt的相关包,然后因为验证码的存储需要用到redis,所以引入redis。最后为了一些工具类,我们引入hutool。
- pom.xml
```xml
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-data-redis
io.jsonwebtoken
jjwt
0.9.1
com.github.axet
kaptcha
0.0.9
cn.hutool
hutool-all
5.3.3
org.apache.commons
commons-lang3
3.11
```
```java
package com.huihui.vueadminapi.controller;
import com.huihui.vueadminapi.common.lang.Result;
import com.huihui.vueadminapi.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description: 测试类
* @author: liyonghui
* @date: 2022/4/25 9:57
*/
@RestController
public class TestController {
@Autowired
SysUserService sysUserService;
@GetMapping("/test")
public Result test() {
return Result.succ(sysUserService.list());
}
}
```
启动redis,然后我们再启动项目,这时候我们再去访问[http://localhost:8081/test](http://localhost:8081/test?fileGuid=OnZDwoxFFL8bnP1c),会发现系统会先判断到你未登录跳转到[http://localhost:8081/login](http://localhost:8081/login?fileGuid=OnZDwoxFFL8bnP1c),因为security内置了登录页,用户名为user,密码在启动项目的时候打印在了控制台。登录完成之后我们才可以正常访问接口。
因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码:
- application.yml
```yaml
spring:
security:
user:
name: user
password: 111111
redis:
database: 0
host: 192.168.101.54
port: 6379
password: 123456
```
清空浏览器的Cookie再次访问http://localhost:8081/test 使用 配置的账号密码登录即可正常访问并返回结果。
#### 用户认证
首先我们来解决用户认证问题,分为首次登陆,和二次认证。
- 首次登录认证:用户名、密码和验证码完成登录
- 二次token认证:请求头携带Jwt进行身份认证
使用用户名密码来登录的,然后我们还想添加图片验证码,那么security给我们提供的UsernamePasswordAuthenticationFilter能使用吗?
首先security的所有过滤器都是没有图片验证码这回事的,看起来不适用了。其实这里我们可以灵活点,如果你依然想沿用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。
我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。
#### 引入Redis
pom中上述引入的redis的start
##### yml配置
```yaml
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.54:3307/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
security:
user:
name: user
password: 111111
#redis配置
redis:
database: 0
host: 192.168.101.54
port: 6379
password: 123456
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
```
##### Redis工具类:
```java
package com.huihui.vueadminapi.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map