# 云上OA
**Repository Path**: swis/on-cloud-oa
## Basic Information
- **Project Name**: 云上OA
- **Description**: 2023尚硅谷云尚办公视频项目练习
- **Primary Language**: Java
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 4
- **Forks**: 1
- **Created**: 2023-05-07
- **Last Updated**: 2024-03-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
新手的项目记录
> 视频链接:[云尚办公](https://www.bilibili.com/video/BV1oM41177Jd/?p=109&spm_id_from=333.1007.top_right_bar_window_history.content.click)
### 环境搭建
Maven搭建
#### 创建工程
创建工程后删除src目录,新建模块,工程结构如图,注意创建子模块父模块的选择
#### 引入依赖
#### 配置mp
[开启mysql时遇到的问题](https://blog.csdn.net/weixin_44235530/article/details/115760098?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167992242416800217275176%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=167992242416800217275176&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-1-115760098-null-null.142^v76^insert_down2,201^v4^add_ask,239^v2^insert_chatgpt&utm_term=net%20start%20mysql%20%E6%9C%8D%E5%8A%A1%E5%90%8D%E6%97%A0%E6%95%88%E3%80%82&spm=1018.2226.3001.4187)
在service-util中新建springboot的配置文件
application.yml
```yml
spring:
application:
name: service-oa
profiles:
active: dev
```
application-dev.yml
可以改成Druid
实体类,mp注解
继承自公共类BeanEntity
使用IdType.AUTO会自动把该字段注入进来
表里面可以没有这个对应的字段
mapper的增删改操作可以直接继承mp的提供的接口
```java
package com.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sy.system.SysRole;
public interface SysRoleMapper extends BaseMapper {
}
```
建立测试类
在java,test包下创建测试类,使用注解,就可以进行测试了
记得@SpringBootTest(classes = 启动类名)
```java
package com.sy.auth;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = ServiceAuthApplication.class)
public class testService {
@Autowired
SysRoleService sysRoleService;
@Test
public void test(){
System.out.println(sysRoleService.list());
}
}
```
测试mapper的操作
上面mapper是一个接口,调用方法的时候需要用到它的实现类对象
更新的时候,updateById传的是一个对象
删除操作时一般都是对字段进行逻辑删除的
首先表中要有is_deleted的字段
mp中提供的注解
调用mapper中的删除方面,自动进行逻辑删除
批量删除
```java
sysRoleMapper.deleteBatchIds(Arrays.asList(11, 12))
```
条件查询
```java
@Test
public void Query1(){
//条件查询
QueryWrapper wrapper = new QueryWrapper<>();
//这里需要跟表中的字段名一致
// wrapper.eq("role_name","总经理");
// wrapper.ge("id",2);
wrapper.clear();
System.out.println(sysRoleMapper.selectList(wrapper));
}
@Test
public void Query2(){
//lambda条件查询
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
// wrapper.eq("role_name","总经理");
// wrapper.ge("id",2);
//这里只要写对应的属性
wrapper.eq(SysRole::getRoleName, "普通管理员");
System.out.println(sysRoleMapper.selectList(wrapper));
}
```
controller返回数据有统一的格式要求,例
```json
{
"code": 200,
"message": "成功",
"data":[
{
"id": 2,
"rolename": "系统管理员"
}
],
"ok": true
}
```
这是有数据的结果
无数据的结果,data部分为null
还有分页格式的结果
#### 统一返回格式
项目中都要用到统一返回结果
这里设计一个统一结果返回类Result,根据controller的处理结果返回前端一个Result类型的对象
因为不同的数据返回结果之间存在出入
这里用泛型接收要传回的数据,根据数据生成真正的类型
Result统一消息类型
```java
public class Result {
//三个属性
private Integer code;
private String message;
private T data;
//构造私有化
private Result(){}
}
```
我们要传回去的就是这种类型的对象,前端直接拿对象里的值就行了
因为消息只有那么几种,所以限制开发者不能随意设置,统一规范,所以将构造方法私有化
后端开发者只需要选择返回消息的类型就好了,通过静态方法向外部提供
因为状态码和消息是一一对应的,而且只有固定的选择,这里用枚举类进行封装
相当于是一个本身类型的常量,跟构造方法的格式有点像
现在开始给Result设计传入数据的方法
```java
package com.sy.common.result;
import lombok.Data;
@Data
public class Result {
private Integer code;
private String message;
private T data;
//构造私有化,外部不能调用内部的方法,只能设置static方法给外部调用
private Result(){}
//封装返回的数据
public static Result build(T body, ResultCodeEnum resultCodeEnum){
Result result = new Result<>();
if(body != null){
result.setData( body);
}
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
//设置消息类型
//无数据的成功消息
public static Result ok(){
return build(null, ResultCodeEnum.SUCCESS);
}
//有数据成功消息
public static Result ok(T data){
return build(data, ResultCodeEnum.SUCCESS);
}
//失败
public static Result fail(){
return build(null, ResultCodeEnum.FAIL);
}
}
```
调用:
```java
@GetMapping("/findAll")
public Result findAll(){
List list = sysRoleService.list();
return Result.ok(list);
}
```
#### 关于Service的接口
```java
public interface SysRoleService extends IService {
}
```
```java
@Service
public class SysRoleServiceImpl extends ServiceImpl implements SysRoleService {
}
```
#### 接口测试
前后端分离开发中,api文档是最好的沟通方式
Swagger是一个规范和完整的框架,能够生成在线文档
knife4j美化了swagger-ui
这个也是公共的
1. 引入依赖
```xml
com.github.xiaoymin
knife4j-spring-boot-starter
```
2. 创建配置类
使用注解@Configuration,@Bean
3. 使用
可以在Controller中添加提示注解
由于springboot版本导致的报错[参考博客](https://blog.csdn.net/weixin_46411355/article/details/128884811?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167999515616800215074941%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=167999515616800215074941&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-128884811-null-null.142^v76^insert_down2,201^v4^add_ask,239^v2^insert_chatgpt&utm_term=Failed%20to%20start%20bean%20documentationPluginsBootstrapper%3B%20nested%20exception%20is%20java.lang.NullPointerException&spm=1018.2226.3001.4187)
在配置文件中加这个
```yml
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
```
#### 分页查询
1. 配置分页插件
[官网](https://baomidou.com/pages/2976a3/#spring-boot)
mapper扫描到配置文件中
springboot启动类换一个扫描包
> 在这里出现了一个bug,因为换了包名,service扫描不到了,把service的包加上,**大括号加逗号**(字符串数组)
>
> 后面还是改了一下包名,我太马虎了
2. 编写controller分页方法
由前端传来两个参数,页号和条数,和一个条件对象
这个条件对象是自己写的,用来对应前端传来的参数,里面只有一个字段,就是用户输入的信息
mp提供一个Page类型来接收page信息
获得信息,使用StringUtils(Spring提供的)对字符串进行判空操作,是否为空或空字符串
使用lambda条件查询
3. 调动service实现分页查询
仔细观察一下这里,使用的是@GetMapping,前端除了发送页号和限制项数之外,还有模糊查询所输入的字符。
这里直接用一个对象接收完全没有问题,**此时要求传参的名称必须用controller中的相同**,否则要使用@RequrestParam注解
[参考博客](https://blog.csdn.net/qq_36685996/article/details/120155107?ops_request_misc=&request_id=&biz_id=102&utm_term=@GetMapper%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E6%8E%A5%E6%94%B6%E5%8F%82%E6%95%B0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-120155107.142^v77^insert_down38,201^v4^add_ask,239^v2^insert_chatgpt&spm=1018.2226.3001.4187)
使用get请求传参是url传参,是没有请求体的,如果想使用请求体传参则使用@PostMapper,使用@RequestBody(请求体)解析JSON格式的对象
> 在rest或者restful风格中,查询使用get请求,添加使用post,修改使用put请求(@PutMapping),删除使用@DeleteMapping
#### 批量删除
如图,JSON数据常见的格式有两种,对象格式和数组格式,正好可以与java对应,使用@RequestBody接收
[@PathVariable、@RequestParam、@RequestBody三者使用](https://blog.csdn.net/weixin_43605266/article/details/114991039?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168009795916800184116473%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168009795916800184116473&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-1-114991039-null-null.142^v77^insert_down38,201^v4^add_ask,239^v2^insert_chatgpt&utm_term=%40pathvariable%E5%92%8C%40requestbody&spm=1018.2226.3001.4187)
@PathVariable对应url中{}中的参数
@RequestParam、@RequestBody都对应的是请求中的数据,例:
```java
@GetMapping("role")
@ResponseBody
public UserRole getUserRole(@RequestParam Integer id) {
return userRoleMapper.getRole(id);
}
```
根据type不同,使用不同的注解
```java
@Api(tags = "角色管理")
@RestController
@RequestMapping("/admin/system/sysRole")
public class SysRoleController {
@Autowired
SysRoleService sysRoleService;
@ApiOperation("查询所有角色")
@GetMapping("findAll")
public Result findAll(){
List list = sysRoleService.list();
return Result.ok(list);
}
@ApiOperation("条件分页查询")
@GetMapping("{page}/{limit}")
public Result pageQueryRole(@PathVariable Long page,
@PathVariable Long limit,
SysRoleQueryVo sysRoleQueryVo){
Page pageParam = new Page<>(page, limit);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
String sysRoleName = sysRoleQueryVo.getRoleName();
if(!StringUtils.isEmpty(sysRoleName)){
wrapper.like(SysRole::getRoleName, sysRoleName);
}
sysRoleService.page(pageParam, wrapper);
return Result.ok(pageParam);
}
@ApiOperation("添加角色")
@PostMapping("save")
public Result save(@RequestBody SysRole sysRole){
boolean isSuccess = sysRoleService.save(sysRole);
return isSuccess ? Result.ok() : Result.fail();
}
@ApiOperation("根据id进行查询")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id){
SysRole sysRole = (SysRole) sysRoleService.getById(id);
return Result.ok(sysRole);
}
@ApiOperation("修改角色")
@PutMapping("update")
public Result update(@RequestBody SysRole sysRole){
boolean isSuccess = sysRoleService.updateById(sysRole);
return isSuccess ? Result.ok() : Result.fail();
}
@ApiOperation("根据id删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id){
boolean isSuccess = sysRoleService.removeById(id);
return isSuccess ? Result.ok() : Result.fail();
}
@ApiOperation("批量删除")
@DeleteMapping("batchRemove")
public Result BatchRemove(@RequestBody List idList){
boolean isSuccess = sysRoleService.removeBatchByIds(idList);
return isSuccess ? Result.ok() : Result.fail();
}
}
```
#### 时间格式
json的默认时区问题
application-dev.yml添加以下内容,注意跟DataSource同级
```yaml
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
```
#### 统一返回格式(异常)
根据不同的异常返回不同的情况,使用统一的格式
有三种情况:
- 全局异常
- 特定异常
- 自定义异常
实现过程:
1. 创建一个类专门对捕获的异常进行处理
2. 使用相应的注解(@ControllerAdvice)将该类交给SpringBoot管理,本质上是一个Controller
3. 处理异常(@ExceptionHandler)
自定义异常过程:
创建一个自定义异常类,继承自RuntimeException
两个变量,一个状态码和message
```java
@Data
public class CustomException extends RuntimeException {
private Integer code;
private String msg;
public CustomException(Integer code, String msg){
super(msg);
this.code = code;
this.msg = msg;
}
/**
* 枚举类型的参数
* @Param resultCodeEnum
*/
public CustomException(ResultCodeEnum resultCodeEnum) {
super(resultCodeEnum.getMessage());
this.code = resultCodeEnum.getCode();
this.msg = resultCodeEnum.getMessage();
}
@Override
public String toString() {
return "CustomException{" +
"code=" + code +
", msg='" + msg + '\'' +
'}';
}
}
```
自定义异常系统不会自定义捕获,需要自己手动捕获抛出
如这里0作为除数抛出自定义异常
```java
try{
int i/0;
} catch(Exception e){
throw new CustomException(2001, "执行自定义异常...");
}
```
首先创建一个处理异常的类,名称这里是GlobalExceptionHandler
> [@ControllerAdvice解释](https://blog.csdn.net/weixin_57726902/article/details/128018761?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168010474816800225580833%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168010474816800225580833&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-1-128018761-null-null.142^v77^insert_down38,201^v4^add_ask,239^v2^insert_chatgpt&utm_term=%40controlleradvice%E6%B3%A8%E8%A7%A3%E4%BD%9C%E7%94%A8&spm=1018.2226.3001.4187)
>
> 本质上就是@Controller,在此基础上做了一些增强
加上@RespondBody返回结果
```java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(){
return Result.fail().message("全局异常处理。。。。。");
}
@ExceptionHandler(CustomException.class)
@ResponseBody
public Result error(CustomException e){
e.printStackTrace();
return Result.fail().code(e.getCode()).message(e.getMsg());
}
}
```

[不知道mysql怎么搞的](https://blog.csdn.net/m0_46278037/article/details/113923726?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166752741216782390575410%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166752741216782390575410&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-113923726-null-null.142%5Ev63%5Econtrol,201%5Ev3%5Econtrol,213%5Ev1%5Et3_esquery_v3&utm_term=ERROR%201045%20%2828000%29%3A%20Access%20denied%20for%20user%20root%40localhost%20%28using%20password%3A%20NO%29&spm=1018.2226.3001.4187)
### 前端知识
#### 基本知识
模板字符串: ``,${}
对象扩展运算符:就是对象的复制
```java
let obj = {name: "xxx", age: 20}
let objCopy = { ... obj }
```
箭头函数:跟lambda表达式差不多,只不过箭头是`=>`
一个参数就不用括号,多个参数加括号,用逗号隔开
vue的简单使用
在一个html文件中,创建一个div标签设置id
在script标签中创建一个vue对象,我们在script标签中书写js代码
生命周期
```html
```
Axios
先将axios引入,直接调用axios里的方法
```js
//js 的普通方法
getList(){
//使用axios发送get请求
axios.get("接口名称").then().catch()
}
```
根据返回的结果,如果返回成功,则进入被`then()`方法捕获,失败被`catch()`捕获
需要用一个对象来接收,这里直接使用箭头表达式来创建对象
```js
axois.get("xxx").then(response => {
this.userList = response.data.data
})
```
`reponse`是响应体,后端返回的是Result对象,实际的数据在Result对象的data中
Node.js
类似于java中的JDK,是js的运行环境
一般使用js需要引入CND,在node.js环境下能够直接运行js文件
在控制台输入`node 文件名.js`
直接执行js文件,在控制台输出
[环境配置](https://blog.csdn.net/m0_67392273/article/details/126113759?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168015034816800211598175%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168015034816800211598175&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-126113759-null-null.142^v77^insert_down38,201^v4^add_ask,239^v2^insert_chatgpt&utm_term=%E9%85%8D%E7%BD%AEnode.js&spm=1018.2226.3001.4187)
NPM(npm)
node.js的包管理工具,相当于java中的maven
可以创建一个标准的前端项目、管理前端依赖
安装node.js时会附带安装npm工具
1. 生成项目
```js
//项目初始化
npm init
//使用默认值初始化
npm init -y
```
会创建一个文件,package.json,类似于java中的maven的pom.xml
2. 下载依赖
可以配置镜像(跟配置阿里镜像一样)
```js
//设置镜像地址
npm config set registry https://registry.npm.taobao.org
//查看配置
npm config list
//下载依赖
npm install 依赖名称 [指定版本,默认为最新版本]
```
或者根据配置文件下载依赖,`npm install`
模块化开发
就是js文件之间的相互引用。每一个js文件中的变量、函数、类都是私有的,对其他文件不可见
导出模块:`export`,在方法或者对象前加这个表示能被其他模块调用,相当于public
导入模块:`import`
局部导出和导入
```js
//文件1
export function fun1(){
console.log("fun1")
}
//文件2
import {fun1, fun2...} from "文件路径(.js可以省略)"
fun1()
```
文件内容全部导出
```js
export default {
//方法1
//方法2
}
import 名称 from "文件"
```
这个名称自定义,相当于起了一个代表文件的对象名
使用文件里的函数时可以这样,类似于对象调用成员方法
```js
名称.方法1
```
在控制台中输入:` node 文件2`,无法正常执行
因为ES6的模块化方式无法在node.js中执行,需要用**Bable**编辑成ES5后再执行,node执行转码后的文件
Bable
框架里一般会直接有,要知道是干什么的
Babel的配置文件是`babelrc`,存放在项目的根目录下,该文件用来设置转码规则和插件
presets字段设定转码规则,然后要安装转码器
转码,把当前文件里的文件转码到目标目录
```js
babel src -d dir
```
dir为指定输出目录,可以`mkdir`一个
#### 引入框架
vue-admin-template是后台管理的精简版
[GitHub地址](https://github.com/PanJiaChen/vue-admin-template)
1. 在GitHub中下载zip文件,解压到工作区
2. 根据配置文件下载相关依赖
3. 启动项目`npm run dev`
src目录下的main.js作为全局导入
框架本身模拟接口在mock文件夹下,要把模板的接口改成自己的
#### 前端接口路径修改
1. 在vue.config.js中做如下修改
```js
// before: require('./mock/mock-server.js')
proxy: {
'/dev-api': { // 匹配所有以 '/dev-api'开头的请求路径
target: 'http://localhost:8800',
changeOrigin: true, // 支持跨域
pathRewrite: { // 重写路径: 去掉路径中开头的'/dev-api'
'^/dev-api': ''
}
}
}
```
登录接口
根据模拟响应给出对应格式的消息
```java
@Api(tags = "登录")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {
//login
@PostMapping("login")
public Result login(){
//{"code":20000,"data":{"token":"admin-token"}},这个状态码由前端去修改
Map map = new HashMap<>();
map.put("token", "admin-token");
return Result.ok(map);
}
//info
@GetMapping("info")
public Result info(){
Map map = new HashMap<>();
map.put("roles", "[admin]");
map.put("name", "admin");
map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
return Result.ok(map);
}
@PostMapping ("logout")
public Result logout(){
return Result.ok();
}
}
```
2. 修改前端的访问路径
3. 修改状态码,找到响应体部分的状态码修改为200
> 写完这三步之后,还是显示404,我vscode虽然有自动保存功能,最后还是重启了一下问题就解决了
#### 路由
1. api中集中配置接口信息
2. router中配置路由
3. 在页面中调用接口
配置路由:
```js
{
path: '/system',
component: Layout,
meta: {
title: '系统管理',
icon: 'el-icon-s-tools'
},
alwaysShow: true,
children: [
{
path: 'sysRole',
component: () => import('@/views/system/sysRole/list'),
meta: {
title: '角色管理',
icon: 'el-icon-s-help'
}
}
]
},
```
定义接口:
这里说一下前面提到的,在那个get方法中输入的查询条件,后端是用普通对象接收的,所以这里就用`params`进行传递,如果使用的是`json`格式,那么就用`data: 对象名称`
```js
import request from '@/utils/request'
const api_name = '/admin/system/sysRole'
export default {
// 分页查询
getPageList(current, limit, searchObj) {
return request({
url: `${api_name}/${current}/${limit}`,
method: 'get',
params: searchObj
})
}
}
```
编辑页面:
引入js
```html
```
#### 接口实现
##### 分页查询
```js
// 初始值
data() {
return {
list: [], // 结果列表
page: 1, // 页号
limit: 10, // 每页条数
total: 0, // 总数
searchObj: {} // 条件对象
}
},
```
来看一下后端返回的对象
返回的是一个IPage对象(或子类)
```js
// 方法
methods: {
fetchData(current = 1) {
this.page = current // 根据所传参数更新页号
api.getPageList(this.page, this.limit, this.searchObj).then(res => {
this.list = res.data.records
this.total = res.data.total
})
}
}
```
这里就不用catch方法了,框架自带的拦截器已经拦截了失败方法
引入element-ui
##### 删除角色
点击删除按钮之后有一个确认窗口的弹窗,这里element-ui有进行提供
点击确认之后进入`then`方法:即调用删除接口
取消进入`catch`方法,这里取消没有操作则不用catch
```js
// 根据id删除数据
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // promise
// 点击确定,远程调用ajax
return api.removeById(id)
}).then((response) => {
this.fetchData(this.page)
this.$message.success(response.message || '删除成功')
})
}
```
##### 添加角色
要创建一个对象进行接收信息,引入element-ui对话框,用一个变量控制是否可见
默认为不可见,引入添加按钮(编辑按钮),点击事件为控制对话框的可见度
根据对话框确定按钮绑定的点击事件,编写一个方法
如果传入的对象没有id值,即为添加方法,调用save接口,有id值即调用修改接口
```js
// 添加或者修改
saveOrUpdate() {
this.saveBtnDisabled = true // 防止表单重复提交
if (!this.sysRole.id) {
this.save()
} else {
this.update()
}
},
save() {
api.save(this.sysRole).then(res => {
// 提示
this.$message.success(res.message || '操作成功')
// 关闭弹窗
this.dialogVisible = false
// 刷新页面
this.fetchData(this.page)
})
}
```
##### 修改角色
修改还是不一样的,要考虑怎么将id注入进来,不然的话就会一直调用添加方法
所以这里需要调用getById方法
对应的前端代码
1. 实现两个接口
2. 给edit按钮添加点击事件,调出弹窗,同时获得当前id对应的数据显示在对话框
这里是双向绑定,数据会自动回显
看到edit方法将当前的id传了过来
```vue
edit(id) {
this.dialogVisible = true
api.getById(id).then(res => {
this.sysRole = res.data
})
},
```
> ps: 拦截器已经把第一层data过滤了
3. 更新,注意 提示、关弹窗、刷新
```vue
update() {
api.updateById(this.sysRole).then(res => {
this.$message.success(res.message || '操作成功')
this.dialogVisible = false
this.fetchData(this.page)
})
}
```
##### 批量删除
添加批量删除的按钮,添加复选框组件
这里定义了一个数组`selections`保存获得的数据,在此基础上获得id数组
```js
handleSelectionChange(selection) {
this.selections = selection
},
batchRemove() {
if (this.selections.length === 0) {
this.$message.warning('请选择要删除的记录!')
return
}
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 获得idList
var idList = []
this.selections.forEach(item => idList.push(item.id))
// 调用接口
return api.batchRemove(idList)
}).then((response) => {
this.fetchData()
this.$message.success(response.message || '删除成功')
})
}
}
```
### 用户管理
用户和角色之间属于多对多,需要创建中间表
#### mp代码生成器
依赖,3.51之后的要不一样,我这里用的是[新版](https://baomidou.com/pages/981406/#%E6%A8%A1%E6%9D%BF%E9%85%8D%E7%BD%AE-templateconfig)
```xml
com.baomidou
mybatis-plus-generator
3.5.3.1
org.freemarker
freemarker
2.3.31
```
在test文件夹中创建类
```java
public class Generator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/co-oa?serverTimezone=GMT%2B8&useSSL=false", "root", "root")
// 全局配置
.globalConfig(builder -> {
builder.author("sy") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D:\\Java\\project\\oa-system\\oa-parent\\service-oa\\src\\main\\java"); // 指定输出目录
})
// 包配置
.packageConfig(builder -> {
builder.parent("com.sy") // 设置父包名
.moduleName("auth"); // 设置父包模块名
})
// 策略配置
.strategyConfig(builder -> {
builder.addInclude("sys_user")
.serviceBuilder()
.formatServiceFileName("%sService") // 去掉I
.formatServiceImplFileName("%sServiceImp");
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
```
删除多余的部分,修改类的包,修改路径
#### 接口实现
##### 基本CURD操作
分页查询、获得用户、保存用户、修改用户、删除用户(ID)、分配角色、修改状态
前五个仿照上面角色界面的写
```java
// 分页查询
@ApiOperation(value = "分页查询")
@GetMapping("{page}/{limit}")
public Result index(@PathVariable Long page, @PathVariable Long limit,
SysUserQueryVo sysUserQueryVo){
Page sysUserPage = new Page(page, limit);
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
String username = sysUserQueryVo.getKeyword();
String createTimeBegin = sysUserQueryVo.getCreateTimeBegin();
String createTimeEnd = sysUserQueryVo.getCreateTimeEnd();
// 设置查询条件
if(!StringUtils.isEmpty(username)){
wrapper.like(SysUser::getName, username);
}
if(!StringUtils.isEmpty(createTimeBegin)){
wrapper.ge(SysUser::getCreateTime, createTimeBegin);
}
if(!StringUtils.isEmpty(createTimeEnd)){
wrapper.le(SysUser::getCreateTime, createTimeEnd);
}
Page userPage = sysUserService.page(sysUserPage, wrapper);
return Result.ok(userPage);
}
```
这里有一个与创建时间做比较的查询条件
> StringUtils还是用的baomidou包下的
数据库中的创建时间要在传来参数的时间之内,就是说查询条件中有根据创建时间查询用户这条选项
> mp中的wapper.like模糊查询、wapper.ge大于、le小于
直接测试保存数据的时候,这里报了一个错误
直接测试service是没有问题的,根据提示是在JSON解析的时候出了问题,String不能转换成Data格式,查了一下,好像是关于时区的问题
去掉一些字段进行保存,测试成功
##### 给用户分配角色
涉及的操作
1. 显示所有角色 和 当前用户的角色
2. 点击确定后 为用户分配角色
删除用户原有的角色
添加新的角色
关系表和角色表
使用代码生成器,删掉多余的部分,Controller,entity等
把对应的的业务操作写在了SysRoleController.java中
1. 显示所有角色 和 当前用户的角色
这里使用一个map对象进行返回,一个map中有两条entry对象,key为String类型,value为List类型
即所有角色的list和当前角色的list
将操作的具体过程封装到`SysRoleService`中
```java
@ApiOperation("查询所有角色 和 当前用户所属角色")
@GetMapping("toAssign/{userId}")
public Result toAssign(@PathVariable Long userId){
Map map = sysUserRoleService.findRolesByUserId(userId);
return Result.ok(map);
}
```
下面编写`findRolesByUserId`方法
有两种方式实现
- 调用默认方法
- 直接在xml文件中编写一条SQL语句
使用默认方法:
查询所有角色
```java
List allRoles = baseMapper.selectList(null);// 查询所有参数是null
```
继承的父类已经将对应的代理注入,可以直接调用。也可以调用service(当前对象)的list方法
查询用户所属角色
这里可以偷个懒,通过用户关系表获得的id,直接和所有角色list中的对比
考验基础的时候到了

2. 点击确定后 为用户分配角色
所需要的条件有 用户id 和 对应角色id的列表
这里用一个对象进行封装
```java
@Service
public class SysRoleServiceImpl extends ServiceImpl implements SysRoleService {
@Autowired
SysUserRoleService sysUserRoleService;
public Map findRolesByUserId(Long userId){
// 查询所有角色
// List allRoles = baseMapper.selectList(null);
List allRoles = this.list();
// 当前用户所属的角色
List userRoleList = sysUserRoleService.list(new LambdaQueryWrapper().eq(SysUserRole::getUserId, userId));
List userRoleId = userRoleList.stream().map(sysUserRole -> sysUserRole.getRoleId()).collect(Collectors.toList());
List roles = allRoles.stream().filter(s -> userRoleId.contains(s.getId())).collect(Collectors.toList());
// 返回结果
Map map = new HashMap<>();
map.put("allRoles", allRoles);
map.put("roles", roles);
return map;
}
public void doAssign(AssginRoleVo assginRoleVo){
// 删除已有的角色 根据条件删除
sysUserRoleService.remove(new LambdaQueryWrapper().eq(SysUserRole::getUserId,
assginRoleVo.getUserId()));
// 添加新的角色
for(Long roleId : assginRoleVo.getRoleIdList()){
if(StringUtils.isEmpty(roleId)) continue;
SysUserRole sysUserRole = new SysUserRole();
sysUserRole.setUserId(assginRoleVo.getUserId());
sysUserRole.setRoleId(roleId);
sysUserRoleService.save(sysUserRole);
}
}
```
设置用户状态
1表示开启,0表示停用,即用户不能再登录
#### 前端
1. 添加路由
2. 定义接口
3. 编写页面调用接口
调用接口的时候注意接口名称是否与js文件中的一致
这里遇到前端数据渲染不出来的问题
解决方法
注意要用`@RequestBody`接收post的对象
### 菜单管理
菜单之间有层级关系(相当于课程号对应的先行课)sys_menu
每种角色有不同的菜单(对应微信公众号里的菜单)sys_role_menu
每个菜单也可以对应多个角色
下面代码生成器生成将两个表
这里将SysRoleMenuController删除
#### curd接口实现
- 获取菜单列表
- 新增菜单
- 修改菜单
- 删除菜单
##### 菜单列表
菜单之间的关系为一个树形结构,使用递归
返回对象的时候往children里加入对象
controller:
```java
// 菜单的列表接口
@ApiOperation(value = "菜单列表")
@GetMapping("findNodes")
public Result findNodes(){
List list = sysMenuService.findNodes();
return Result.ok(list);
}
```
serviceImpl
```java
@Override
public List findNodes() {
// 1. 查询所有菜单的数据
List sysMenuList = this.list();
// 2. 构建树形结构 使用工具类menuHelper
List resultList = MenuHelper.buildTree(sysMenuList);
return resultList;
}
```
MenuHelper工具类
```java
public class MenuHelper {
public static List buildTree(List sysMenuList) {
// 结果
List trees = new ArrayList<>();
// 遍历列表,对第一层的菜单元素进行递归 parentId = 0
for (SysMenu sysMenu : sysMenuList){
if(sysMenu.getParentId().longValue() == 0){
trees.add(getChildren(sysMenu, sysMenuList));
}
}
return trees;
}
private static SysMenu getChildren(SysMenu sysMenu, List sysMenuList) {
List children = new ArrayList<>();
// 当前节点的id = 下一层节点的parentId
for (SysMenu menu : sysMenuList){
if(menu.getParentId().longValue() == sysMenu.getId()){
children.add(getChildren(menu, sysMenuList));
}
}
sysMenu.setChildren(children);
return sysMenu;
}
}
```
##### 删除
要求有子集菜单的目录不能直接删除(非级联删除)
serviceImpl
```java
@Override
public void removeMenuById(Long id) {
/**
* 判断当前菜单是否有子菜单,有则不能删除
* 只需在表中查找 是否存在parentId = id的元素
*/
Long count = baseMapper.selectCount(new LambdaQueryWrapper().eq(SysMenu::getParentId, id));
if (count > 0L) {
throw new CustomException(201, "不能删除当前菜单");
}
baseMapper.deleteById(id);
}
```
#### 为角色分配菜单
过程跟给用户分配角色是差不多的
在角色界面添加分配按钮
点击分配后,显示当前角色
- 获得菜单列表 和 已分配的菜单
- 分配菜单(删除原有的和添加新的)
需要一个中间表sys_role_menu
查询已经分配的菜单
```java
@Override
public List findSysMenuByRoleId(Long roleId) {
//筛选状态为1的列表 1代表可用
List allSysMenuList = this.list(new LambdaQueryWrapper().eq(SysMenu::getStatus, 1));
//根据角色id获得关系表里的元素
List sysRoleMenuList = sysRoleMenuMapper.selectList(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, roleId));
//根据获得的元素获得相应的菜单id
List menuIdList = sysRoleMenuList.stream().map(e -> e.getMenuId()).collect(Collectors.toList());
// 判断当前菜单是否已添加
allSysMenuList.forEach(permission -> {
if (menuIdList.contains(permission.getId())) {
permission.setSelect(true);
} else {
permission.setSelect(false);
}
});
List sysMenuList = MenuHelper.buildTree(allSysMenuList);
return sysMenuList;
}
```
为角色分配菜单
- 根据角色id删除原有的菜单
- 获得新分配的菜单列表进行遍历添加
#### 前端
1. 添加分配的隐藏路由
router的index.js中
2. 在角色页面添加跳转按钮
跳转方法
3. 配置接口
4. 书写界面
### 权限管理
不同用户有不同角色,不同角色有不同的权限,不同的权限显示不同的菜单(可操作的功能不一样)
所以前端的菜单实际上是动态的
实现接口:
- 用户登录
- 获得可操作菜单
用户登录
1. 根据用户名,查询用户是否存在
2. 是否被禁用
3. 登录成功之后保持登录状态
**基于token实现**
把登录信息加密(生成一个唯一的字符串)塞到cookie中
cookie不能跨域传递,所以把cookie的值放到request头部
> **jwt**,JSON Web Token,一个工具,生成唯一的字符串,并对其进行加密处理,有三部分组成:jwt头部(算法,类型)、有效载荷(携带的信息)、签名哈希(根据签名的内容计算出来的唯一的字符串,保证信息不被篡改)
获得可操作菜单
1. 从token中获得用户id
2. 根据用户id获得可操作菜单
jwt配置
在common-util中引入依赖jwt依赖,创建工具类
```xml
io.jsonwebtoken
jjwt
```
工具类
#### 实现登录接口
> 之前添加用户的时候,输入的密码也需要进行加密。这里使用MD5的加密方式。
>
> MD5加密是不可逆的。登录时判断密码是否正确,就是对登录的密码进行一次MD5加密,比较跟数据库中的是否一致
```java
@PostMapping("login")
public Result login(@RequestBody LoginVo loginVo){
// 1. 根据用户名查询数据库
String username = loginVo.getUsername();
SysUser user = sysUserService.getOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username));
// 2. 判断用户是否存在
if (user == null) {
throw new CustomException(201, "用户不存在");
}
// 3. 验证密码
String pws_db = user.getPassword();
String psw = MD5.encrypt(loginVo.getPassword());
if (!pws_db.equals(psw)) {
throw new CustomException(201, "密码错误");
}
// 4. 用户是否被禁用
if (user.getStatus().intValue() == 0) {
throw new CustomException(201, "用户已经被禁用,请联系管理员");
}
// 5. 生成token
String token = JwtHelper.createToken(user.getId(), user.getUsername());
// 6. 返回token
Map map = new HashMap<>();
map.put("token", token);
return Result.ok(map);
}
```
#### 获得可操作的菜单和按钮
菜单表中记录了菜单和按钮,type字段,0表示根菜单,1表示菜单,2表示按钮
##### Controller
```java
@GetMapping("info")
public Result info(HttpServletRequest request){
// 1. 从请求头中获取token
String token = request.getHeader("token");
// 2. 从token中获得用户id或者名称,把用户信息查出来
Long userId= JwtHelper.getUserId(token);
SysUser sysUser = sysUserService.getById(userId);
// 3. 根据用户Id获得可操作菜单
List routerList = sysMenuService.findUserMenuListByUserId(userId);
// 4. 根据用户Id获得可操作按钮
List permsList = sysMenuService.findUserPermsByUserId(userId);
// 6. 返回相关数据
Map map = new HashMap<>();
map.put("roles", "[admin]");
map.put("name", sysUser.getName());
map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
map.put("routers", routerList);
map.put("buttons", permsList);
return Result.ok(map);
}
```
##### service实现
1. 根据用户id获得用户可以操作菜单列表
这里涉及多表关联,需要自己写SQL语句
用户id ——> 角色id——>菜单列表
```java
1. 判断角色是不是管理员,管理员能获得所有菜单,用户要根据角色筛选菜单
2. 根据返回的菜单 封装成前端路由对象的形式
3. 返回结果列表
```
```java
@Override
public List findUserMenuListByUserId(Long userId) {
List sysMenusList = null;
// 1. 判断是不是管理员 userId = 1,是则返回所有列表
if (userId.longValue() == 1) {
sysMenusList = baseMapper.selectList(new LambdaQueryWrapper()
.eq(SysMenu::getStatus, 1).orderByAsc(SysMenu::getSortValue));
} else {
sysMenusList = baseMapper.findMenuListByUserId(userId);
}
// 2. 把查询出来的数据封装成前端路由的结构
List sysMenuTreeList = MenuHelper.buildTree(sysMenusList);
List routerList = this.buildRouter(sysMenuTreeList);
return routerList;
}
```
###### 封装RouterVo
RouterVo的属性
```java
@Data
public class RouterVo
{
private String path;
private boolean hidden;
private String component;
private Boolean alwaysShow;
private MetaVo meta;
private List children;
}
```
对应的前端路由对象
下面对封装过程进行书写,依然使用了递归
脑子不够,想了很多遍,记录一下
```java
private List buildRouter(List menus){
1. 创建数组,收集结果
2. 遍历菜单树列表
新建一个routerVo封装当前菜单的信息
// 添加隐藏路由
如果当前菜单对象的type=1的话,那么在它的children中可能会存在隐藏路由,这个路由跟当前的菜单对象是平级的。判断component是否为空筛选,将隐藏hidden的值改为true。
ps:一个menu对应一个routerVo,只是说如果menu里包含隐藏路由的话就多加一个routerVo
// 递归下一层
如果是一个根目录(存在子菜单)setAlwaysShow(true)
}
```
```java
private List buildRouter(List menus) {
List routers = new ArrayList<>();
for (SysMenu menu : menus) {
RouterVo router = new RouterVo();
router.setHidden(false);
router.setAlwaysShow(false);
router.setPath(getRouterPath(menu));
router.setComponent(menu.getComponent());
router.setMeta(new MetaVo(menu.getName(), menu.getIcon()));
List children = menu.getChildren();
// 添加隐藏路由
if(menu.getType().intValue() == 1) {
// 挑出来
List hiddenMenuList = children.stream()
.filter(item -> !StringUtils.isEmpty(item.getComponent()))
.collect(Collectors.toList());
// 修改hidden值
for (SysMenu hiddenMenu : hiddenMenuList) {
RouterVo hiddenRouter = new RouterVo();
hiddenRouter.setHidden(true);
hiddenRouter.setAlwaysShow(false);
hiddenRouter.setPath(getRouterPath(hiddenMenu));
hiddenRouter.setComponent(hiddenMenu.getComponent());
hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
// 收集
routers.add(hiddenRouter);
}
} else {
if (!CollectionUtils.isEmpty(children)) {
if(children.size() > 0){
router.setAlwaysShow(true);
}
router.setChildren(buildRouter(children));
}
}
routers.add(router);
}
return routers;
}
```
获得可操作的按钮就不写了,那个很容易理解
###### 实现多表查询
编写xml文件

前面传入的参数加上`@Param("属性名")`,一个参数可以不加
userId ——> userId,roleId——>roleId,menuId
这里要获得的是菜单列表
测试的SQL语句
```sql
SELECT DISTINCT *
FROM sys_menu m
INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
WHERE ur.user_id = 2
AND m.status = 1
AND rm.is_deleted = 0
AND ur.is_deleted = 0
AND m.is_deleted = 0
```
xml中
```xml
m.id,m.parent_id,m.name,m.type,m.path,m.component,m.perms,m.icon,m.sort_value,m.status,m.create_time,m.update_time,m.is_deleted
```
之前写mapperXml的时候有时候放在了resources里面,这里没有,maven默认情况下,只会加载编译src-main-java目录里面的java类型的文件
解决方法:
1. 把xml文件放到resources目录下面
2. 通过配置方式进行加载
在pom.xml和配置文件中添加配置
让maven加载文件类型
配置文件中的mp部分写上xml的路径
一定要记住接口名别写错
#### Spring Security
服务端权限控制,包括认证和授权
引入
1. 在公共模块中新建模块,引入spring-security相关依赖
2. 在新模块中创建一个配置类,开启默认功能
3. 在业务模块中引入新模块
```java
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
```
```xml
com.sy
common-util
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
provided
```
测试默认功能
##### 认证授权分析
SpringSecurity的默认功能,即每访问一个接口都要输入用户名和密码,针对本项目,设置自己的拦截方式
客户端发送请求访问接口
1. 认证
- 当前用户是否合法(跟登录接口功能部分重叠)
- 当前用户是否处于登录状态,即请求头中是否包含token
2. 授权
- 当前用户是否具有访问该接口的权限
时序图
用户提交信息,先进入`UserPasswordAuthenticationFilter`这个过滤器,该过滤器将请求中的信息封装成一个对象
先看默认是怎么处理的
双击shift进入源码查看
这里进行登录认证处理,注意源码中是从request中获得的用户名和密码(通过`getParameter`的方式),将用户信息封装成一个对象
直接看到后面的具体实现
进入`retrieveUser()`中,调用`loadUserByUsername()`根据用户名称查询,返回一个**查询对象**
进入`additionalAuthenticationChecks()`中,看到使用了`passwordEncoder`对封装的用户密码和返回的查询结果的密码进行校验。
debug就不写了,默认的流程大概就是这样
由上面的分析大概可以知道我们需要自定义哪些地方
##### 登录认证
1. SpringSecurity默认是从请求中直接获得`username`的
所以自定义一个类`TokenLoginFilter`,继承自`UsernamePasswordAuthenticationFilter`
重写`attemptAuthentication`方法
```java
// 登录认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
// 1. 通过输入流获得信息
LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);
// 2. 封装成一个对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
// 3. 调用方法完成认证
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
```
2. 根据用户名查询用户信息肯定是需要自己写的
SpringSecurity默认又不知道查我们的数据库,查询返回的用户信息也需要定义一个类来封装(UserDetails)
查询方法
新建一个接口,继承自`org.springframework.security.core.userdetails.UserDetailsService`
将实现类`UserDetailsServiceImpl`写到service实现类的包中,也可以让实现类直接实现springsecurity的`UserDetailsService`接口
重写方法`loadUserByUsername`
判断用户是否存在,判断用户是否被停用
**将查询到的结果封装到自定义的用户类中(CustomUser)**
```java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Autowired
SysMenuService sysMenuService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getUserByUserName(username);
if(null == sysUser) {
throw new UsernameNotFoundException("用户名不存在!");
}
if(sysUser.getStatus().intValue() == 0) {
throw new CustomException(ResultCodeEnum.ACCOUNT_STOP);
}
List userPermsList = sysMenuService.findUserPermsByUserId(sysUser.getId());
List authorities = new ArrayList<>();
for (String perm : userPermsList) {
authorities.add(new SimpleGrantedAuthority(perm.trim()));
}
return new CustomUser(sysUser, authorities);
}
}
```
使用了自定义异常,新建枚举`ACCOUNT_STOP`
这里注意接收权限列表的类型`SimpleGrantedAuthority`
3. 校验密码
自定义一个密码校验器,实现`PasswordEncoder`接口,使用MD5的方法进行校验

4. 登录认证
进行以上操作后,把返回的UserDetails对象,还有密码校验的结果封装到authentication对象中返回到`TokenLoginFilter`
认证成功后,生成`token`
并把返回的用户权限保存到缓存中,这里使用redis做缓存,key:用户名,value:权限列表
配置一下redis
> redis连接不上有几个点,配置文件修改保护模式,bind本机
>
> linux端口6379的防火墙关闭
>
> 有密码springboot配置文件中写密码
```yml
redis:
host: 192.168.235.128
port: 6379
database: 0
timeout: 1800000
password: 111111
jedis:
pool:
max-active: 20 #最大连接数
max-wait: -1 #最大阻塞等待时间(负数表示没限制)
max-idle: 5 #最大空闲
min-idle: 0 #最小空闲
```
在`TokenLoginFilter`类中继续重写两个方法
添加构造方法,将redis传进来
```java
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登录接口及提交方式,可以指定任意路径
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
this.redisTemplate = redisTemplate;
}
```
```java
/**
* 登录成功
* @param request
* @param response
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
CustomUser customUser = (CustomUser) auth.getPrincipal();
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
//保存权限数据
redisTemplate.opsForValue().set(customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities()));
Map map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if(e.getCause() instanceof RuntimeException) {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.SERVICE_ERROR));
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
}
}
```
因为不是在RestController中,所以使用原生方式进行返回
使用工具类ResponseUtil
```java
public class ResponseUtil {
public static void out(HttpServletResponse response, Result r) {
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
mapper.writeValue(response.getWriter(), r);
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
##### 解析token
我们已经将当前用户的权限加到缓存中了,下面要进行判断,当前用户的请求中是否包含token,并对token中的用户名进行校验
如果用户名校验成功,我们将缓存中的权限列表拿出来一起封装成一个authentication对象,并将其放到上下文中
校验失败返回null
```java
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("uri:"+request.getRequestURI());
//如果是登录接口,直接放行
if("/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
}
}
// 自定义解析方法
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// 从请求中取出token
String token = request.getHeader("token");
logger.info("token:"+token);
// 判断是否有token
if (!StringUtils.isEmpty(token)) {
String username = JwtHelper.getUsername(token);
logger.info("useruame:"+username);
// 校验token
if (!StringUtils.isEmpty(username)) {
// 从缓存中获得权限列表
// 转换类型
String authoritiesString = (String) redisTemplate.opsForValue().get(useruame);
List