# vueadmin-api **Repository Path**: hhgs_admin/vueadmin-api ## Basic Information - **Project Name**: vueadmin-api - **Description**: vueadmin后台api第一版环境 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2022-04-14 - **Last Updated**: 2024-05-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: 前后台分离管理系统 ## README ## 项目参考文档地址:https://www.zhuawaba.com/post/19#1.%20%E5%89%8D%E8%A8%80 项目参考视频地址:https://www.bilibili.com/video/BV1af4y1s7Wh?p=54 一,新建springBoot项目 ##### 方式一:用mybatis-spring-boot-starter集成 ###### pom.xml ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.0 com.huihui vueadmin-api 0.0.1-SNAPSHOT vueadmin-api vueadmin后台api 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java runtime org.projectlombok lombok true com.github.xiaoymin knife4j-spring-boot-starter 2.0.8 org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.3 org.springframework.boot spring-boot-maven-plugin ``` ###### application.yml ###### ```yml server: port: 8081 # DataSource Config spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.101.54:3307/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath*:/mapper/**Mapper.xml ``` ###### 启动类增加注解: ``` @MapperScan("com.huihui.vueadminapi.mapper") ``` ##### 方式二:用mybatis-plus-boot-starter集成 ###### 1,pom.xml 这里加的是mybatis-plus-generator依赖。 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.0 com.huihui vueadmin-api 0.0.1-SNAPSHOT vueadmin-api vueadmin后台api 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java runtime org.projectlombok lombok true com.github.xiaoymin knife4j-spring-boot-starter 2.0.8 com.baomidou mybatis-plus-boot-starter 3.4.1 org.springframework.boot spring-boot-maven-plugin ``` ###### 2,application.yml ```yml server: port: 8081 # DataSource Config spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.101.54:3307/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: mapper-locations: classpath*:/mapper/**Mapper.xml #mybatis: # mapper-locations: classpath*:/mapper/**Mapper.xml ``` ###### 启动类增加注解: ```java @MapperScan("com.huihui.vueadminapi.mapper") ``` ## 二:配置easycode模板: #### 方式一:对应easycode模板: 新增模板生成映射文件前记得在数据库字段类型映射位置(File->Settings-->Other Settings-->EasyCode-->Type Mapper)位置增加tinyint的对应关系:tinyint((\d+))? #### easycode模板 ###### entity.java ```java ##引入宏定义 $!{define.vm} ##使用宏定义设置回调(保存位置与文件后缀) #save("/entity", ".java") ##使用宏定义设置包后缀 #setPackageSuffix("entity") ##使用全局变量实现默认包导入 $!{autoImport.vm} import java.io.Serializable; import io.swagger.annotations.*; import lombok.Data; ##使用宏定义实现类注释信息 #tableComment("实体类") @Data @ApiModel("$tableInfo.comment") public class $!{tableInfo.name} implements Serializable { private static final long serialVersionUID = $!tool.serial(); #foreach($column in $tableInfo.fullColumn) #if(${column.comment})/** * ${column.comment} */#end @ApiModelProperty("$column.comment") private $!{tool.getClsNameByFullName($column.type)} $!{column.name}; #end } ``` ###### mapper.java ```java ##定义初始变量 #set($tableName = $tool.append($tableInfo.name, "Mapper")) ##设置回调 $!callback.setFileName($tool.append($tableName, ".java")) $!callback.setSavePath($tool.append($tableInfo.savePath, "/mapper")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}mapper; import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name}; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import java.util.List; /** * $!{tableInfo.comment}($!{tableInfo.name})表数据库访问层 * * @author $!author * @since $!time.currTime() */ @Mapper @Repository public interface $!{tableName} { /** * 通过ID查询单条数据 * * @param $!pk.name 主键 * @return 实例对象 */ $!{tableInfo.name} queryById($!pk.shortType $!pk.name); /** * 查询指定行数据 * * @param offset 查询起始位置 * @param limit 查询条数 * @return 对象列表 */ List<$!{tableInfo.name}> queryAllByLimit(@Param("offset") int offset, @Param("limit") int limit); /** * 通过实体作为筛选条件查询 * * @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象 * @return 对象列表 */ List<$!{tableInfo.name}> queryAll($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})); /** * 新增数据 * * @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象 * @return 影响行数 */ int insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})); /** * 修改数据 * * @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象 * @return 影响行数 */ int update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})); /** * 通过主键删除数据 * * @param $!pk.name 主键 * @return 影响行数 */ int deleteById($!pk.shortType $!pk.name); } ``` ###### mapper.xml ```java ##引入mybatis支持 $!{mybatisSupport.vm} ##设置保存名称与保存位置 $!callback.setFileName($tool.append($!{tableInfo.name}, "Mapper.xml")) $!callback.setSavePath($tool.append($modulePath, "/src/main/resources/mapper")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #foreach($column in $tableInfo.fullColumn) #end insert into $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name}(#foreach($column in $tableInfo.otherColumn)$!column.obj.name#if($velocityHasNext), #end#end) values (#foreach($column in $tableInfo.otherColumn)#{$!{column.name}}#if($velocityHasNext), #end#end) update $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name} #foreach($column in $tableInfo.otherColumn) $!column.obj.name = #{$!column.name}, #end where $!pk.obj.name = #{$!pk.name} delete from $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name} where $!pk.obj.name = #{$!pk.name} ``` ###### service.java ```java ##定义初始变量 #set($tableName = $tool.append($tableInfo.name, "Service")) ##设置回调 $!callback.setFileName($tool.append($tableName, ".java")) $!callback.setSavePath($tool.append($tableInfo.savePath, "/service")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}service; import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name}; import $!{tableInfo.savePackageName}.mapper.$!{tableInfo.name}Mapper; import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; /** * $!{tableInfo.comment}($!{tableInfo.name})表服务实现类 * * @author $!author * @since $!time.currTime() */ @Service("$!tool.firstLowerCase($!{tableInfo.name})Service") public class $!{tableName} { @Autowired private $!{tableInfo.name}Mapper $!tool.firstLowerCase($!{tableInfo.name})Mapper; /** * 通过ID查询单条数据 * * @param $!pk.name 主键 * @return 实例对象 */ public $!{tableInfo.name} queryById($!pk.shortType $!pk.name) { return this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.queryById($!pk.name); } /** * 查询多条数据 * * @param offset 查询起始位置 * @param limit 查询条数 * @return 对象列表 */ public List<$!{tableInfo.name}> queryAllByLimit(int offset, int limit) { return this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.queryAllByLimit(offset, limit); } /** * 新增数据 * * @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象 * @return 实例对象 */ public $!{tableInfo.name} insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) { this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.insert($!tool.firstLowerCase($!{tableInfo.name})); return $!tool.firstLowerCase($!{tableInfo.name}); } /** * 修改数据 * * @param $!tool.firstLowerCase($!{tableInfo.name}) 实例对象 * @return 实例对象 */ public $!{tableInfo.name} update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) { this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.update($!tool.firstLowerCase($!{tableInfo.name})); return this.queryById($!{tool.firstLowerCase($!{tableInfo.name})}.get$!tool.firstUpperCase($pk.name)()); } /** * 通过主键删除数据 * * @param $!pk.name 主键 * @return 是否成功 */ public boolean deleteById($!pk.shortType $!pk.name) { return this.$!{tool.firstLowerCase($!{tableInfo.name})}Mapper.deleteById($!pk.name) > 0; } } ``` ###### controller.java ```java ##定义初始变量 #set($tableName = $tool.append($tableInfo.name, "Controller")) ##设置回调 $!callback.setFileName($tool.append($tableName, ".java")) $!callback.setSavePath($tool.append($tableInfo.savePath, "/controller")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}controller; import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name}; import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service; import org.springframework.web.bind.annotation.*; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; /** * $!{tableInfo.comment}($!{tableInfo.name})表控制层 * * @author $!author * @since $!time.currTime() */ @Api(tags = "$!{tableInfo.comment}($!{tableInfo.name})") @RestController @RequestMapping("$!tool.firstLowerCase($tableInfo.name)") public class $!{tableName} { /** * 服务对象 */ @Autowired private $!{tableInfo.name}Service $!tool.firstLowerCase($tableInfo.name)Service; /** * 通过主键查询单条数据 * * @param id 主键 * @return 单条数据 */ @ApiOperation(value = "根据id查询 $!{tableInfo.comment}") @GetMapping("selectOne/{id}") public $!{tableInfo.name} selectOne(@ApiParam(value = "$!pk.comment ID") @PathVariable("id") $!pk.shortType id) { return this.$!{tool.firstLowerCase($tableInfo.name)}Service.queryById(id); } } ``` #### ## #### 方式二:对应easycode模板: 新增模板生成映射文件前记得在数据库字段类型映射位置(File->Settings-->Other Settings-->EasyCode-->Type Mapper)位置增加tinyint的对应关系:tinyint((\d+))? #### easycode模板 ###### entity.java.vm ```java ##导入宏定义 $!{define.vm} ##保存文件(宏定义) #save("/entity", ".java") ##包路径(宏定义) #setPackageSuffix("entity") ##自动导入包(全局变量) $!{autoImport.vm} import com.baomidou.mybatisplus.extension.activerecord.Model; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import java.io.Serializable; ##表注释(宏定义) ##tableComment("表实体类") /** * $!{tableInfo.comment}($!{tableInfo.name})表实体类 * * @author $!author * @since $!time.currTime() */ @EqualsAndHashCode(callSuper = true) @Data @ApiModel(description = "") @SuppressWarnings("serial") public class $!{tableInfo.name} extends Model<$!{tableInfo.name}> implements Serializable { private static final long serialVersionUID = $!tool.serial(); #foreach($column in $tableInfo.fullColumn) ##if(${column.comment})/** ##* ${column.comment} ##*/#end @ApiModelProperty("$column.comment") private $!{tool.getClsNameByFullName($column.type)} $!{column.name}; #end } ``` ###### mapper.java.vm ```java ##定义初始变量 #set($tableName = $tool.append($tableInfo.name, "Mapper")) ##设置回调 $!callback.setFileName($tool.append($tableName, ".java")) $!callback.setSavePath($tool.append($tableInfo.savePath, "/mapper")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}mapper; import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name}; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * $!{tableInfo.comment}($!{tableInfo.name})表数据库访问层 * * @author $!author * @since $!time.currTime() */ public interface $!{tableName} extends BaseMapper<$!{tableInfo.name}>{ } ``` ###### service.java.vm ```java ##定义初始变量 #set($tableName = $tool.append($tableInfo.name, "Service")) ##设置回调 $!callback.setFileName($tool.append($tableName, ".java")) $!callback.setSavePath($tool.append($tableInfo.savePath, "/service")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}service; import com.baomidou.mybatisplus.extension.service.IService; import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name}; /** * $!{tableInfo.comment}($!{tableInfo.name})表服务接口层 * * @author $!author * @since $!time.currTime() */ public interface $!{tableInfo.name}Service extends IService<$!{tableInfo.name}>{ } ``` ###### impl.java.vm ```java ##导入宏定义 $!{define.vm} ##设置表后缀(宏定义) #setTableSuffix("ServiceImpl") ##保存文件(宏定义) #save("/service/impl", "ServiceImpl.java") ##包路径(宏定义) #setPackageSuffix("service.impl") import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import $!{tableInfo.savePackageName}.mapper.$!{tableInfo.name}Mapper; import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name}; import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service; import org.springframework.stereotype.Service; ##表注释(宏定义) ##tableComment("表服务实现类") /** * $!{tableInfo.comment}($!{tableInfo.name})表服务实现类 * * @author $!author * @since $!time.currTime() */ @Service public class $!{tableInfo.name}ServiceImpl extends ServiceImpl<$!{tableInfo.name}Mapper, $!{tableInfo.name}> implements $!{tableInfo.name}Service { } ``` ###### controller.java.vm ```java ##定义初始变量 #set($tableName = $tool.append($tableInfo.name, "Controller")) ##设置回调 $!callback.setFileName($tool.append($tableName, ".java")) $!callback.setSavePath($tool.append($tableInfo.savePath, "/controller")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}controller; import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service; import io.swagger.annotations.Api; import lombok.AllArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; /** * $!{tableInfo.comment}($!{tableInfo.name})表服务控制层 * * @author $!author * @since $!time.currTime() */ @Api(tags = "$!{tableInfo.comment}($!{tableInfo.name})") @Validated @RestController @AllArgsConstructor @RequestMapping("$!tool.firstLowerCase($tableInfo.name)") public class $!{tableName} { @Resource private final $!{tableInfo.name}Service $!tool.firstLowerCase($tableInfo.name)Service; } ``` ###### mapper.xml.vm ```xml ##引入mybatis支持 $!{mybatisSupport.vm} ##设置保存名称与保存位置 $!callback.setFileName($tool.append($!{tableInfo.name}, "Mapper.xml")) $!callback.setSavePath($tool.append($modulePath, "/src/main/resources/mapper")) ##拿到主键 #if(!$tableInfo.pkColumn.isEmpty()) #set($pk = $tableInfo.pkColumn.get(0)) #end #foreach($column in $tableInfo.fullColumn) #end ``` ## 三:初始化数据库 - 新建数据库:vueadmin - 导入vueadmin.sql文件 ## 四:结果封装 因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。 这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的 - 是否成功,可用code表示(如200表示成功,400表示异常) - 结果消息 - 结果数据 所以可得到封装如下: ```java package com.huihui.vueadminapi.common.lang; import lombok.Data; import java.io.Serializable; @Data public class Result implements Serializable { private int code; private String msg; private Object data; public static Result succ(Object data) { return succ(200, "操作成功", data); } public static Result succ(int code, String msg, Object data) { Result r = new Result(); r.setCode(code); r.setMsg(msg); r.setData(data); return r; } public static Result fail(String msg) { return fail(400, msg, null); } public static Result fail(int code, String msg, Object data) { Result r = new Result(); r.setCode(code); r.setMsg(msg); r.setData(data); return r; } } ``` 另外出了在结果封装类上的code可以提现数据是否正常,我们还可以通过http的状态码来提现访问是否遇到了异常,比如401表示五权限拒绝访问等,注意灵活使用。 ## 五:全局异常处理 有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。 处理办法如下:通过使用[@ControllerAdvice](https://github.com/ControllerAdvice)来进行统一异常处理,[@ExceptionHandler](https://github.com/ExceptionHandler)(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。 步骤二、定义全局异常处理,[@ControllerAdvice](https://github.com/ControllerAdvice)表示定义全局控制器异常处理,[@ExceptionHandler](https://github.com/ExceptionHandler)表示针对性异常处理,可对每种异常针对性处理。 ```java package com.huihui.vueadminapi.common.exception; import com.huihui.vueadminapi.common.lang.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { // 实体校验异常捕获 @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result handler(MethodArgumentNotValidException e) { BindingResult result = e.getBindingResult(); ObjectError objectError = result.getAllErrors().stream().findFirst().get(); log.error("实体校验异常:----------------{}", objectError.getDefaultMessage()); return Result.fail(objectError.getDefaultMessage()); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IllegalArgumentException.class) public Result handler(IllegalArgumentException e) { log.error("Assert异常:----------------{}", e.getMessage()); return Result.fail(e.getMessage()); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = RuntimeException.class) public Result handler(RuntimeException e) { log.error("运行时异常:----------------{}", e.getMessage()); return Result.fail(e.getMessage()); } } ``` 上面我们捕捉了几个异常: - ShiroException:shiro抛出的异常,比如没有权限,用户登录异常 - IllegalArgumentException:处理Assert的异常 - MethodArgumentNotValidException:处理实体校验的异常 - RuntimeException:捕捉其他异常 ## 六:整合Security 很多人不懂spring security,觉得这个框架比shiro要难,的确,security更加复杂一点,同时功能也更加强大,我们首先来看一下security的原理,这里我们引用一张来自江南一点雨大佬画的一张原理图([https://blog.csdn.net/u012702547/article/details/89629415](https://blog.csdn.net/u012702547/article/details/89629415?fileGuid=OnZDwoxFFL8bnP1c)): ![img](images/062ed76a42e84bab92b6903c34dca4c5.png) (引自江南一点雨的博客) 上面这张图一定要好好看,特别清晰,毕竟security是责任链的设计模式,是一堆过滤器链的组合,如果对于这个流程都不清楚,那么你就谈不上理解security。那么针对我们现在的这个系统,我们可以自己设计一个security的认证方案,结合江南一点雨大佬的博客,我们得到这样一套流程: https://www.processon.com/view/link/606b0b5307912932d09adcb3?fileGuid=OnZDwoxFFL8bnP1c ![图片](images/6c727925347b4349904e0731aa01d5ea.png) 流程说明: 1. 客户端发起一个请求,进入 Security 过滤器链。 2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。 3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。 4. 进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。 5. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。 Spring Security 实战干货:必须掌握的一些内置 Filter:[https://blog.csdn.net/qq_35067322/article/details/102690579](https://blog.csdn.net/qq_35067322/article/details/102690579?fileGuid=OnZDwoxFFL8bnP1c) ok,上面我们说的流程中涉及到几个组件,有些是我们需要根据实际情况来重写的。因为我们是使用json数据进行前后端数据交互,并且我们返回结果也是特定封装的。我们先再总结一下我们需要了解的几个组件: - LogoutFilter - 登出过滤器 - logoutSuccessHandler - 登出成功之后的操作类 - UsernamePasswordAuthenticationFilter - from提交用户名密码登录认证过滤器 - AuthenticationFailureHandler - 登录失败操作类 - AuthenticationSuccessHandler - 登录成功操作类 - BasicAuthenticationFilter - Basic身份认证过滤器 - SecurityContextHolder - 安全上下文静态工具类 - AuthenticationEntryPoint - 认证失败入口 - ExceptionTranslationFilter - 异常处理过滤器 - AccessDeniedHandler - 权限不足操作类 - FilterSecurityInterceptor - 权限判断拦截器、出口 有了上面的组件,那么认证与授权两个问题我们就已经接近啦,我们现在需要做的就是去重写我们的一些关键类。 #### 引入Security与jwt 首先我们导入security包,因为我们前后端交互用户凭证用的是JWT,所以我们也导入jwt的相关包,然后因为验证码的存储需要用到redis,所以引入redis。最后为了一些工具类,我们引入hutool。 - pom.xml ```xml org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-data-redis io.jsonwebtoken jjwt 0.9.1 com.github.axet kaptcha 0.0.9 cn.hutool hutool-all 5.3.3 org.apache.commons commons-lang3 3.11 ``` ```java package com.huihui.vueadminapi.controller; import com.huihui.vueadminapi.common.lang.Result; import com.huihui.vueadminapi.service.SysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @description: 测试类 * @author: liyonghui * @date: 2022/4/25 9:57 */ @RestController public class TestController { @Autowired SysUserService sysUserService; @GetMapping("/test") public Result test() { return Result.succ(sysUserService.list()); } } ``` 启动redis,然后我们再启动项目,这时候我们再去访问[http://localhost:8081/test](http://localhost:8081/test?fileGuid=OnZDwoxFFL8bnP1c),会发现系统会先判断到你未登录跳转到[http://localhost:8081/login](http://localhost:8081/login?fileGuid=OnZDwoxFFL8bnP1c),因为security内置了登录页,用户名为user,密码在启动项目的时候打印在了控制台。登录完成之后我们才可以正常访问接口。 因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码: - application.yml ```yaml spring: security: user: name: user password: 111111 redis: database: 0 host: 192.168.101.54 port: 6379 password: 123456 ``` 清空浏览器的Cookie再次访问http://localhost:8081/test 使用 配置的账号密码登录即可正常访问并返回结果。 #### 用户认证 首先我们来解决用户认证问题,分为首次登陆,和二次认证。 - 首次登录认证:用户名、密码和验证码完成登录 - 二次token认证:请求头携带Jwt进行身份认证 使用用户名密码来登录的,然后我们还想添加图片验证码,那么security给我们提供的UsernamePasswordAuthenticationFilter能使用吗? 首先security的所有过滤器都是没有图片验证码这回事的,看起来不适用了。其实这里我们可以灵活点,如果你依然想沿用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。 我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。 #### 引入Redis pom中上述引入的redis的start ##### yml配置 ```yaml server: port: 8081 # DataSource Config spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.101.54:3307/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 security: user: name: user password: 111111 #redis配置 redis: database: 0 host: 192.168.101.54 port: 6379 password: 123456 mybatis-plus: mapper-locations: classpath*:/mapper/**Mapper.xml ``` ##### Redis工具类: ```java package com.huihui.vueadminapi.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Component public class RedisUtil { @Autowired private RedisTemplate redisTemplate; /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } //============================String============================= /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } //================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map 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 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 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 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 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 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 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 getZSetRank(String key, long start, long end) { return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end); } } ``` ##### Redis配置类 RedisConfig-序列化配置 ```java 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; } } ``` #### 用户认证 首先我们来解决用户认证问题,分为首次登陆,和二次认证。 - 首次登录认证:用户名、密码和验证码完成登录 - 二次token认证:请求头携带Jwt进行身份认证 使用用户名密码来登录的,然后我们还想添加图片验证码,那么security给我们提供的UsernamePasswordAuthenticationFilter能使用吗? 首先security的所有过滤器都是没有图片验证码这回事的,看起来不适用了。其实这里我们可以灵活点,如果你依然想沿用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。 我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。 #### 生成验证码 首先我们先生成验证码,之前我们已经引用了google的验证码生成器,我们先来配置一下图片验证码的生成规则: ```java /** * @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; } } ``` 上面我定义了图片验证码的长宽字体颜色等,自己可以调整哈。 然后我们通过控制器提供生成验证码的方法: - com.huihui.vueadminapi.controller.AuthController ```java 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 ```java 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); } } ``` ##### 配置SecurityConfig - com.huihui.vueadminapi.config.SecurityConfig ```java 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 验证码展示正常。 #### 身份认证 - 1 然后认证失败的话,我们之前说过,登录失败的时候交给AuthenticationFailureHandler,所以我们自定义了 ##### LoginFailureHandler - com.huihui.vueadminapi.security.LoginFailureHandler ```java 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返回给前端而已哈。 ##### LoginSuccessHandler - com.huihui.vueadminapi.security.LoginSuccessHandler ```java 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(); } } ``` ##### 修改SecurityConfig 把刚刚新增的loginFailureHandler注入进去。 修改后的效果: ```java 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,实现先验证验证码 通过后再验证账号密码。 ##### 图片验证码认证 ###### CaptchaFilter - com.huihui.vueadminapi.security.CaptchaFilter ```java 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工具类,这个工具类代码我们就不贴出来了。 - com.huihui.vueadminapi.utils.RedisUtil ###### CaptchaException自定义异常 然后验证码出错的时候我们返回异常信息,这是一个认证异常,所以我们自定了一个CaptchaException: - com.huihui.vueadminapi.common.exception.CaptchaException ```java package com.huihui.vueadminapi.common.exception; import org.springframework.security.core.AuthenticationException; public class CaptchaException extends AuthenticationException { public CaptchaException(String msg) { super(msg); } } ``` ###### Const常量 - com.huihui.vueadminapi.common.lang.Const ```java 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添加上此过滤器,修改后效果: ```java 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认证配置 登录成功之后前端就可以获取到了jwt的信息,前端中我们是保存在了store中,同时也保存在了localStorage中,然后每次axios请求之前,我们都会添加上我们的请求头信息,可以回顾一下: - 前端项目的axios.js ![图片](images/0b2e368e1940427ca8a05e59d5c4836a.png) 所以后端进行用户身份识别的时候,我们需要通过请求头中获取jwt,然后解析出我们的用户名,这样我们就可以知道是谁在访问我们的接口啦,然后判断用户是否有权限等操作。 那么我们自定义一个过滤器用来进行识别jwt。 ##### 后台JWT工具 - com.huihui.vueadminapi.utils.JwtUtils ```java 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()); } } ``` ##### yml配置: application.yml新增配置 ```yaml markerhub: jwt: # 加密秘钥 secret: f4e2e52034348f86b67cde581c0f9eb5 # token有效时长,7天,单位秒 expire: 604800 header: Authorization ``` ##### LoginSuccessHandler修改 ```java 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(); } } ``` ###### postman做测试验证。 测试前把 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 ![image-20220425134936214](images/image-20220425134936214.png) ##### JWT验证 ###### 1,增加JWT验证的filter - com.huihui.vueadminapi.security.JwtAuthenticationFilter ```java 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); } } ``` ###### 2,修改SecurityConfig 增加JwtAuthenticationFilter的支持 ```java 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) ; } } ``` ###### 3,重启后台验证 重启后台后用postman访问地址:http://localhost:8081/test 发现返回的是登录页面,原因是header里面没有==Authorization== 重复步骤“postman做测试验证”步骤可以获取==Authorization== ,然后在header里面加上==Authorization== 即可成功访问资源。 ![image-20220425143400772](images/image-20220425143400772.png) ##### 异常处理: ###### 1,认证失败的异常 - com.huihui.vueadminapi.security.JwtAuthenticationEntryPoint ```java 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(); } } ``` ###### 2,权限不足的异常 - com.huihui.vueadminapi.security.JwtAccessDeniedHandler ```java 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(); } } ``` ###### 3,配置生效 - com.huihui.vueadminapi.config.SecurityConfig修改 新增内容如下: ```java //异常处理器 .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) ``` 4,重启后台验证 postman访问地址:http://localhost:8081/test 把==Authorization== 去掉或者值随便修改个值,此时返回的内容不再是登录页面, 【权限不足的测试后续用到再做测试,本次测试是“认证失败”的验证】而是: > { > > "msg": "请先登录", > > "code": 400 > > } > > ### 身份认证 - 2 上述的例子登录的账号密码在配置文件中,本例要改成数据库登录。 ##### 1,新增用户实现类 ###### UserDetailsServiceImpl - com.huihui.vueadminapi.security.UserDetailsServiceImpl ```java 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 getUserAuthority(Long userId){ return null; } } ``` ###### SysUserService修改 ```java 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 getByUserName(String userName); } ``` SysUserServiceImpl ```java 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 implements SysUserService { @Override public SysUser getByUserName(String userName) { return getOne(new QueryWrapper().eq("username", userName)); } } ``` ###### AccountUser 自定义用户类 - 该类是通过UserDetailServceImpl里面的UserDetails的实现类User参考实现的,目的是覆盖User,增加自定义字段userId ```java 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 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 authorities) { this(userId, username, password, true, true, true, true, authorities); } public Collection 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 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; } } ``` ##### 2,SecurityConfig使实现类生效 ###### 使用户逻辑生效 ```java @Autowired UserDetailsServiceImpl userDetailService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } ``` ##### 3,密码加密方式 ###### ```java @Bean BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } ``` ##### 4,生成密码测试类 ```java 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": "" > > } ### 解决授权 ##### 1,UserDetailsServiceImpl ```java //新增: /** * 获取用户权限信息(角色、菜单权限) * @param userId * @return */ public List getUserAuthority(Long userId){ // 角色(ROLE_admin)、菜单操作权限 sys:user:list String authority = sysUserService.getUserAuthorityInfo(userId); // ROLE_admin,ROLE_normal,sys:user:list,.... return AuthorityUtils.commaSeparatedStringToAuthorityList(authority); } ``` ###### 1.1,SysUserService ```java //新增方法 String getUserAuthorityInfo(Long userId); ``` ###### 1.2,SysUserMapper.java ```java 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 { List getNavMenuIds(Long userId); } ``` ###### 1.3,SysUserMapper.xml ```xml ``` ##### 2,SysUserServiceImpl ```java 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 implements SysUserService { @Autowired SysRoleService sysRoleService; @Autowired SysUserMapper sysUserMapper; @Autowired SysMenuService sysMenuService; @Override public SysUser getByUserName(String userName) { return getOne(new QueryWrapper().eq("username", userName)); } @Override public String getUserAuthorityInfo(Long userId) { SysUser sysUser = this.getById(userId); List roles = sysRoleService.list(new QueryWrapper() .inSql("id", "select role_id from sys_user_role where user_id = " + userId)); List menuIds = sysUserMapper.getNavMenuIds(userId); List 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; } } ``` ##### 3,JWTAuthenticationFilter ```java //新增 @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 grantedAuthorities = userDetailsService.getUserAuthority(sysUser.getId()); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } ``` ##### 3,增加测试类 ```java package com.huihui.vueadminapi.controller; import com.huihui.vueadminapi.common.lang.Result; import com.huihui.vueadminapi.service.SysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.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); } } ``` ##### 5,重启后台登录访问测试。 无权限的验证: ``` 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,修改SysUserServiceImpl ```java //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 roles = sysRoleService.list(new QueryWrapper() .inSql("id", "select role_id from sys_user_role where user_id = " + userId)); List menuIds = sysUserMapper.getNavMenuIds(userId); List 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; } ``` ##### 2,重启后台验证 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缓存 ### 缓存清空 当用户,角色,菜单发生改变时要清空缓存。解决方式: 这里只写处了删除方法,但未调用,后续集成功能后使用。 ###### 1,SysUserService ```java void clearUserAuthorityInfo(String username); void clearUserAuthorityInfoByRoleId(Long roleId); void clearUserAuthorityInfoByMenuId(Long menuId); ``` ###### 2,SysUserMapper ```java @Repository public interface SysUserMapper extends BaseMapper { List getNavMenuIds(Long userId); List listByMenuId(Long menuId); } ``` ###### 3,SysUserMapper.xml ```xml ``` ###### 4,SysUserServiceImpl //新增方法 ```java @Override public void clearUserAuthorityInfo(String username) { redisUtil.del("GrantedAuthority:" + username); } @Override public void clearUserAuthorityInfoByRoleId(Long roleId) { List sysUsers = this.list(new QueryWrapper() .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 sysUsers = sysUserMapper.listByMenuId(menuId); sysUsers.forEach(u -> { this.clearUserAuthorityInfo(u.getUsername()); }); } ``` ### 退出数据返回 #### 1,JwtLogoutSuccessHandler ```java 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(); } } ``` #### 2,SecurityConfig ```java @Autowired JwtLogoutSuccessHandler jwtLogoutSuccessHandler; //configure方法新增 //退出登录 .and() .logout() .logoutSuccessHandler(jwtLogoutSuccessHandler) ``` ## 七:菜单接口开发 ### 导航菜单新增 #### 1,新增转换实体 ```java 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 children = new ArrayList<>(); } ``` #### 2,菜单实体新增子菜单集 ```java 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 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 children = new ArrayList<>(); } ``` #### 3,SysMenuService ```java 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 { List getcurrentUserNav(); } ``` #### 4,SysMenuServiceImpl ```java 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 implements SysMenuService { @Autowired SysUserService sysUserService; @Autowired SysUserMapper sysUserMapper; @Override public List getcurrentUserNav() { String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); SysUser sysUser = sysUserService.getByUserName(username); // 获取用户的所有菜单 List menuIds = sysUserMapper.getNavMenuIds(sysUser.getId()); List menus = buildTreeMenu(this.listByIds(menuIds)); return convert(menus); } /** * 把list转成树形结构的数据 */ private List buildTreeMenu(List menus) { List 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 convert(List menus) { List 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; } } ``` #### 5,SysMenuController ```java 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() ); } } ``` #### 6,重启后台验证 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 ``` ### 菜单其他操作 #### 1,SysMenuController.java 菜单地址改成:sys/menu 新增 ```java @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 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().eq("parent_id", id)); if (count > 0) { return Result.fail("请先删除子菜单"); } // 清除所有与该菜单相关的权限缓存 sysUserService.clearUserAuthorityInfoByMenuId(id); sysMenuService.removeById(id); // 同步删除中间关联表 sysRoleMenuService.remove(new QueryWrapper().eq("menu_id", id)); return Result.succ(""); } ``` #### 2,SysMenuService 新增 ```java List tree(); ``` #### 3,SysMenuServiceImpl 新增 ```java @Override public List tree() { // 获取所有菜单信息 List sysMenus = this.list(new QueryWrapper().orderByAsc("orderNum")); // 转成树状结构 return buildTreeMenu(sysMenus); } ``` #### 4,AuthController 新增 ```java @GetMapping("/captcha") public Result captcha() throws IOException { String key = UUID.randomUUID().toString(); String code = producer.createText(); key="112233"; //这里改成12345 code="12345"; ``` #### 5,菜单实体增加校验 ```java 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 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 children = new ArrayList<>(); } ``` #### 6,重启后台验证菜单功能 直接登录web,手动测试菜单功能项 ## 八:角色接口开发 #### 1,SysRoleController ```java 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 roleMenus = sysRoleMenuService.list(new QueryWrapper().eq("role_id", id)); List 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 pageData = sysRoleService.page(getPage(), new QueryWrapper() .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().in("role_id", ids)); sysRoleMenuService.remove(new QueryWrapper().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 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().eq("role_id", roleId)); sysRoleMenuService.saveBatch(sysRoleMenus); // 删除缓存 sysUserService.clearUserAuthorityInfoByRoleId(roleId); return Result.succ(menuIds); } } ``` #### 2,实体类增加校验 ```java 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 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 menuIds = new ArrayList<>(); } ``` #### 3,重启后台验证 重启后登录前端。对角色列表进行crud测试。 #### 碰到的问题: 数据库角色表主键又长又乱。 解决办法: ##### 方式一: 实体类的主键ID位置增加注解,效果: ```java @TableId(value="id",type= IdType.AUTO) private Long id; ``` ##### 方式二: application.yml文件中添加 ```yaml mybatis-plus: global-config: db-config: id-type: auto #id生成规则:数据库id自增 ``` #### 碰到的问题2: 新增的角色状态显示异常: ##### 解决办法: - com.huihui.vueadminapi.common.lang.Const ```java //修改常量值 public final static Integer STATUS_ON = 1; public final static Integer STATUS_OFF = 0; ``` ## 九:用户接口开发 #### 1,用户实体增加校验 ```java 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 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 sysRoles = new ArrayList<>(); } ``` #### 2,SysRoleService ```java 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 { List listRolesByUserId(Long userId); } ``` #### 3,SysRoleServiceImpl ```java 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 implements SysRoleService { @Override public List listRolesByUserId(Long userId) { List sysRoles = this.list(new QueryWrapper().inSql("id", "select role_id from sys_user_role where user_id = " + userId)); return sysRoles; } } ``` #### 4,SysUserController ```java 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 roles = sysRoleService.listRolesByUserId(id); sysUser.setSysRoles(roles); return Result.succ(roles); } @GetMapping("/list") @PreAuthorize("hasAuthority('sys:user:list')") public Result list(String username) { Page pageData = sysUserService.page(getPage(), new QueryWrapper().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().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 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().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()); } } ``` #### 5,重启后台验证 登录web,测试用户功能。 ## 十:细节修复 #### 1,修改密码 ##### 1.1新增实体类: ```java @Data public class PassDto implements Serializable { @NotBlank(message = "新密码不能为空") private String password; @NotBlank(message = "旧密码不能为空") private String currentPass; } ``` ##### 1.2,SysUserController新增方法 - com.huihui.vueadminapi.controller.SysUserController ```java @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()); } ``` #### 2,个人中心 1,前台新增个人中心 - /src/router/index.js ```vue { path: '/userCenter', name: 'UserCenter', meta: { title: "个人中心" }, //懒加载 component: () => import( '@/views/UserCenter.vue') }, ``` #### 3,token清空后退出 - /src/router/index.js ```js 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){ //此处省略... } ``` #### 4,axios修改 ```js import axios from "axios"; 修改为: import axios from "./axios"; ```