项目参考文档地址:https://www.zhuawaba.com/post/19#1. 前言 项目参考视频地址:https://www.bilibili.com/video/BV1af4y1s7Wh?p=54
一,新建springBoot项目
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.huihui</groupId>
<artifactId>vueadmin-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>vueadmin-api</name>
<description>vueadmin后台api</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
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-generator依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.huihui</groupId>
<artifactId>vueadmin-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>vueadmin-api</name>
<description>vueadmin后台api</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.8</version>
</dependency>
<!-- <dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>-->
<!--整合mybatis plus https://baomidou.com/-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
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
@MapperScan("com.huihui.vueadminapi.mapper")
新增模板生成映射文件前记得在数据库字段类型映射位置(File->Settings-->Other Settings-->EasyCode-->Type Mapper)位置增加tinyint的对应关系:tinyint((\d+))?
##引入宏定义
$!{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
}
##定义初始变量
#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);
}
##引入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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="$!{tableInfo.savePackageName}.mapper.$!{tableInfo.name}Mapper">
<resultMap type="$!{tableInfo.savePackageName}.entity.$!{tableInfo.name}" id="$!{tableInfo.name}Map">
#foreach($column in $tableInfo.fullColumn)
<result property="$!column.name" column="$!column.obj.name" jdbcType="$!column.ext.jdbcType"/>
#end
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="$!{tableInfo.name}Map">
select
#allSqlColumn()
from $!{tableInfo.obj.parent.name}.$!tableInfo.obj.name
where $!pk.obj.name = #{$!pk.name}
</select>
<!--查询指定行数据-->
<select id="queryAllByLimit" resultMap="$!{tableInfo.name}Map">
select
#allSqlColumn()
from $!{tableInfo.obj.parent.name}.$!tableInfo.obj.name
limit #{offset}, #{limit}
</select>
<!--通过实体作为筛选条件查询-->
<select id="queryAll" resultMap="$!{tableInfo.name}Map">
select
#allSqlColumn()
from $!{tableInfo.obj.parent.name}.$!tableInfo.obj.name
<where>
#foreach($column in $tableInfo.fullColumn)
<if test="$!column.name != null#if($column.type.equals("java.lang.String")) and $!column.name != ''#end">
and $!column.obj.name = #{$!column.name}
</if>
#end
</where>
</select>
<!--新增所有列-->
<insert id="insert" keyProperty="$!pk.name" useGeneratedKeys="true">
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)
</insert>
<!--通过主键修改数据-->
<update id="update">
update $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name}
<set>
#foreach($column in $tableInfo.otherColumn)
<if test="$!column.name != null#if($column.type.equals("java.lang.String")) and $!column.name != ''#end">
$!column.obj.name = #{$!column.name},
</if>
#end
</set>
where $!pk.obj.name = #{$!pk.name}
</update>
<!--通过主键删除-->
<delete id="deleteById">
delete from $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name} where $!pk.obj.name = #{$!pk.name}
</delete>
</mapper>
##定义初始变量
#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;
}
}
##定义初始变量
#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);
}
}
新增模板生成映射文件前记得在数据库字段类型映射位置(File->Settings-->Other Settings-->EasyCode-->Type Mapper)位置增加tinyint的对应关系:tinyint((\d+))?
##导入宏定义
$!{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
}
##定义初始变量
#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}>{
}
##定义初始变量
#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}>{
}
##导入宏定义
$!{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 {
}
##定义初始变量
#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;
}
##引入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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="$!{tableInfo.savePackageName}.mapper.$!{tableInfo.name}Mapper">
<resultMap type="$!{tableInfo.savePackageName}.entity.$!{tableInfo.name}" id="$!{tableInfo.name}Map">
#foreach($column in $tableInfo.fullColumn)
<result property="$!column.name" column="$!column.obj.name" jdbcType="$!column.ext.jdbcType"/>
#end
</resultMap>
</mapper>
因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。
这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的
所以可得到封装如下:
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来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。
步骤二、定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。
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());
}
}
上面我们捕捉了几个异常:
很多人不懂spring security,觉得这个框架比shiro要难,的确,security更加复杂一点,同时功能也更加强大,我们首先来看一下security的原理,这里我们引用一张来自江南一点雨大佬画的一张原理图(https://blog.csdn.net/u012702547/article/details/89629415):
(引自江南一点雨的博客)
上面这张图一定要好好看,特别清晰,毕竟security是责任链的设计模式,是一堆过滤器链的组合,如果对于这个流程都不清楚,那么你就谈不上理解security。那么针对我们现在的这个系统,我们可以自己设计一个security的认证方案,结合江南一点雨大佬的博客,我们得到这样一套流程:
https://www.processon.com/view/link/606b0b5307912932d09adcb3?fileGuid=OnZDwoxFFL8bnP1c
流程说明:
Spring Security 实战干货:必须掌握的一些内置 Filter:https://blog.csdn.net/qq_35067322/article/details/102690579
ok,上面我们说的流程中涉及到几个组件,有些是我们需要根据实际情况来重写的。因为我们是使用json数据进行前后端数据交互,并且我们返回结果也是特定封装的。我们先再总结一下我们需要了解的几个组件:
有了上面的组件,那么认证与授权两个问题我们就已经接近啦,我们现在需要做的就是去重写我们的一些关键类。
首先我们导入security包,因为我们前后端交互用户凭证用的是JWT,所以我们也导入jwt的相关包,然后因为验证码的存储需要用到redis,所以引入redis。最后为了一些工具类,我们引入hutool。
<!-- springboot security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
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/login,因为security内置了登录页,用户名为user,密码在启动项目的时候打印在了控制台。登录完成之后我们才可以正常访问接口。 因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码:
application.yml
spring:
security:
user:
name: user
password: 111111
redis:
database: 0
host: 192.168.101.54
port: 6379
password: 123456
清空浏览器的Cookie再次访问http://localhost:8081/test 使用 配置的账号密码登录即可正常访问并返回结果。
首先我们来解决用户认证问题,分为首次登陆,和二次认证。
使用用户名密码来登录的,然后我们还想添加图片验证码,那么security给我们提供的UsernamePasswordAuthenticationFilter能使用吗?
首先security的所有过滤器都是没有图片验证码这回事的,看起来不适用了。其实这里我们可以灵活点,如果你依然想沿用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。
我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。
pom中上述引入的redis的start
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
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<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//================有序集合 sort set===================
/**
* 有序set添加元素
*
* @param key
* @param value
* @param score
* @return
*/
public boolean zSet(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
return redisTemplate.opsForZSet().add(key, typles);
}
public void zIncrementScore(String key, Object value, long delta) {
redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
}
/**
* 获取zset数量
* @param key
* @param value
* @return
*/
public long getZsetScore(String key, Object value) {
Double score = redisTemplate.opsForZSet().score(key, value);
if(score==null){
return 0;
}else{
return score.longValue();
}
}
/**
* 获取有序集 key 中成员 member 的排名 。
* 其中有序集成员按 score 值递减 (从大到小) 排序。
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}
}
RedisConfig-序列化配置
package com.huihui.vueadminapi.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
首先我们来解决用户认证问题,分为首次登陆,和二次认证。
使用用户名密码来登录的,然后我们还想添加图片验证码,那么security给我们提供的UsernamePasswordAuthenticationFilter能使用吗?
首先security的所有过滤器都是没有图片验证码这回事的,看起来不适用了。其实这里我们可以灵活点,如果你依然想沿用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。
我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。
首先我们先生成验证码,之前我们已经引用了google的验证码生成器,我们先来配置一下图片验证码的生成规则:
/**
* @author liyonghui
* @description 验证码配置类
* @date 2022/4/25 10:20
*/
@Configuration
public class KaptchaConfig {
@Bean
DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "4");
properties.put("kaptcha.image.height", "40");
properties.put("kaptcha.image.width", "120");
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
上面我定义了图片验证码的长宽字体颜色等,自己可以调整哈。 然后我们通过控制器提供生成验证码的方法:
package com.huihui.vueadminapi.controller;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import com.google.code.kaptcha.Producer;
import com.huihui.vueadminapi.common.lang.Const;
import com.huihui.vueadminapi.common.lang.Result;
import com.huihui.vueadminapi.entity.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Principal;
/**
* @author liyonghui
* @description 权限验证接口
* @date 2022/4/25 10:21
*/
@RestController
public class AuthController extends BaseController {
@Autowired
Producer producer;
/**
* @return com.huihui.vueadminapi.common.lang.Result
* @description: 生成验证码
* @author: liyonghui
* @date 2022/4/25 10:21
*/
@GetMapping("/captcha")
public Result captcha() throws IOException {
String key = UUID.randomUUID().toString();
String code = producer.createText();
BufferedImage image = producer.createImage(code);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
BASE64Encoder encoder = new BASE64Encoder();
String str = "data:image/jpeg;base64,";
String base64Img = str + encoder.encode(outputStream.toByteArray());
redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120);
return Result.succ(
MapUtil.builder()
.put("token", key)
.put("captchaImg", base64Img)
.build()
);
}
}
因为前后端分离,我们禁用了session,所以我们把验证码放在了redis中,使用一个随机字符串作为key,并传送到前端,前端再把随机字符串和用户输入的验证码提交上来,这样我们就可以通过随机字符串获取到保存的验证码和用户的验证码进行比较了是否正确了。 然后因为图片验证码的方式,所以我们进行了encode,把图片进行了base64编码,这样前端就可以显示图片了。
而前端的处理,我们之前是使用了mockjs进行随机生成数据的,现在后端有接口之后,1,我们只需要在main.js中去掉mockjs的引入即可,2, axios.js文件的顶部 axios.defaults.baseURL写后台的访问地址,如:"http://localhost:8081"这样前端就可以访问后端的接口而不被mock拦截了。
注意:若此时启动前台访问登录页面,则登录页面的二维码无法呈现,原因是跨域问题。解决办法如下
com.huihui.vueadminapi.config.CorsConfig
package com.huihui.vueadminapi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author liyonghui
* @description 跨域配置类
* @date 2022/4/25 10:30
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
// .allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
package com.huihui.vueadminapi.config;
import com.huihui.vueadminapi.security.CaptchaFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String[] URL_WHITELIST = {
"/webjars/**",
"/favicon.ico",
"/captcha",
"/login",
"/logout",
};
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
//登录配置
.formLogin()
// .failureHandler(loginFailureHandler)
// .successHandler(loginSuccessHandler)
//禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() //白名单
.anyRequest().authenticated()
//异常处理器
//配置自定义的过滤器
;
}
}
重启服务,访问http://localhost:8081/login 验证码展示正常。
然后认证失败的话,我们之前说过,登录失败的时候交给AuthenticationFailureHandler,所以我们自定义了
package com.huihui.vueadminapi.security;
import cn.hutool.json.JSONUtil;
import com.huihui.vueadminapi.common.lang.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail(exception.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
其实主要就是获取异常的消息,然后封装到Result,最后转成json返回给前端而已哈。
package com.huihui.vueadminapi.security;
import cn.hutool.json.JSONUtil;
import com.huihui.vueadminapi.common.lang.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
把刚刚新增的loginFailureHandler注入进去。
修改后的效果:
package com.huihui.vueadminapi.config;
import com.huihui.vueadminapi.security.CaptchaFilter;
import com.huihui.vueadminapi.security.LoginFailureHandler;
import com.huihui.vueadminapi.security.LoginSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
public static final String[] URL_WHITELIST = {
"/webjars/**",
"/favicon.ico",
"/captcha",
"/login",
"/logout",
};
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
//登录配置
.formLogin()
.failureHandler(loginFailureHandler)
.successHandler(loginSuccessHandler)
//禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() //白名单
.anyRequest().authenticated()
//异常处理器
//配置自定义的过滤器
;
}
}
访问地址:http://localhost:8081/login 发现,提示账号密码错误,并没有先验证验证码而是直接验证了账号密码。故需要新增一个验证码的filter,实现先验证验证码 通过后再验证账号密码。
package com.huihui.vueadminapi.security;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.huihui.vueadminapi.common.exception.CaptchaException;
import com.huihui.vueadminapi.common.lang.Const;
import com.huihui.vueadminapi.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author liyonghui
* @description 图片验证码过滤器
* @date 2022/4/25 11:00
*/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
RedisUtil redisUtil;
@Autowired
LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) {
try {
// 校验验证码
validate(httpServletRequest);
} catch (CaptchaException e) {
// 交给认证失败处理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
// 校验验证码逻辑
private void validate(HttpServletRequest httpServletRequest) {
String code = httpServletRequest.getParameter("code");
String key = httpServletRequest.getParameter("token");
if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
throw new CaptchaException("验证码错误");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {
throw new CaptchaException("验证码错误");
}
// 一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY, key);
}
}
上面代码中,因为验证码需要存储,所以添加了RedisUtil工具类,这个工具类代码我们就不贴出来了。
然后验证码出错的时候我们返回异常信息,这是一个认证异常,所以我们自定了一个CaptchaException:
package com.huihui.vueadminapi.common.exception;
import org.springframework.security.core.AuthenticationException;
public class CaptchaException extends AuthenticationException {
public CaptchaException(String msg) {
super(msg);
}
}
package com.huihui.vueadminapi.common.lang;
public class Const {
public final static String CAPTCHA_KEY = "captcha";
public final static Integer STATUS_ON = 0;
public final static Integer STATUS_OFF = 1;
public static final String DEFULT_PASSWORD = "888888";
public static final String DEFULT_AVATAR = "https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg";
}
为使图片验证码过滤器生效,需要在SecurityConfig添加上此过滤器,修改后效果:
package com.huihui.vueadminapi.config;
import com.huihui.vueadminapi.security.CaptchaFilter;
import com.huihui.vueadminapi.security.LoginFailureHandler;
import com.huihui.vueadminapi.security.LoginSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CaptchaFilter captchaFilter;
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
public static final String[] URL_WHITELIST = {
"/webjars/**",
"/favicon.ico",
"/captcha",
"/login",
"/logout",
};
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
//登录配置
.formLogin()
.failureHandler(loginFailureHandler)
.successHandler(loginSuccessHandler)
//禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() //白名单
.anyRequest().authenticated()
//异常处理器
//配置自定义的过滤器
.and()
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
;
}
}
再CaptchaFilter的validate方法处打个断点。
输入个错误的验证码,页面返回的是:验证码错误。
输入个正确的验证码,页面返回的是:账号密码错误。
登录成功之后前端就可以获取到了jwt的信息,前端中我们是保存在了store中,同时也保存在了localStorage中,然后每次axios请求之前,我们都会添加上我们的请求头信息,可以回顾一下:
所以后端进行用户身份识别的时候,我们需要通过请求头中获取jwt,然后解析出我们的用户名,这样我们就可以知道是谁在访问我们的接口啦,然后判断用户是否有权限等操作。
那么我们自定义一个过滤器用来进行识别jwt。
package com.huihui.vueadminapi.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author liyonghui
* @description jwt工具类
* @date 2022/4/25 11:47
*/
@Data
@Component
@ConfigurationProperties(prefix = "markerhub.jwt")
public class JwtUtils {
private long expire;
private String secret;
private String header;
// 生成jwt
public String generateToken(String username) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)// 7天過期
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 解析jwt
public Claims getClaimByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
// jwt是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
application.yml新增配置
markerhub:
jwt:
# 加密秘钥
secret: f4e2e52034348f86b67cde581c0f9eb5
# token有效时长,7天,单位秒
expire: 604800
header: Authorization
package com.huihui.vueadminapi.security;
import cn.hutool.json.JSONUtil;
import com.huihui.vueadminapi.common.lang.Result;
import com.huihui.vueadminapi.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
// 生成jwt,并放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
response.setHeader(jwtUtils.getHeader(), jwt);
Result result = Result.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
测试前把 AuthController里面的captcha最顶部,设置 key和code
key="112233";
code="123456";
http://localhost:8081/login?username=user&password=111111&code=123456&token=112233
发送请求前获取验证码
Postman的Pre-request Script ---> Send a request
package com.huihui.vueadminapi.security;
import cn.hutool.core.util.StrUtil;
import com.huihui.vueadminapi.entity.SysUser;
import com.huihui.vueadminapi.service.SysUserService;
import com.huihui.vueadminapi.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author liyonghui
* @description JWT验证
* @date 2022/4/25 14:19
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
SysUserService sysUserService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtils.getHeader());
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(username, null, null);
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}
}
增加JwtAuthenticationFilter的支持
package com.huihui.vueadminapi.config;
import com.huihui.vueadminapi.security.CaptchaFilter;
import com.huihui.vueadminapi.security.JwtAuthenticationFilter;
import com.huihui.vueadminapi.security.LoginFailureHandler;
import com.huihui.vueadminapi.security.LoginSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CaptchaFilter captchaFilter;
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
//增加的内容--1
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
public static final String[] URL_WHITELIST = {
"/webjars/**",
"/favicon.ico",
"/captcha",
"/login",
"/logout",
};
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
//登录配置
.formLogin()
.failureHandler(loginFailureHandler)
.successHandler(loginSuccessHandler)
//禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() //白名单
.anyRequest().authenticated()
//异常处理器
//配置自定义的过滤器
.and()
//增加的内容--2
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
;
}
}
重启后台后用postman访问地址:http://localhost:8081/test 发现返回的是登录页面,原因是header里面没有==Authorization==
重复步骤“postman做测试验证”步骤可以获取==Authorization== ,然后在header里面加上==Authorization== 即可成功访问资源。
package com.huihui.vueadminapi.security;
import cn.hutool.json.JSONUtil;
import com.huihui.vueadminapi.common.lang.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @description: 认证失败
* @author: liyonghui
* @date: 2022/4/25 14:45
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail("请先登录");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
package com.huihui.vueadminapi.security;
import cn.hutool.json.JSONUtil;
import com.huihui.vueadminapi.common.lang.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @description: 权限不足异常处理类
* @author: liyonghui
* @date: 2022/4/25 14:46
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail(accessDeniedException.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
//异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
4,重启后台验证
postman访问地址:http://localhost:8081/test 把==Authorization== 去掉或者值随便修改个值,此时返回的内容不再是登录页面,
【权限不足的测试后续用到再做测试,本次测试是“认证失败”的验证】而是:
{
"msg": "请先登录",
"code": 400
}
上述的例子登录的账号密码在配置文件中,本例要改成数据库登录。
package com.huihui.vueadminapi.security;
import com.huihui.vueadminapi.entity.SysUser;
import com.huihui.vueadminapi.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @description: 用户实现类
* @author: liyonghui
* @date: 2022/4/25 15:46
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUserName(userName);
if (sysUser == null) {
throw new UsernameNotFoundException("用户名或密码不正确");
}
return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));
}
/**
* 获取用户权限信息(角色、菜单权限)
* @param userId
* @return
*/
public List<GrantedAuthority> getUserAuthority(Long userId){
return null;
}
}
package com.huihui.vueadminapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.huihui.vueadminapi.entity.SysUser;
/**
* 用户表(SysUser)表服务接口层
*
* @author liyonghui
* @since 2022-04-14 14:09:17
*/
public interface SysUserService extends IService<SysUser> {
SysUser getByUserName(String userName);
}
SysUserServiceImpl
package com.huihui.vueadminapi.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.huihui.vueadminapi.mapper.SysUserMapper;
import com.huihui.vueadminapi.entity.SysUser;
import com.huihui.vueadminapi.service.SysMenuService;
import com.huihui.vueadminapi.service.SysRoleService;
import com.huihui.vueadminapi.service.SysUserService;
import com.huihui.vueadminapi.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 用户表(SysUser)表服务实现类
*
* @author liyonghui
* @since 2022-04-14 14:09:17
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Override
public SysUser getByUserName(String userName) {
return getOne(new QueryWrapper<SysUser>().eq("username", userName));
}
}
package com.huihui.vueadminapi.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
import java.util.Collection;
/**
* @description: 自定义用户类
* @author: liyonghui
* @date: 2022/4/25 15:54
*/
public class AccountUser implements UserDetails {
private Long userId;
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
public String getPassword() {
return this.password;
}
public String getUsername() {
return this.username;
}
public boolean isEnabled() {
return this.enabled;
}
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
public void eraseCredentials() {
this.password = null;
}
public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
}
@Autowired
UserDetailsServiceImpl userDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
package com.huihui.vueadminapi;
import com.huihui.vueadminapi.common.lang.Result;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
class VueadminApiApplicationTests {
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Test
void contextLoads() {
}
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Test
void test1() {
// 加密后密码
String password = bCryptPasswordEncoder.encode("111111");
boolean matches = bCryptPasswordEncoder.matches("111111", password);
System.out.println("匹配结果:" + matches);
System.out.println(Result.succ(password));
}
}
匹配结果:true Result(code=200, msg=操作成功, data=$2a$10$5ihW2csXoPk1kXuFNtRw..be7pCxbTCDIn.4D4gW4B7od/b3P7bY6)
用生成的密码把数据库的test的test账号的密码替换掉。
4,重启后台验证
目的验证数据库权限验证是否生效。
postman工具Post方式访问地址:http://localhost:8081/login?username=test&password=111111&code=123456&token=112233
Pre-request Script之前添加
pm.sendRequest("http://127.0.0.1:8081/captcha", function (err, response) {
console.log(response.json());
});
下发请求可以看到能正常返回:
{
"msg": "操作成功",
"code": 200,
"data": ""
}
//新增:
/**
* 获取用户权限信息(角色、菜单权限)
* @param userId
* @return
*/
public List<GrantedAuthority> getUserAuthority(Long userId){
// 角色(ROLE_admin)、菜单操作权限 sys:user:list
String authority = sysUserService.getUserAuthorityInfo(userId); // ROLE_admin,ROLE_normal,sys:user:list,....
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
//新增方法
String getUserAuthorityInfo(Long userId);
package com.huihui.vueadminapi.mapper;
import com.huihui.vueadminapi.entity.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 用户表(SysUser)表数据库访问层
*
* @author liyonghui
* @since 2022-04-14 14:09:17
*/
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {
List<Long> getNavMenuIds(Long userId);
}
<select id="getNavMenuIds" resultType="java.lang.Long">
SELECT
DISTINCT rm.menu_id
FROM
sys_user_role ur
LEFT JOIN `sys_role_menu` rm ON rm.role_id = ur.role_id
WHERE
ur.user_id = #{userId};
</select>
package com.huihui.vueadminapi.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.huihui.vueadminapi.entity.SysMenu;
import com.huihui.vueadminapi.entity.SysRole;
import com.huihui.vueadminapi.mapper.SysUserMapper;
import com.huihui.vueadminapi.entity.SysUser;
import com.huihui.vueadminapi.service.SysMenuService;
import com.huihui.vueadminapi.service.SysRoleService;
import com.huihui.vueadminapi.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户表(SysUser)表服务实现类
*
* @author liyonghui
* @since 2022-04-14 14:09:17
*/
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Autowired
SysRoleService sysRoleService;
@Autowired
SysUserMapper sysUserMapper;
@Autowired
SysMenuService sysMenuService;
@Override
public SysUser getByUserName(String userName) {
return getOne(new QueryWrapper<SysUser>().eq("username", userName));
}
@Override
public String getUserAuthorityInfo(Long userId) {
SysUser sysUser = this.getById(userId);
List<SysRole> roles = sysRoleService.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id = " + userId));
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
List<SysMenu> menus = sysMenuService.listByIds(menuIds);
String roleNames = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
String permNames = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
String authority = roleNames.concat(",").concat(permNames);
log.info("用户ID - {} ---拥有的权限:{}", userId, authority);
return authority;
}
}
//新增
@Autowired
UserDetailsServiceImpl userDetailsService;
//修改方法
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtils.getHeader());
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
log.info("用户-{},正在登陆!", username);
SysUser sysUser = sysUserService.getByUserName(username);
List<GrantedAuthority> grantedAuthorities = userDetailsService.getUserAuthority(sysUser.getId());
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities);
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}
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.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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;
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@PreAuthorize("hasRole('admin')")
@GetMapping("/test")
public Result test() {
return Result.succ(sysUserService.list());
}
// 普通用户、超级管理员
@PreAuthorize("hasAuthority('sys:user:list')")
@GetMapping("/test/pass")
public Result pass() {
// 加密后密码
String password = bCryptPasswordEncoder.encode("111111");
boolean matches = bCryptPasswordEncoder.matches("111111", password);
System.out.println("匹配结果:" + matches);
return Result.succ(password);
}
}
无权限的验证:
1,获取密钥
请求方式:POST
http://localhost:8081/login?username=test&password=111111&code=123456&token=112233
Pre-request Script:
pm.sendRequest("http://127.0.0.1:8081/captcha", function (err, response) {
console.log(response.json());
});
2,用登录的密钥访问test接口。
http://localhost:8081/test
增加header:
Authorization:值是从上面的请求的返回结果中获取
返回结果:
{
"code": 400,
"msg": "不允许访问",
"data": null
}
有权限的验证:
1,获取密钥
请求方式:POST
http://localhost:8081/login?username=admin&password=111111&code=123456&token=112233
Pre-request Script:
pm.sendRequest("http://127.0.0.1:8081/captcha", function (err, response) {
console.log(response.json());
});
2,用登录的密钥访问test接口。
http://localhost:8081/test
增加header:
Authorization:值是从上面的请求的返回结果中获取
返回结果:
{
"code": 200,
"msg": "操作成功",
"data": [
{
"id": 1,
"username": "admin",
"password": "$bY6",
....
}
]
}
总结:test接口限定了必须有admin权限的用户才能访问,而admin用户拥有所有权限。所以可正常访问接口。
//1.增加redisutil
@Autowired
RedisUtil redisUtil;
//2.修改方法getUserAuthorityInfo
@Override
public String getUserAuthorityInfo(Long userId) {
SysUser sysUser = this.getById(userId);
String authority = null;
if (redisUtil.hasKey("GrantedAuthority:" + sysUser.getUsername())) {
// 优先从缓存获取
authority = (String) redisUtil.get("GrantedAuthority:" + sysUser.getUsername());
} else {
List<SysRole> roles = sysRoleService.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id = " + userId));
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
List<SysMenu> menus = sysMenuService.listByIds(menuIds);
String roleNames = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
String permNames = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority = roleNames.concat(",").concat(permNames);
log.info("用户ID - {} ---拥有的权限:{}", userId, authority);
redisUtil.set("GrantedAuthority:" + sysUser.getUsername(), authority, 60 * 60);
}
return authority;
}
1,先用admin登录
//POST
http://localhost:8081/login?username=admin&password=111111&code=123456&token=112233
//Pre-request Script
pm.sendRequest("http://127.0.0.1:8081/captcha", function (err, response) {
console.log(response.json());
});
2,再调用pass接口
http://localhost:8081/test/pass
3,第一次访问没走redis
在SysUserServiceImpl的getUserAuthorityInfo方法打断点。
4,第二次走redis缓存
当用户,角色,菜单发生改变时要清空缓存。解决方式:
这里只写处了删除方法,但未调用,后续集成功能后使用。
void clearUserAuthorityInfo(String username);
void clearUserAuthorityInfoByRoleId(Long roleId);
void clearUserAuthorityInfoByMenuId(Long menuId);
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {
List<Long> getNavMenuIds(Long userId);
List<SysUser> listByMenuId(Long menuId);
}
<select id="listByMenuId" resultType="com.huihui.vueadminapi.entity.SysUser">
SELECT DISTINCT
su.*
FROM
sys_user_role ur
LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN sys_user su ON ur.user_id = su.id
WHERE
rm.menu_id = #{menuId}
</select>
//新增方法
@Override
public void clearUserAuthorityInfo(String username) {
redisUtil.del("GrantedAuthority:" + username);
}
@Override
public void clearUserAuthorityInfoByRoleId(Long roleId) {
List<SysUser> sysUsers = this.list(new QueryWrapper<SysUser>()
.inSql("id", "select user_id from sys_user_role where role_id = " + roleId));
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
@Override
public void clearUserAuthorityInfoByMenuId(Long menuId) {
List<SysUser> sysUsers = sysUserMapper.listByMenuId(menuId);
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
package com.huihui.vueadminapi.security;
import cn.hutool.json.JSONUtil;
import com.huihui.vueadminapi.common.lang.Result;
import com.huihui.vueadminapi.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader(jwtUtils.getHeader(), "");
Result result = Result.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
//configure方法新增
//退出登录
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
package com.huihui.vueadminapi.common.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* {
* name: 'SysUser',
* title: '用户管理',
* icon: 'el-icon-s-custom',
* path: '/sys/users',
* component: 'sys/User',
* children: []
* },
*/
@Data
public class SysMenuDto implements Serializable {
private Long id;
private String name;
private String title;
private String icon;
private String path;
private String component;
private List<SysMenuDto> children = new ArrayList<>();
}
package com.huihui.vueadminapi.entity;
import java.util.ArrayList;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
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;
import java.util.List;
/**
* (SysMenu)表实体类
*
* @author liyonghui
* @since 2022-04-14 14:09:14
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "")
@SuppressWarnings("serial")
public class SysMenu extends Model<SysMenu> implements Serializable {
private static final long serialVersionUID = 837566818131180301L;
@ApiModelProperty("主键ID")
private Long id;
@ApiModelProperty("父菜单ID,一级菜单为0")
private Long parentId;
@ApiModelProperty("菜单名称")
private String name;
@ApiModelProperty("菜单URL")
private String path;
@ApiModelProperty("授权(多个用逗号分隔,如:user:list,user:create)")
private String perms;
@ApiModelProperty("$column.comment")
private String component;
@ApiModelProperty("类型 0:目录 1:菜单 2:按钮")
private Integer type;
@ApiModelProperty("菜单图标")
private String icon;
@ApiModelProperty("排序")
private Integer ordernum;
@ApiModelProperty("创建时间")
private Date created;
@ApiModelProperty("更新时间")
private Date updated;
@ApiModelProperty("状态")
private Integer statu;
//新增项-用以存储子菜单
@TableField(exist = false)
private List<SysMenu> children = new ArrayList<>();
}
package com.huihui.vueadminapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.huihui.vueadminapi.common.dto.SysMenuDto;
import com.huihui.vueadminapi.entity.SysMenu;
import java.util.List;
/**
* (SysMenu)表服务接口层
*
* @author liyonghui
* @since 2022-04-14 14:09:14
*/
public interface SysMenuService extends IService<SysMenu> {
List<SysMenuDto> getcurrentUserNav();
}
package com.huihui.vueadminapi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.huihui.vueadminapi.common.dto.SysMenuDto;
import com.huihui.vueadminapi.entity.SysUser;
import com.huihui.vueadminapi.mapper.SysMenuMapper;
import com.huihui.vueadminapi.entity.SysMenu;
import com.huihui.vueadminapi.mapper.SysUserMapper;
import com.huihui.vueadminapi.service.SysMenuService;
import com.huihui.vueadminapi.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* (SysMenu)表服务实现类
*
* @author liyonghui
* @since 2022-04-14 14:09:14
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
@Autowired
SysUserService sysUserService;
@Autowired
SysUserMapper sysUserMapper;
@Override
public List<SysMenuDto> getcurrentUserNav() {
String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SysUser sysUser = sysUserService.getByUserName(username);
// 获取用户的所有菜单
List<Long> menuIds = sysUserMapper.getNavMenuIds(sysUser.getId());
List<SysMenu> menus = buildTreeMenu(this.listByIds(menuIds));
return convert(menus);
}
/**
* 把list转成树形结构的数据
*/
private List<SysMenu> buildTreeMenu(List<SysMenu> menus) {
List<SysMenu> finalMenus = new ArrayList<>();
for (SysMenu menu : menus) {
// 先寻找各自的孩子
for (SysMenu e : menus) {
if (e.getParentId() == menu.getId()) {
menu.getChildren().add(e);
}
}
// 提取出父节点
if (menu.getParentId() == 0L) {
finalMenus.add(menu);
}
}
return finalMenus;
}
/**
* menu转menuDto
*/
private List<SysMenuDto> convert(List<SysMenu> menus) {
List<SysMenuDto> menuDtos = new ArrayList<>();
menus.forEach(m -> {
SysMenuDto dto = new SysMenuDto();
dto.setId(m.getId());
dto.setName(m.getPerms());
dto.setTitle(m.getName());
dto.setComponent(m.getComponent());
dto.setIcon(m.getIcon());
dto.setPath(m.getPath());
if (m.getChildren().size() > 0) {
dto.setChildren(convert(m.getChildren()));
}
menuDtos.add(dto);
});
return menuDtos;
}
}
package com.huihui.vueadminapi.controller;
import cn.hutool.core.map.MapUtil;
import com.huihui.vueadminapi.common.lang.Result;
import com.huihui.vueadminapi.entity.SysUser;
import com.huihui.vueadminapi.service.SysMenuService;
import com.huihui.vueadminapi.service.SysUserService;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.security.Principal;
/**
* (SysMenu)表服务控制层
*
* @author liyonghui
* @since 2022-04-14 14:09:15
*/
@Api(tags = "(/sys/menu)")
@Validated
@RestController
@AllArgsConstructor
@RequestMapping("/sys/menu")
public class SysMenuController {
@Resource
private final SysMenuService sysMenuService;
@Autowired
SysUserService sysUserService;
/**
* 获取当前用户的菜单栏以及权限
*/
@GetMapping("/nav")
public Result nav(Principal principal) {
String username = principal.getName();
SysUser sysUser = sysUserService.getByUserName(username);
// ROLE_Admin,sys:user:save
String[] authoritys = StringUtils.tokenizeToStringArray(
sysUserService.getUserAuthorityInfo(sysUser.getId())
, ",");
return Result.succ(
MapUtil.builder()
.put("nav", sysMenuService.getcurrentUserNav())
.put("authoritys", authoritys)
.map()
);
}
}
1,先用admin登录
//POST
http://localhost:8081/login?username=admin&password=111111&code=123456&token=112233
//Pre-request Script
pm.sendRequest("http://127.0.0.1:8081/captcha", function (err, response) {
console.log(response.json());
});
2,再调用菜单接口
//GET
http://localhost:8081/sysMenu/nav
//Header
Authorization:xx
菜单地址改成:sys/menu
新增
@GetMapping("/info/{id}")
@PreAuthorize("hasAuthority('sys:menu:list')")
public Result info(@PathVariable(name = "id") Long id) {
return Result.succ(sysMenuService.getById(id));
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('sys:menu:list')")
public Result list() {
List<SysMenu> menus = sysMenuService.tree();
return Result.succ(menus);
}
@PostMapping("/save")
@PreAuthorize("hasAuthority('sys:menu:save')")
public Result save(@Validated @RequestBody SysMenu sysMenu) {
sysMenu.setCreated(new Date());
sysMenuService.save(sysMenu);
return Result.succ(sysMenu);
}
@PostMapping("/update")
@PreAuthorize("hasAuthority('sys:menu:update')")
public Result update(@Validated @RequestBody SysMenu sysMenu) {
sysMenu.setUpdated(new Date());
sysMenuService.updateById(sysMenu);
// 清除所有与该菜单相关的权限缓存
sysUserService.clearUserAuthorityInfoByMenuId(sysMenu.getId());
return Result.succ(sysMenu);
}
@PostMapping("/delete/{id}")
@PreAuthorize("hasAuthority('sys:menu:delete')")
public Result delete(@PathVariable("id") Long id) {
int count = sysMenuService.count(new QueryWrapper<SysMenu>().eq("parent_id", id));
if (count > 0) {
return Result.fail("请先删除子菜单");
}
// 清除所有与该菜单相关的权限缓存
sysUserService.clearUserAuthorityInfoByMenuId(id);
sysMenuService.removeById(id);
// 同步删除中间关联表
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("menu_id", id));
return Result.succ("");
}
新增
List<SysMenu> tree();
新增
@Override
public List<SysMenu> tree() {
// 获取所有菜单信息
List<SysMenu> sysMenus = this.list(new QueryWrapper<SysMenu>().orderByAsc("orderNum"));
// 转成树状结构
return buildTreeMenu(sysMenus);
}
新增
@GetMapping("/captcha")
public Result captcha() throws IOException {
String key = UUID.randomUUID().toString();
String code = producer.createText();
key="112233";
//这里改成12345
code="12345";
package com.huihui.vueadminapi.entity;
import java.util.ArrayList;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
/**
* (SysMenu)表实体类
*
* @author liyonghui
* @since 2022-04-14 14:09:14
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "")
@SuppressWarnings("serial")
public class SysMenu extends Model<SysMenu> implements Serializable {
private static final long serialVersionUID = 837566818131180301L;
@ApiModelProperty("主键ID")
@TableId(value="id",type= IdType.AUTO)
private Long id;
@NotNull(message = "上级菜单不能为空")
@ApiModelProperty("父菜单ID,一级菜单为0")
private Long parentId;
@NotBlank(message = "菜单名称不能为空")
@ApiModelProperty("菜单名称")
private String name;
@ApiModelProperty("菜单URL")
private String path;
@NotBlank(message = "菜单授权码不能为空")
@ApiModelProperty("授权(多个用逗号分隔,如:user:list,user:create)")
private String perms;
@ApiModelProperty("$column.comment")
private String component;
@NotNull(message = "菜单类型不能为空")
@ApiModelProperty("类型 0:目录 1:菜单 2:按钮")
private Integer type;
@ApiModelProperty("菜单图标")
private String icon;
@TableField("orderNum")
@ApiModelProperty("排序")
private Integer ordernum;
@ApiModelProperty("创建时间")
private Date created;
@ApiModelProperty("更新时间")
private Date updated;
@ApiModelProperty("状态")
private Integer statu;
@TableField(exist = false)
private List<SysMenu> children = new ArrayList<>();
}
直接登录web,手动测试菜单功能项
package com.huihui.vueadminapi.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.huihui.vueadminapi.common.lang.Const;
import com.huihui.vueadminapi.common.lang.Result;
import com.huihui.vueadminapi.entity.SysRole;
import com.huihui.vueadminapi.entity.SysRoleMenu;
import com.huihui.vueadminapi.entity.SysUserRole;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* (SysRole)表服务控制层
*
* @author liyonghui
* @since 2022-04-14 14:09:15
*/
@Api(tags = "(sys/role)")
@Validated
@RestController
@AllArgsConstructor
@RequestMapping("/sys/role")
public class SysRoleController extends BaseController {
@PreAuthorize("hasAuthority('sys:role:list')")
@GetMapping("/info/{id}")
public Result info(@PathVariable("id") Long id) {
SysRole sysRole = sysRoleService.getById(id);
// 获取角色相关联的菜单id
List<SysRoleMenu> roleMenus = sysRoleMenuService.list(new QueryWrapper<SysRoleMenu>().eq("role_id", id));
List<Long> menuIds = roleMenus.stream().map(p -> p.getMenuId()).collect(Collectors.toList());
sysRole.setMenuIds(menuIds);
return Result.succ(sysRole);
}
@PreAuthorize("hasAuthority('sys:role:list')")
@GetMapping("/list")
public Result list(String name) {
Page<SysRole> pageData = sysRoleService.page(getPage(),
new QueryWrapper<SysRole>()
.like(StrUtil.isNotBlank(name), "name", name)
);
return Result.succ(pageData);
}
@PostMapping("/save")
@PreAuthorize("hasAuthority('sys:role:save')")
public Result save(@Validated @RequestBody SysRole sysRole) {
sysRole.setCreated(new Date());
sysRole.setStatu(Const.STATUS_ON);
sysRoleService.save(sysRole);
return Result.succ(sysRole);
}
@PostMapping("/update")
@PreAuthorize("hasAuthority('sys:role:update')")
public Result update(@Validated @RequestBody SysRole sysRole) {
sysRole.setUpdated(new Date());
sysRoleService.updateById(sysRole);
// 更新缓存
sysUserService.clearUserAuthorityInfoByRoleId(sysRole.getId());
return Result.succ(sysRole);
}
@PostMapping("/delete")
@PreAuthorize("hasAuthority('sys:role:delete')")
@Transactional
public Result info(@RequestBody Long[] ids) {
sysRoleService.removeByIds(Arrays.asList(ids));
// 删除中间表
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("role_id", ids));
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().in("role_id", ids));
// 缓存同步删除
Arrays.stream(ids).forEach(id -> {
// 更新缓存
sysUserService.clearUserAuthorityInfoByRoleId(id);
});
return Result.succ("");
}
@Transactional
@PostMapping("/perm/{roleId}")
@PreAuthorize("hasAuthority('sys:role:perm')")
public Result info(@PathVariable("roleId") Long roleId, @RequestBody Long[] menuIds) {
List<SysRoleMenu> sysRoleMenus = new ArrayList<>();
Arrays.stream(menuIds).forEach(menuId -> {
SysRoleMenu roleMenu = new SysRoleMenu();
roleMenu.setMenuId(menuId);
roleMenu.setRoleId(roleId);
sysRoleMenus.add(roleMenu);
});
// 先删除原来的记录,再保存新的
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("role_id", roleId));
sysRoleMenuService.saveBatch(sysRoleMenus);
// 删除缓存
sysUserService.clearUserAuthorityInfoByRoleId(roleId);
return Result.succ(menuIds);
}
}
package com.huihui.vueadminapi.entity;
import java.util.ArrayList;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.List;
/**
* (SysRole)表实体类
*
* @author liyonghui
* @since 2022-04-14 14:09:15
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "")
@SuppressWarnings("serial")
public class SysRole extends Model<SysRole> implements Serializable {
private static final long serialVersionUID = -14252938893737275L;
@ApiModelProperty("角色ID")
@TableId(value="id",type= IdType.AUTO)
private Long id;
@NotBlank(message = "角色名称不能为空")
@ApiModelProperty("角色名称")
private String name;
@NotBlank(message = "角色编码不能为空")
@ApiModelProperty("角色标识")
private String code;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("创建时间")
private Date created;
@ApiModelProperty("更新时间")
private Date updated;
@ApiModelProperty("状态")
private Integer statu;
/**
* @description 角色关联菜单ID
* @author liyonghui
* @date 2022/4/28 14:59
*/
@TableField(exist = false)
private List<Long> menuIds = new ArrayList<>();
}
重启后登录前端。对角色列表进行crud测试。
数据库角色表主键又长又乱。
解决办法:
实体类的主键ID位置增加注解,效果:
@TableId(value="id",type= IdType.AUTO)
private Long id;
application.yml文件中添加
mybatis-plus:
global-config:
db-config:
id-type: auto #id生成规则:数据库id自增
新增的角色状态显示异常:
//修改常量值
public final static Integer STATUS_ON = 1;
public final static Integer STATUS_OFF = 0;
package com.huihui.vueadminapi.entity;
import java.util.ArrayList;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.List;
/**
* 用户表(SysUser)表实体类
*
* @author liyonghui
* @since 2022-04-14 14:09:17
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "")
@SuppressWarnings("serial")
public class SysUser extends Model<SysUser> implements Serializable {
private static final long serialVersionUID = -73832255149920096L;
@ApiModelProperty("用户ID")
@TableId(value="id",type= IdType.AUTO)
private Long id;
@NotBlank(message = "用户名不能为空")
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("头像")
private String avatar;
@ApiModelProperty("邮箱")
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@ApiModelProperty("城市")
private String city;
@ApiModelProperty("创建时间")
private Date created;
@ApiModelProperty("更新时间")
private Date updated;
@ApiModelProperty("最后登录时间")
private Date lastLogin;
@ApiModelProperty("状态")
private Integer statu;
@TableField(exist = false)
private List<SysRole> sysRoles = new ArrayList<>();
}
package com.huihui.vueadminapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.huihui.vueadminapi.entity.SysRole;
import java.util.List;
/**
* (SysRole)表服务接口层
*
* @author liyonghui
* @since 2022-04-14 14:09:15
*/
public interface SysRoleService extends IService<SysRole> {
List<SysRole> listRolesByUserId(Long userId);
}
package com.huihui.vueadminapi.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.huihui.vueadminapi.mapper.SysRoleMapper;
import com.huihui.vueadminapi.entity.SysRole;
import com.huihui.vueadminapi.service.SysRoleService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* (SysRole)表服务实现类
*
* @author liyonghui
* @since 2022-04-14 14:09:15
*/
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
@Override
public List<SysRole> listRolesByUserId(Long userId) {
List<SysRole> sysRoles = this.list(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + userId));
return sysRoles;
}
}
package com.huihui.vueadminapi.controller;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ser.Serializers;
import com.huihui.vueadminapi.common.dto.PassDto;
import com.huihui.vueadminapi.common.lang.Const;
import com.huihui.vueadminapi.common.lang.Result;
import com.huihui.vueadminapi.entity.SysRole;
import com.huihui.vueadminapi.entity.SysUser;
import com.huihui.vueadminapi.entity.SysUserRole;
import com.huihui.vueadminapi.service.SysUserService;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* 用户表(SysUser)表服务控制层
*
* @author liyonghui
* @since 2022-04-14 14:09:17
*/
@Api(tags = "用户表(SysUser)")
@Validated
@RestController
@AllArgsConstructor
@RequestMapping("/sys/user")
public class SysUserController extends BaseController {
@Autowired
BCryptPasswordEncoder passwordEncoder;
@GetMapping("/selectOne")
public SysUser selectOne() {
return sysUserService.getById("1");
}
@GetMapping
@PreAuthorize("hasAuthority('sys:user:list')")
public Result info(@PathVariable("id") Long id) {
SysUser sysUser = sysUserService.getById(id);
Assert.notNull(sysUser, "找不到该管理员");
List<SysRole> roles = sysRoleService.listRolesByUserId(id);
sysUser.setSysRoles(roles);
return Result.succ(roles);
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('sys:user:list')")
public Result list(String username) {
Page<SysUser> pageData = sysUserService.page(getPage(), new QueryWrapper<SysUser>().like(StrUtil.isNotBlank(username), "username", username));
return Result.succ(pageData);
}
@PostMapping("save")
@PreAuthorize("hasAuthority('sys:user:save')")
public Result save(@Validated @RequestBody SysUser sysUser) {
sysUser.setCreated(new Date());
sysUser.setStatu(Const.STATUS_ON);
//默认密码
String password = passwordEncoder.encode(Const.DEFULT_PASSWORD);
sysUser.setPassword(password);
//默认头像
sysUser.setAvatar(Const.DEFULT_AVATAR);
sysUserService.save(sysUser);
return Result.succ(sysUser);
}
@PostMapping("/update")
@PreAuthorize("hasAuthority('sys:user:update')")
public Result update(@Validated @RequestBody SysUser sysUser) {
sysUser.setUpdated(new Date());
sysUserService.updateById(sysUser);
return Result.succ(sysUser);
}
@Transactional
@PostMapping("/delete")
@PreAuthorize("hasAuthority('sys:user:delete')")
public Result delete(@RequestBody Long[] ids) {
sysUserService.removeByIds(Arrays.asList(ids));
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("user_id", ids));
return Result.succ("");
}
@Transactional
@PostMapping("/role/{userId}")
@PreAuthorize("hasAuthority('sys:user:role')")
public Result rolePerm(@PathVariable("userId") Long userId, @RequestBody Long[] roleIds) {
List<SysUserRole> userRoles = new ArrayList<>();
Arrays.stream(roleIds).forEach(r -> {
SysUserRole sysUserRole = new SysUserRole();
sysUserRole.setRoleId(r);
sysUserRole.setUserId(userId);
userRoles.add(sysUserRole);
});
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().eq("user_id", userId));
sysUserRoleService.saveBatch(userRoles);
// 删除缓存
SysUser sysUser = sysUserService.getById(userId);
sysUserService.clearUserAuthorityInfo(sysUser.getUsername());
return Result.succ("");
}
@PostMapping("/repass")
@PreAuthorize("hasAuthority('sys:user:repass')")
public Result repass(@RequestBody Long userId) {
SysUser sysUser = sysUserService.getById(userId);
sysUser.setPassword(passwordEncoder.encode(Const.DEFULT_PASSWORD));
sysUser.setUpdated(new Date());
sysUserService.updateById(sysUser);
return Result.succ("");
}
@PostMapping("/updatePass")
public Result updatePass(@Validated @RequestBody PassDto passDto, Principal principal) {
SysUser sysUser = sysUserService.getByUserName(principal.getName());
boolean matches = passwordEncoder.matches(passDto.getCurrentPass(), sysUser.getPassword());
if (!matches) {
return Result.fail("旧密码不正确");
}
sysUser.setPassword(passwordEncoder.encode(passDto.getPassword()));
sysUser.setUpdated(new Date());
sysUserService.updateById(sysUser);
return Result.succ(new Date());
}
}
登录web,测试用户功能。
@Data
public class PassDto implements Serializable {
@NotBlank(message = "新密码不能为空")
private String password;
@NotBlank(message = "旧密码不能为空")
private String currentPass;
}
@PostMapping("/updatePass")
public Result updatePass(@Validated @RequestBody PassDto passDto, Principal principal) {
SysUser sysUser = sysUserService.getByUserName(principal.getName());
boolean matches = passwordEncoder.matches(passDto.getCurrentPass(), sysUser.getPassword());
if (!matches) {
return Result.fail("旧密码不正确");
}
sysUser.setPassword(passwordEncoder.encode(passDto.getPassword()));
sysUser.setUpdated(new Date());
sysUserService.updateById(sysUser);
return Result.succ(new Date());
}
1,前台新增个人中心
{
path: '/userCenter',
name: 'UserCenter',
meta: {
title: "个人中心"
},
//懒加载
component: () => import( '@/views/UserCenter.vue')
},
router.beforeEach((to, from, next) => {
let hasRoute = store.state.menus.hasRoutes;
let token = localStorage.getItem("token")
if (to.path == '/login') {
next()
} else if (!token) {
next({path: '/login'})
} else if(token && !hasRoute){
//此处省略...
}
import axios from "axios";
修改为:
import axios from "./axios";
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。