从零开始搭建一个项目骨架,最好选择合适熟悉的技术,并且在未来易拓展,适合微服务化体系等。所以一般以Springboot作为我们的框架基础,这是离不开的了。
然后数据层,我们常用的是Mybatis,易上手,方便维护。但是单表操作比较困难,特别是添加字段或减少字段的时候,比较繁琐,所以这里我推荐使用Mybatis Plus( mp.baomidou.com/ ),为简化开发而生,只需简单配置,即可快速进行CRUD操作,从而节省大量时间。
SpringSecurity,使用security作为我们的权限控制和会话控制的框架。
- SpringBoot
- mybatis plus
- spring security
- lombok
- redis
- hibernate validatior
- jwt
二、新建SpringBoot 项目,注意版本
1、新建SpringBoot工程
这里,我们使用IDEA来开发我们项目
开发工具与环境:
idea
mysql
jdk 8
maven3.3.9
新建SpringBoot
删除部分内容
2、整合MyBatis plus,生成代码
(1)引入依赖
com.baomidou
mybatis-plus-boot-starter
3.4.1
com.baomidou
mybatis-plus-generator
3.4.1
org.freemarker
freemarker
2.3.30
mysql
mysql-connector-java
runtime
复制代码
(2)设置配置文件
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/zhengadminvue?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml复制代码
新建一个包:通过@mapperScan注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。
@Configuration @ManagedBean("cn.itbluebox.springbootadminvue.mapper") public class MybatisPlusConfig {
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); //分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); //防止全表更新插件 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> configuration.setUseDeprecatedExecutor(false); }
}
复制代码
创建对应的mapper文件
(3)创建数据库和表
SQL语句
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`name` varchar(64) NOT NULL,
`path` varchar(255) DEFAULT NULL COMMENT '菜单URL',
`perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
`component` varchar(255) DEFAULT NULL,
`type` int(5) NOT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
`icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
`orderNum` int(11) DEFAULT NULL COMMENT '排序',
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`code` varchar(64) NOT NULL,
`remark` varchar(64) DEFAULT NULL COMMENT '备注',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`) USING BTREE,
UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL,
`menu_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL,
`email` varchar(64) DEFAULT NULL,
`city` varchar(64) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
`last_login` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4;复制代码
(4)代码生成
package cn.itbluebox.springbootadminvue; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {/** * <p> * 读取控制台内容 * </p> */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("itbluebox"); gc.setOpen(false); // gc.setSwagger2(true); 实体属性 Swagger2 注解 gc.setServiceName("%sService"); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/itzheng-vue-admin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("root"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig();
// pc.setModuleName(scanner("模块名"));
pc.setParent("cn.itbluebox.springbootadminvue");
mpg.setPackageInfo(pc);// 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); /* cfg.setFileCreate(new IFileCreate() { @Override public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { // 判断自定义文件夹是否需要创建 checkDir("调用默认方法创建的目录,自定义目录用"); if (fileType == FileType.MAPPER) { // 已经生成 mapper 文件判断存在,不想重新生成返回 false return !new File(filePath).exists(); } // 允许生成模板文件 return true; } }); */ cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); // 配置自定义输出模板 //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 // templateConfig.setEntity("templates/entity2.java"); // templateConfig.setService(); // templateConfig.setController(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setSuperEntityClass("BaseEntity"); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 公共父类 strategy.setSuperControllerClass("BaseController"); // 写于父类中的公共字段 strategy.setSuperEntityColumns("id", "created", "updated", "statu"); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true);
// strategy.setTablePrefix("sys_");//动态调整
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
复制代码
1、获取对应项目所有的表和字段的信息
2、新建一个freemarker的页面模板
3、提供相关需要进行渲染的动态数据
# 获取表
SELECT
*
FROM
information_schema. TABLES
WHERE
TABLE_SCHEMA = (SELECT DATABASE());复制代码
# 获取字段
SELECT
*
FROM
information_schema. COLUMNS
WHERE
TABLE_SCHEMA = (SELECT DATABASE())
AND TABLE_NAME = "sys_user";复制代码
sys_user_role,sys_user,sys_role_menu,sys_role,sys_menu复制代码
自动生成代码
我们发现实体类和controller报错缺少对应的Bese
创建BaseEntity
package cn.itbluebox.springbootadminvue.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;@Data
public class BaseEntity implements Serializable {@TableId(value = "id", type = IdType.AUTO) private Long id; private LocalDateTime created; private LocalDateTime updated; private Integer statu;
}
复制代码
注意每一个Controller的引入
(5)编写测试方法
/** *
* 前端控制器 *
* * @author itbluebox * @since 2022-05-26 */ @RestController @RequestMapping("/sys-user") public class SysUserController extends BaseController {@Autowired private SysUserService sysUserService; @GetMapping("list") public List<SysUser> getUserList(){ List<SysUser> list = sysUserService.list(new QueryWrapper<>(null)); return list; }
}
复制代码
在启动类上设置对应的mapper扫描
@SpringBootApplication @MapperScan("cn.itbluebox.springbootadminvue.mapper") public class SpringbootAdminvueApplication {
public static void main(String[] args) { SpringApplication.run(SpringbootAdminvueApplication.class, args); }
}
复制代码
启动项目
访问接口
http://localhost:8081/sys-user/list
访问成功
在数据库当中添加一些数据
刷新页面
三、结果封装
因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。
这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的
- 是否成功,可用code表示(如200表示成功,400表示异常)
- 结果消息
- 结果数据
package cn.itbluebox.springbootadminvue.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 success(Object data){ return success(200,"操作成功",data); } public static Result success(int code,String msg,Object data){ Result r = new Result(); r.setData(data); r.setMsg(msg); r.setCode(code); 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.setData(data); r.setMsg(msg); r.setCode(code); return r; }
}
复制代码
修改SysUserController
/** *
* 前端控制器 *
* @author itbluebox * @since 2022-05-26 */ @RestController @RequestMapping("/sys-user") public class SysUserController extends BaseController {@Autowired private SysUserService sysUserService; @GetMapping("list") public Result getUserList(){ List<SysUser> list = sysUserService.list(new QueryWrapper<>(null)); return Result.success(list); }
}
复制代码
http://localhost:8081/sys-user/list
四、全局异常处理
有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。
处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,
@ExceptionHandler(value = RuntimeException.class)复制代码
来指定捕获的Exception各个类型异常,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。
步骤二、定义全局异常处理,
@ControllerAdvice复制代码
表示定义全局控制器异常处理,
@ExceptionHandler复制代码
表示针对性异常处理,可对每种异常针对性处理。
/** * 全局异常处理 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.FORBIDDEN) @ExceptionHandler(value = AccessDeniedException.class) public Result handler(AccessDeniedException e) { log.info("security权限不足:----------------{}", e.getMessage()); return Result.fail("权限不足"); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result handler(MethodArgumentNotValidException e) { log.info("实体校验异常:----------------{}", e.getMessage()); BindingResult bindingResult = e.getBindingResult(); ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); 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); return Result.fail(e.getMessage()); }
}
复制代码
五、整合Spring Security
1、Spring Security介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
它提供了一组可以在Spring应用上下文中配置的Bean,
充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,
为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
流程说明:
客户端发起一个请求,进入 Security 过滤器链。
当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。
当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。
如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
2、引入Security与jwt
首先我们导入security包,因为我们前后端交互用户凭证用的是JWT,所以我们也导入jwt的相关包,然后因为验证码的存储需要用到redis,所以引入redis。最后为了一些工具类,我们引入hutool。
-
pom.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
复制代码
重新启动项目
访问: http://localhost:8081
用户名:user
密码:控制台已经输出
http://localhost:8081/sys-user/list
因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码:
application.yml
spring:
security:
user:
name: user
password: 111111复制代码
3、设置Redis的工具类
package cn.itbluebox.springbootadminvue.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;@Component
public class RedisUtil {@Autowired private RedisTemplate redisTemplate; /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } //============================String============================= /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } //================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } //============================set============================= /** * 根据key获取Set中的所有值 * * @param key 键 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } //===============================list================================= /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * * @param key 键 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } //================有序集合 sort set=================== /** * 有序set添加元素 * * @param key * @param value * @param score * @return */ public boolean zSet(String key, Object value, double score) { return redisTemplate.opsForZSet().add(key, value, score); } public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) { return redisTemplate.opsForZSet().add(key, typles); } public void zIncrementScore(String key, Object value, long delta) { redisTemplate.opsForZSet().incrementScore(key, value, delta); } public void zUnionAndStore(String key, Collection otherKeys, String destKey) { redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey); } /** * 获取zset数量 * @param key * @param value * @return */ public long getZsetScore(String key, Object value) { Double score = redisTemplate.opsForZSet().score(key, value); if(score==null){ return 0; }else{ return score.longValue(); } } /** * 获取有序集 key 中成员 member 的排名 。 * 其中有序集成员按 score 值递减 (从大到小) 排序。 * @param key * @param start * @param end * @return */ public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) { return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end); }
}
复制代码
4、设置RedisConfig
package cn.itbluebox.springbootadminvue.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来返回我们特定格式的封装结果数据。
1、生成验证码
首先我们先生成验证码,之前我们已经引用了google的验证码生成器,我们先来配置一下图片验证码的生成规则:
KaptchaConfig
package cn.itbluebox.springbootadminvue.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Properties;
@Configuration
public class KaptchaConfig {@Bean public DefaultKaptcha producer() { Properties properties = new Properties(); properties.put("kaptcha.border", "no"); properties.put("kaptcha.textproducer.font.color", "black"); properties.put("kaptcha.textproducer.char.space", "4"); properties.put("kaptcha.image.height", "40"); properties.put("kaptcha.image.width", "120"); properties.put("kaptcha.textproducer.font.size", "30"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; }
}
复制代码
package cn.itbluebox.springbootadminvue.common.lang;
public class Const {
public final static String CAPTCHA_KEY = "captcha";
}
复制代码
package cn.itbluebox.springbootadminvue.controller;
import cn.hutool.core.map.MapUtil;
import cn.itbluebox.springbootadminvue.common.lang.Const;
import cn.itbluebox.springbootadminvue.common.lang.Result;
import com.google.code.kaptcha.Producer;
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.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.UUID;@RestController
public class AuthController extends BaseController {@Autowired Producer producer; @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.success( MapUtil.builder() .put("token",key) .put("captchaImg",base64Img) .build() ); }
}
复制代码
注意在上面的BaseController 当中添加一些新内容
public class BaseController {
@Autowired HttpServletRequest req; @Autowired RedisUtil redisUtil;
}
复制代码
启动
先启动Redis
启动项目
2、前端实现验证码显示
启动前端项目
去除moke
3、解决跨域问题
@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);
}
}
复制代码
4、设置过滤器
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] URL_WHITELIST = { "/login", "/logout", "/captcha", "/favicon.ico", }; protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() //登录配置 .formLogin()
/* .successHandler()
.failureHandler()
*/
//禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
//异常处理器
//配置自定义的过滤器
;
}
}
复制代码
重新启动项目
刷新页面
@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("用户名或密码错误"); outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8")); outputStream.flush(); outputStream.close(); }
}
复制代码
@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(); //生成jwt 。 并放置到请求头中 Result result = Result.success("成功"); outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8")); outputStream.flush(); outputStream.close(); }
}
复制代码
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private LoginFailureHandler loginFailureHandler; @Autowired private LoginSuccessHandler loginSuccessHandler; private static final String[] URL_WHITELIST = { "/login", "/logout", "/captcha", "/favicon.ico", }; protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() //登录配置 .formLogin() .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) //禁用session .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //配置拦截规则 .and() .authorizeRequests() .antMatchers(URL_WHITELIST).permitAll() .anyRequest().authenticated() //异常处理器 //配置自定义的过滤器 ; }
}
复制代码
刷新页面
5、设置点击刷新二维码
复制代码
设置点击后清空对应的内容
6、设置验证码过滤器
(1)设置验证码错误异常
public class CaptchaException extends AuthenticationException {
public CaptchaException(String msg) { super(msg); }
}
复制代码
(2)验证码过滤器
package cn.itbluebox.springbootadminvue.security;
import cn.itbluebox.springbootadminvue.common.exception.CaptchaException;
import cn.itbluebox.springbootadminvue.common.lang.Const;
import cn.itbluebox.springbootadminvue.utils.RedisUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
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;@Component
public class CaptchaFilter extends OncePerRequestFilter {@Autowired private RedisUtil redisUtil; @Autowired private LoginFailureHandler loginFailureHandler; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String url = request.getRequestURI(); if("/login".equals(url) && request.getMethod().equals("POST") ){ try{ //校验验证码 validate(request); //如果不正确,就跳转到认证失败处理器 }catch (CaptchaException e){ //交给失败的处理器(认证失败处理器) loginFailureHandler.onAuthenticationFailure(request,response,e); } } filterChain.doFilter(request,response); } //校验逻辑 private void validate(HttpServletRequest request) { String code = request.getParameter("code"); String key = request.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); }
}
复制代码
7、配置过滤器
//异常处理器
//配置自定义的过滤器
.and()
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);复制代码
七、完成登录并生成JWT
登录成功之后前端就可以获取到了jwt的信息,
前端中我们是保存在了store中,
同时也保存在了localStorage中,
然后每次axios请求之前,
我们都会添加上我们的请求头信息,可以回顾一下。
1、编写JwtUtils
package cn.itbluebox.springbootadminvue.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;
@Data
@Component
@ConfigurationProperties(prefix = "itbluebox.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.ES512,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()); }
}
复制代码
2、编写Jwt对应的配置文件
itbluebox:
jwt:
header: Authorization
expire: 604800 #7天,秒单位
secret: 212wdseqw23red232r3rds23r21212hg #填够32位复制代码
八、身份认证 - 1
登录成功之后前端就可以获取到了jwt的信息,前端中我们是保存在了store中,同时也保存在了localStorage中,然后每次axios请求之前,我们都会添加上我们的请求头信息
所以后端进行用户身份识别的时候,我们需要通过请求头中获取jwt,然后解析出我们的用户名,这样我们就可以知道是谁在访问我们的接口啦,然后判断用户是否有权限等操作。
那么我们自定义一个过滤器用来进行识别jwt。
1、JwtAuthenticationFilter
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired private JwtUtils jwtUtils; 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(ObjectUtils.isEmpty(claim)){ 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
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private LoginFailureHandler loginFailureHandler; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired CaptchaFilter captchaFilter; @Bean JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()); return jwtAuthenticationFilter; } private static final String[] URL_WHITELIST = { "/login", "/logout", "/captcha", "/favicon.ico", }; protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() //登录配置 .formLogin() .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) //禁用session .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //配置拦截规则 .and() .authorizeRequests() .antMatchers(URL_WHITELIST).permitAll() .anyRequest().authenticated() //异常处理器 //配置自定义的过滤器 .and() .addFilter(jwtAuthenticationFilter()) .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) ; }
}
复制代码
3、发起请求测试
http://localhost:8081/sys-user/list
九、用户认证失败或权限不足异常处理
1、认证失败处理器
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { }
}
复制代码
2、异常处理器
@Component public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { }
}
复制代码
3、SecurityConfig当中
@Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired JwtAccessDeniedHandler jwtAccessDeniedHandler;
复制代码
//异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)复制代码
4、完善JwtAccessDeniedHandler和JwtAuthenticationEntryPoint
(1)JwtAccessDeniedHandler
@Component public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override public void handle(HttpServletRequest request, 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(); }
}
复制代码
(2)JwtAuthenticationEntryPoint
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override public void commence(HttpServletRequest request, 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(); }
}
复制代码
5、内容测试
向接口发送请求: http://localhost:8081/sys-user/list
6、用户登录查库
UserDetailServiceImpl
SysUser sysUser = sysUserService.getByUserName(username);复制代码
public interface SysUserService extends IService
{ SysUser getByUserName(String username);
}
复制代码
@Service public class SysUserServiceImpl extends ServiceImpl
implements SysUserService { @Override public SysUser getByUserName(String username) { return getOne(new QueryWrapper<SysUser>().eq("username",username)); }
}
复制代码
package cn.itbluebox.springbootadminvue.security; import cn.hutool.core.lang.Assert; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection;
public class AccountUser implements UserDetails {
private Long userId; private String password; private final String username; private final Collection<? extends GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) { this(userId, username, password, true, true, true, true, authorities); } public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor"); this.userId = userId; this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return this.accountNonExpired; } @Override public boolean isAccountNonLocked() { return this.accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return this.credentialsNonExpired; } @Override public boolean isEnabled() { return this.enabled; }
}
复制代码
完善SecurityConfig
@Bean BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder(); }
复制代码
完善UserDetailServiceImpl
@Service public class UserDetailServiceImpl implements UserDetailsService {
@Autowired private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserService.getByUserName(username); if(ObjectUtils.isEmpty(sysUser)){ throw new UsernameNotFoundException("用户名或密码不正确"); } return new AccountUser(sysUser.getId(),sysUser.getUsername(),sysUser.getPassword(),getUserAuthority(sysUser.getId())); } /* * 获取用户权限信息(角色,菜单权限) * */ public List<GrantedAuthority> getUserAuthority(Long userId){ return null; }
}
复制代码
完善SecurityConfig
@Autowired
UserDetailServiceImpl userDetailService;复制代码
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailService); }
复制代码
@RestController @RequestMapping("/sys-user") public class SysUserController extends BaseController {
@Autowired private SysUserService sysUserService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @GetMapping("list") public Result getUserList(){ List<SysUser> list = sysUserService.list(new QueryWrapper<>(null)); return Result.success(list); } @GetMapping("list/pass") public Result pass(){ //加密后的密码 String password = bCryptPasswordEncoder.encode("111111"); boolean matches = bCryptPasswordEncoder.matches("111111", password); System.out.println("匹配结果:"+matches); return Result.success(password); }
}
复制代码
编写一个测试方法生成一下密码
@SpringBootTest class SpringbootAdminvueApplicationTests {
@Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Test void contextLoads() { String password = bCryptPasswordEncoder.encode("111111"); boolean matches = bCryptPasswordEncoder.matches("111111", password); System.out.println("匹配结果:"+matches); System.out.println(password); }
}
复制代码
在数据库当中添加对应的账号和密码
将配置文件当中SpringSecurity的内容注释掉
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/itzheng-vue-admin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
# security:
# user:
# name: user
# password: 111111
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
itbluebox:
jwt:
header: Authorization
expire: 604800 #7天,秒单位
secret: 212wdseqw23red232r3rds23r21212hg #填够32位复制代码
发送登录请求
7、用户授权
然后关于权限部分,也是security的重要功能,当用户认证成功之后,我们就知道谁在访问系统接口,这是又有一个问题,就是这个用户有没有权限来访问我们这个接口呢,要解决这个问题,我们需要知道用户有哪些权限,哪些角色,这样security才能我们做权限判断。
之前我们已经定义及几张表,用户、角色、菜单、以及一些关联表,一般当权限粒度比较细的时候,我们都通过判断用户有没有此菜单或操作的权限,而不是通过角色判断,而用户和菜单是不直接做关联的,是通过用户拥有哪些角色,然后角色拥有哪些菜单权限这样来获得的。
问题1:我们是在哪里赋予用户权限的?有两个地方:
- 1、用户登录,调用调用UserDetailsService.loadUserByUsername()方法时候可以返回用户的权限信息。
- 2、接口调用进行身份认证过滤器时候JWTAuthenticationFilter,需要返回用户权限信息
问题2:在哪里决定什么接口需要什么权限?
Security内置的权限注解:
- @PreAuthorize:方法执行前进行权限检查
- @PostAuthorize:方法执行后进行权限检查
- @Secured:类似于@PreAuthorize
可以在Controller的方法前添加这些注解表示接口需要什么权限。
@RestController @RequestMapping("/sys-user") public class SysUserController extends BaseController {
@Autowired private SysUserService sysUserService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; //合作权限拥有admin的才能访问 @PreAuthorize("hasRole('admin')") @GetMapping("list") public Result getUserList(){ List<SysUser> list = sysUserService.list(new QueryWrapper<>(null)); return Result.success(list); } //普通用户、超级管理员 //当前方法只有拥有sys:user:list的权限的管理员才能访问方法 @PreAuthorize("hasAnyAuthority('sys:user:list')") @GetMapping("list/pass") public Result pass(){ //加密后的密码 String password = bCryptPasswordEncoder.encode("111111"); boolean matches = bCryptPasswordEncoder.matches("111111", password); System.out.println("匹配结果:"+matches); return Result.success(password); }
}
复制代码
8、完善权限方法
/* * 获取用户权限信息(角色,菜单权限) * */ 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); }
复制代码
String getUserAuthorityInfo(Long userId);复制代码
在SysUserServiceImpl当中
@Service public class SysUserServiceImpl extends ServiceImpl
implements SysUserService { @Autowired private SysRoleService sysUserService; @Autowired private SysUserMapper sysUserMapper; @Override public SysUser getByUserName(String username) { return getOne(new QueryWrapper<SysUser>().eq("username",username)); } @Override public String getUserAuthorityInfo(Long userId) { //通过用户id获取对应的角色信息 String authority = null; //获取角色 //通过用户id,查出对应的用户角色id,通过角色id查询,对应用户的角色信息 List<SysRole> roles = sysUserService.list(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + userId)); if(roles.size() > 0){ String roleCode = roles.stream().map(r -> "ROLE_"+r.getCode()).collect(Collectors.joining(",")); authority = roleCode; } //获取菜单操作权限 List<Long> menuIds = sysUserMapper.getNavMenuIds(userId); return null; }
}
复制代码
<select id="getNavMenuIds" resultType="java.lang.Long"> select DISTINCT rm.menu_id from sys_user_role ur left join sys_role_menu rm on ur.role_id = rm.role_id where ur.user_id = #{userId} </select>
复制代码
完善SysUserServiceImpl
/** *
* 服务实现类 *
* @author itbluebox * @since 2022-05-26 */ @Service public class SysUserServiceImpl extends ServiceImplimplements SysUserService { @Autowired private SysRoleService sysUserService; @Autowired private SysUserMapper sysUserMapper; @Autowired private SysMenuService sysMenuService; @Override public SysUser getByUserName(String username) { return getOne(new QueryWrapper<SysUser>().eq("username",username)); } @Override public String getUserAuthorityInfo(Long userId) { //通过用户id获取对应的角色信息 String authority = null; //获取角色编码 //通过用户id,查出对应的用户角色id,通过角色id查询,对应用户的角色信息 List<SysRole> roles = sysUserService.list(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + userId)); if(roles.size() > 0){ String roleCode = roles.stream().map(r -> "ROLE_"+r.getCode()).collect(Collectors.joining(",")); authority = roleCode.concat(","); } //获取菜单操作权限 List<Long> menuIds = sysUserMapper.getNavMenuIds(userId); if(menuIds.size() > 0){ List<SysMenu> sysMenus = sysMenuService.listByIds(menuIds); String menuPerms = sysMenus.stream().map(m -> m.getPerms()).collect(Collectors.joining(",")); authority = authority.concat(menuPerms); } return authority; }
}
复制代码
完善JwtAuthenticationFilter
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired private JwtUtils jwtUtils; @Autowired private UserDetailServiceImpl userDetailService; @Autowired private 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(ObjectUtils.isEmpty(claim)){ throw new JwtException("token 异常"); } if(jwtUtils.isTokenExpired(claim)){ throw new JwtException("token已经过期"); } String username = claim.getSubject(); SysUser sysUser = sysUserService.getByUserName(username); //获取用户的权限信息 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,null,userDetailService.getUserAuthority(sysUser.getId())); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request,response); }
}
复制代码
9、测试运行
http://localhost:8081/captcha
发起获取验证码请求
发起登录请求
http://localhost:8081/login
复制token
粘贴到回去信息的header当中
发起获取信息请求: http://localhost:8081/sys-user/list