spring-boot+mybatis-plus+spring-security+jwt+OAuth+redis认证授权
1.首先了解oauth是什么东西,具体是用来干嘛的。
OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。
本文对OAuth 2.0的设计思路和运行流程,做一个简明通俗的解释。http://en.wikipedia.org/wiki/OAuth
网上看到写的很好的博客:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
2.搭建一个spring-boot工程
1.我的目录结构如下
一个个的操作的话篇幅太多,不使用作者这种,一个spring-boot工程也是一样的。
2.添加必要的依赖
mydome的maven包,一般创建springboot项目时会自动帮我们生成的
<?xml version="1.0" encoding="UTF-8"?>xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 pom com-dome-provider com-dome-model com-dome-common com-dome-biz org.springframework.boot spring-boot-starter-parent 2.4.0 com demo 0.0.1-SNAPSHOT com Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-maven-plugin 2.4.0 com.dome.provider.App build-info maven-compiler-plugin 1.8 1.8 UTF-8
com-dome-model模块的maven
<?xml version="1.0" encoding="UTF-8"?>xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> demo com 0.0.1-SNAPSHOT 4.0.0 com-dome-model io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java org.projectlombok lombok org.springframework.boot spring-boot-starter-validation com.baomidou mybatis-plus-generator 3.3.0 org.apache.velocity velocity-engine-core 2.1 com.baomidou mybatis-plus-boot-starter 3.1.0 com.baomidou mybatis-plus-extension 3.1.0
com-dome-common模块
<?xml version="1.0" encoding="UTF-8"?>xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> demo com 0.0.1-SNAPSHOT 4.0.0 com-dome-common com com-dome-model 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis io.jsonwebtoken jjwt 0.9.1 org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-aop eu.bitwalker UserAgentUtils 1.21 com.github.oshi oshi-core 4.4.2 com.alibaba fastjson 1.2.71 com.github.axet kaptcha 0.0.9 org.apache.poi poi 3.9 org.apache.poi poi-ooxml 3.9 org.projectlombok lombok io.swagger swagger-annotations 1.5.20 compile jakarta.validation jakarta.validation-api com.google.guava guava 20.0 compile
com-dome-biz模块
<?xml version="1.0" encoding="UTF-8"?>xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> demo com 0.0.1-SNAPSHOT 4.0.0 com-dome-biz com com-dome-model 0.0.1-SNAPSHOT com com-dome-common 0.0.1-SNAPSHOT src/main/resources true src/main/java **/*.xml
com-dome-privoder模块
<?xml version="1.0" encoding="UTF-8"?>xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> demo com 0.0.1-SNAPSHOT 4.0.0 com-dome-provider com com-dome-model 0.0.1-SNAPSHOT com com-dome-common 0.0.1-SNAPSHOT com com-dome-biz 0.0.1-SNAPSHOT org.springframework.boot spring-boot-devtools org.springframework.boot spring-boot-starter-security com.baomidou mybatis-plus-extension 3.3.0 compile com.baomidou mybatis-plus-extension 3.3.0 compile com.baomidou mybatis-plus-extension 3.1.0 org.springframework.boot spring-boot-maven-plugin com.dome.provider.App com-dome-provider org.apache.maven.plugins maven-surefire-plugin 2.21.0 true
添加一个有趣的banner.txt,在com-dome-provider的resources中
// _ooOoo_ // // o8888888o // // 88" . "88 // // (| ^_^ |) // // O\ = /O // // ____/`---'\____ // // .' \\| |// `. // // / \\||| : |||// \ // // / _||||| -:- |||||- \ // // | | \\\ - /// | | // // | \_| ''\---/'' | | // // \ .-\__ `-` ___/-. / // // ___`. .' /--.--\ `. . ___ // // ."" '< `.___\_<|>_/___.' >'"". // // | | : `- \`.;`\ _ /`;.`/ - ` : | | // // \ \ `-. \_ __\ /__ _/ .-` / / // // ========`-.____`-.___\_____/___.-`____.-'======== // // `=---=' // // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // // 佛祖保佑 永不宕机 永无BUG //
项目大致的结构已经搭建完成
3.添加一下工具类和配置信息
在@SpringBootApplication加上要扫描的包目录和@MapperScan指向存放dao的目录,添加yml配置。
server: port: 8080 spring: #时间戳统一转换 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 #数据库配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/sprin_security?useSSL=false&useUnicode=true&nullCatalogMeansCurrent=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&maxReconnects=10 password: yourpass username: root hikari: minimum-idle: 1 maximum-pool-size: 10 redis: host: 127.0.0.1 port: 6379 password: yourpass jedis: pool: # 连接池最大连接数(使用负值表示没有限制) max-active: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: 10000ms # 连接池中的最大空闲连接 max-idle: 8 # 连接池中的最小空闲连接 min-idle: 0 # 连接超时时间(毫秒) timeout: 10000ms servlet: multipart: # spring-boot 最大上传文件大小 max-file-size: 30MB max-request-size: 30MB aop: auto: true mybatis-plus: configuration: #配置日志 默认控制台输出 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.dome.model.po jwt: tokenHeader: Authorization secret: iwqjhda8232bjgh432 #JWT的超期限时间(60*60*24*7) expiration: 604800 tokenHead: 'Bearer '
4.数据库
作者使用的是MySQL5.7的数据库,用完这些就可以启动了。
/* Navicat Premium Data Transfer Source Server : 阿里云 Source Server Type : MySQL Source Server Version : 50724 Source Host : 127.0.0.1:3306 Source Schema : sprin_security Target Server Type : MySQL Target Server Version : 50724 File Encoding : 65001 Date: 30/11/2020 16:01:47 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sys_menu -- ---------------------------- DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_menu` ( `id` bigint(36) NOT NULL AUTO_INCREMENT, `type` tinyint(2) NULL DEFAULT 1 COMMENT '菜单类型 1 = 1级 2 = 2级.......', `parent` bigint(36) NULL DEFAULT 0 COMMENT '父级菜单id', `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单名称', `url` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '后台生成URL', `icon` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '菜单图标icon', `permission` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限 sys:user:query = 系统管理:用户管理:查询', `sort` bigint(36) NULL DEFAULT NULL COMMENT '排序', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', `create_name` bigint(36) NULL DEFAULT NULL COMMENT '创建人', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `update_name` bigint(36) NULL DEFAULT NULL COMMENT '修改人', `version` bigint(36) NULL DEFAULT 1 COMMENT '版本号', `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除0 = 存在 1 = 删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sys_menu -- ---------------------------- INSERT INTO `sys_menu` VALUES (1, 1, 0, '系统管理', '/sys', NULL, 'sys', NULL, '2020-11-19 15:36:32', 1, '2020-11-19 15:38:16', 1, 1, 0); INSERT INTO `sys_menu` VALUES (2, 2, 1, '用户管理', '/sys/user', NULL, 'sys:user', NULL, '2020-11-19 15:36:56', 1, '2020-11-19 15:38:16', 1, 1, 0); INSERT INTO `sys_menu` VALUES (3, 3, 2, '新增用户', '/sys/user/add', NULL, 'sys:user:add', NULL, '2020-11-19 15:37:30', 1, '2020-11-19 15:38:16', 1, 1, 0); INSERT INTO `sys_menu` VALUES (4, 3, 2, '查询', '/sys/user/query', NULL, 'sys:user:query', NULL, '2020-11-19 15:38:10', 1, '2020-11-19 15:38:16', 1, 1, 0); INSERT INTO `sys_menu` VALUES (6, 3, 2, '删除', '/sys/user/del', NULL, 'sys:user:del', NULL, '2020-11-19 15:38:43', 1, '2020-11-19 15:38:43', 1, 1, 0); INSERT INTO `sys_menu` VALUES (7, 3, 2, '详情', '/sys/user/findById', NULL, 'sys:user:findById', NULL, '2020-11-27 09:48:43', NULL, '2020-11-27 09:48:59', NULL, 1, 0); -- ---------------------------- -- Table structure for sys_role -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` bigint(36) NOT NULL AUTO_INCREMENT, `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色编码', `name` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '角色名称', `role_describe` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述', `status` tinyint(1) NULL DEFAULT 0 COMMENT '角色状态0 = 启用 = 禁用', `create_name` bigint(36) NULL DEFAULT NULL COMMENT '创建人', `update_name` bigint(36) NULL DEFAULT NULL COMMENT '修改人', `version` bigint(36) NULL DEFAULT 1 COMMENT '版本号', `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除0 = 存在 1 = 删除', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sys_role -- ---------------------------- INSERT INTO `sys_role` VALUES (1, 'gly', '管理员', '系统管理员', 0, 1, 1, 1, 0, '2020-11-19 15:35:33', '2020-11-19 15:35:33'); -- ---------------------------- -- Table structure for sys_role_menu -- ---------------------------- DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_role_menu` ( `id` bigint(36) NOT NULL AUTO_INCREMENT, `role_id` bigint(20) NULL DEFAULT NULL COMMENT '角色id', `menu_id` bigint(20) NULL DEFAULT NULL COMMENT '菜单id', `create_name` bigint(255) NULL DEFAULT NULL COMMENT '创建人', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色菜单中间表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sys_role_menu -- ---------------------------- INSERT INTO `sys_role_menu` VALUES (1, 1, 1, 1, '2020-11-19 15:40:05'); INSERT INTO `sys_role_menu` VALUES (2, 1, 2, 1, '2020-11-19 15:40:07'); INSERT INTO `sys_role_menu` VALUES (3, 1, 3, 1, '2020-11-19 15:40:11'); INSERT INTO `sys_role_menu` VALUES (4, 1, 4, 1, '2020-11-19 15:40:12'); INSERT INTO `sys_role_menu` VALUES (5, 1, 6, 1, '2020-11-19 15:40:13'); INSERT INTO `sys_role_menu` VALUES (7, 1, 7, 1, '2020-11-30 15:06:04'); -- ---------------------------- -- Table structure for sys_user -- ---------------------------- DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` bigint(36) NOT NULL AUTO_INCREMENT, `user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', `nick_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户昵称', `phone` decimal(12, 0) NULL DEFAULT NULL COMMENT '手机号', `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱', `status` tinyint(1) NULL DEFAULT NULL COMMENT '用户状态 0 = 启用 1 = 禁用', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', `create_name` bigint(36) NULL DEFAULT NULL COMMENT '创建人id', `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', `update_name` bigint(36) NULL DEFAULT NULL COMMENT '修改人id', `version` bigint(36) NULL DEFAULT 1 COMMENT '版本号 ', `del_flag` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除0 = 存在 1 = 删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sys_user -- ---------------------------- INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$Fu0pt5Mf1ggTgVXM32GHmuBWntgp8a1WyVEgufwzuzlgoXT.Ou7aK', '管理员', 1111111111, '1111@qq.com', 0, '2020-11-20 15:02:31', 1, '2020-11-27 11:24:39', 1, 1, 0); -- ---------------------------- -- Table structure for sys_user_role -- ---------------------------- DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `id` bigint(36) NOT NULL AUTO_INCREMENT, `user_id` bigint(36) NULL DEFAULT NULL COMMENT '用户id', `role_id` bigint(36) NULL DEFAULT NULL COMMENT '角色id', `create_name` bigint(255) NULL DEFAULT NULL COMMENT '创建人', `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色中间表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sys_user_role -- ---------------------------- INSERT INTO `sys_user_role` VALUES (1, 1, 1, 1, '2020-11-19 15:41:40'); SET FOREIGN_KEY_CHECKS = 1;
3.用到的一些工具类和文件
mybatis-plus的配置和代码生成配置:https://www.cnblogs.com/yiMro/p/13529094.html
jwt工具类:https://www.cnblogs.com/yiMro/p/14061996.html
全局异常捕获:https://www.cnblogs.com/yiMro/p/13672652.html
redis工具:https://www.cnblogs.com/yiMro/p/13529150.html
客服端硬盘号工具
package com.dome.common.utils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * @author YinXiaoWei * @date 2020/11/25 16:00 * 获取客户端硬盘号 */ public class HdSerialInfoUtil { public static String getHdSerialInfo() { String line = ""; //定义变量 硬盘序列号 String HdSerial = ""; try { //获取命令行参数 Process proces = Runtime.getRuntime().exec("cmd /c dir c:"); BufferedReader buffreader = new BufferedReader(new InputStreamReader(proces.getInputStream(),"gbk")); while ((line = buffreader.readLine()) != null) { //读取参数并获取硬盘序列号 if (line.indexOf("卷的序列号是 ") != -1) { HdSerial = line.substring(line.indexOf("卷的序列号是 ") + "卷的序列号是 ".length(), line.length()); break; } } } catch (IOException e) { e.printStackTrace(); } return HdSerial; } }
客服端ip工具
package com.dome.common.utils; import eu.bitwalker.useragentutils.Browser; import eu.bitwalker.useragentutils.UserAgent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; /** * @author YinXiaoWei * @date 2020/11/25 15:34 * ip工具类 */ public class IpUtil { private static final Logger logger = LoggerFactory.getLogger(IpUtil.class); /** * 获取客户端IP地址 * @param request * @return */ public static String getIpAddr(HttpServletRequest request) { String ipAddress = null; try { ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if (ipAddress.equals("127.0.0.1")) { // 根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { logger.error("获取用户的主机发生异常",e); } ipAddress = inet.getHostAddress(); } } // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 ***.***.***.***".length() int ipAddressMax = 15; if (ipAddress != null && ipAddress.length() > ipAddressMax) { // = 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } } catch (Exception e) { ipAddress=""; logger.error("获取用户的ip地址发生异常",e); } //服务端和客户端在一台机器 if(ipAddress.toString().contains( "0:0:0:0:0:0:0:1")) { ipAddress = "127.0.0.1"; } return ipAddress; } /** * 获取浏览器信息 * @param request * @return */ public static String getBrowser(HttpServletRequest request){ String ua = request.getHeader("User-Agent"); //转成UserAgent对象 UserAgent userAgent = UserAgent.parseUserAgentString(ua); Browser browser = userAgent.getBrowser(); //浏览器名 return browser.toString(); } }
验证码生成器
package com.dome.provider.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; /** * @author YinXiaoWei * @date 2020/11/25 15:55 * 图片验证码配置 */ @Configuration public class KaptchaConfig { @Bean public DefaultKaptcha producer(){ Properties p =new Properties(); //是否有边框 p.put("kaptcha.border","no"); //字体颜色 p.put("kaptcha.textproducer.font.color","black"); //图片宽度 p.put("kaptcha.image.width","135"); //使用哪些字符生成验证码 p.put("kaptcha.textproducer.char.string","ABCDEFHKIJZPYRSTWXabcdefhkijzprstwx0123456789"); //图片高度 p.put("kaptcha.image.height","50"); //字体大小 p.put("kaptcha.textproducer.font.size","43"); //字体颜色 p.put("kaptcha.noise.color","blue"); //字符的个数 p.put("kaptcha.textproducer.char.length","4"); //字体 p.put("kaptcha.textproducer.font.names","Arial"); Config config =new Config(p); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
package com.dome.common.utils; import com.google.code.kaptcha.Producer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.IOException; /** * @author YinXiaoWei * @date 2020/11/25 15:43 * 验证码图片的工具类 */ @Component public class VerifyCodeUtil { private static final String AUTH_NAME = "AUTH_NAME"; @Autowired private Producer producer; @Autowired private RedisUtil redisUtil; /** * 生成验证码 * @return */ public String getAuthCode() { String id = HdSerialInfoUtil.getHdSerialInfo(); if (redisUtil.hasKey(AUTH_NAME + id)) { redisUtil.delKey(AUTH_NAME + id); } String code = producer.createText().toLowerCase(); redisUtil.setObject(AUTH_NAME + id, code); return code; } /** * 校验验证码 * @param code * @return */ public boolean verifyAuthCode(String code) { String authCode = code.toLowerCase(); String id = HdSerialInfoUtil.getHdSerialInfo(); if (!redisUtil.hasKey(AUTH_NAME + id)) { return false; } if (!redisUtil.getObject(AUTH_NAME + id).toString().trim().equals(authCode)) { return false; } return true; } /** * 返回图片 io流 * @param response * @param code */ public void backImg(HttpServletResponse response, String code) { BufferedImage img = producer.createImage(code); ServletOutputStream outputStream = null; try { outputStream = response.getOutputStream(); ImageIO.write(img,"jpg", outputStream); } catch (IOException e) { throw new RuntimeException("生成图片验证码错误!"); } } }
在resources添加logback.xml文件用于生成日志文件
<?xml version="1.0" encoding="UTF-8"?>"false"> "LOG_HOME" value="/Logs"/> "STDOUT" class="ch.qos.logback.core.ConsoleAppender"> class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n "FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> ${LOG_HOME}/com-dome-provider.log.%d{yyyy-MM-dd}.log 30 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> 10MB "ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> ${LOG_HOME}/com-dome-provider-error-log.%d{yyyy-MM-dd}.log 60 class="ch.qos.logback.classic.filter.ThresholdFilter"> WARN %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n "org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/> "org.hibernate.type.descriptor.sql.BasicExtractor" level="DEBUG"/> "org.hibernate.SQL" level="DEBUG"/> "org.hibernate.engine.QueryParameters" level="DEBUG"/> "org.hibernate.engine.query.HQLQueryPlan" level="DEBUG"/> "com.apache.ibatis" level="TRACE"/> "java.sql.Connection" level="DEBUG"/> "java.sql.Statement" level="DEBUG"/> "java.sql.PreparedStatement" level="DEBUG"/> "com.bool.order.meeting.core.mapper" level="DEBUG"> "INFO"> ref ref="STDOUT"/> ref ref="FILE"/> ref ref="ERROR_FILE" />
4.添加controller、service、mapper层级代码代码
添加各个层级的代码,建议自己手扣好一点。
修改字段生成的代码:
1.实体类中跟数据库tinyint位数为一的字段会是布尔类型改为Integer类型。
2.在时间字段上加上@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")==>用于处理时间格式
3.controller类上加上@Slf4j==>日志
@RequiredArgsConstructor(onConstructor = @__(@Autowired))==>可使用 private final RoleService roleService;(final)的方式注入
@Api(value = "系统管理 角色管理", tags = "系统管理 角色管理")==>swagger注解
4.@MapperScan和yml中mybatis-plus下的mapper-lications、type-aliases-package一定要指向正确,不然就会出现xxx找不到的问题。
5.如果确定配置没有写错还是出现xxx找不到的问题(作者是自动生成的所以不存在层级关系和代码名称的问题),那看一下xml文件的位置,如果不在resources下springboot是没有办法识别 的,在maven添加
src/main/resources true src/main/java **/*.xml
6.写一个hellWord接口测试访问(security的maven依赖注释掉,不然就会跳转到登录页面。用户名:user 密码:项目启动在控制台中生成)
5.security认证授权
Swagger
相信无论是前端还是后端开发,都或多或少地被接口文档折磨过。前端经常抱怨后端给的接口文档与实际情况不一致。后端又觉得编写及维护接口文档会耗费不少精力,经常来不及更新。其实无论是前端调用后端,还是后端调用后端,都期望有一个好的接口文档。但是这个接口文档对于程序员来说,就跟注释一样,经常会抱怨别人写的代码没有写注释,然而自己写起代码起来,最讨厌的,也是写注释。所以仅仅只通过强制来规范大家是不够的,随着时间推移,版本迭代,接口文档往往很容易就跟不上代码了。 swagger详情介绍:https://www.jianshu.com/p/349e130e40d5import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.ParameterBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.schema.ModelRef; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.service.Parameter; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; import java.util.List; /** * @author YinXiaoWei * @date 2020/11/24 14:26 * swagger */ @Configuration @EnableSwagger2 @EnableKnife4j public class SwaggerConfig { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) .paths(PathSelectors.any()) .build() .globalOperationParameters(getParameterList()); } private ApiInfo apiInfo() { Contact contact = new Contact("spring-security", "http://www.spring-security.com/", "15289@qq.com"); return new ApiInfoBuilder() .title("com.dome.spring") .description("前端API接口") .contact(contact) .version("1.0.0") .build(); } private ListgetParameterList(){ List parameters = new ArrayList<>(); parameters.add(new ParameterBuilder() .name(this.tokenHeader) .description("令牌") .defaultValue(this.tokenHead) .modelRef(new ModelRef("string")) .parameterType("header") .required(false) .build()); return parameters; } }
推荐Knife4j,在swagger-ui的基础上导入maven依赖在到SwaggerConfig文件上加上@EnableKnife4j注解就可以访问很好看的Knife4j页面了。
Knife4j访问地址:http://ip:host/doc.html#/home swagger-ui访问地址:http://ip:host/swagger-ui.html
com.github.xiaoymin knife4j-spring-boot-starter 2.0.4
认证授权
根据自己的要求自定义UserDetails实体类
package com.dome.provider.config.security.dto; import com.dome.model.po.role.Role; import com.dome.model.po.user.User; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.List; import java.util.stream.Collectors; /** * @author YinXiaoWei * @date 2020/11/26 9:33 */ @Data public class JwtUserDto implements UserDetails { /** * 用户数据 */ private User myUser; private ListroleInfo; /** * 用户权限的集合 */ @JsonIgnore private List authorities; public List getRoles() { return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); } /** * 加密后的密码 * @return */ @Override public String getPassword() { return myUser.getPassword(); } /** * 用户名 * @return */ @Override public String getUsername() { return myUser.getUserName(); } /** * 是否过期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 是否锁定 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 凭证是否过期 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 * @return */ @Override public boolean isEnabled() { return myUser.getStatus() == 0 ? true : false; } public JwtUserDto(User myUser, List authorities) { this.myUser = myUser; this.authorities = authorities; } }
添加需要用到的dao方法,附上MenuMapper.xml代码和VO类,因为集成了mybatis-plus所以写了一个基本就够用了。
<?xml version="1.0" encoding="UTF-8"?>id, type, parent, name, url, icon, permission, sort, create_time, create_name, update_time, update_name, version, del_flag m.id as id, m.parent, m.name as title,m.icon,m.url as href, m.type, m.permission, m.sort
package com.dome.model.vo.menu; import io.swagger.annotations.ApiModel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.List; /** * @author YinXiaoWei * @date 2020/11/26 10:34 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @ApiModel(value="MenuIndexVo对象", description="菜单信息返回表") public class MenuIndexVo implements Serializable { private Integer id; private Integer parentId; private String title; private String icon; private Integer type; private String href; private String permission; private Listchildren; }
创建UserDetailsServiceImpl类,重写UserDetailsService方法,动态连接数据库
package com.dome.provider.config.security; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.dome.biz.service.menu.MenuService; import com.dome.biz.service.role.RoleService; import com.dome.biz.service.user.UserRoleService; import com.dome.biz.service.user.UserService; import com.dome.common.utils.RedisUtil; import com.dome.model.po.role.Role; import com.dome.model.po.user.User; import com.dome.model.po.user.UserRole; import com.dome.model.vo.menu.MenuIndexVo; import com.dome.provider.config.security.dto.JwtUserDto; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * @author YinXiaoWei * @date 2020/11/26 10:25 * 重写登录实现类 */ @Service @Slf4j public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Autowired private UserRoleService userRoleService; @Autowired private RoleService roleService; @Autowired private MenuService menuService; @Override public JwtUserDto loadUserByUsername(String userName) { //根据用户名获取用户 User user = userService.existUser(userName); if (user == null ){ throw new BadCredentialsException("用户名或密码错误"); }else if (user.getStatus().equals(User.Status.VALID)) { throw new LockedException("用户被锁定,请联系管理员解锁"); } ListgrantedAuthorities = new ArrayList<>(15); List list = menuService.listByUserId(user.getId().toString()); List collect = list.stream().map(MenuIndexVo::getPermission).collect(Collectors.toList()); for (String authority : collect){ if (!StringUtils.isEmpty(authority)){ GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority); grantedAuthorities.add(grantedAuthority); } } //将用户所拥有的权限加入GrantedAuthority集合中 JwtUserDto loginUser =new JwtUserDto(user,grantedAuthorities); loginUser.setRoleInfo(getRoleInfo(user)); return loginUser; } /** * 获得用户所有角色 * @param user * @return */ public List getRoleInfo(User user) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.lambda().eq(UserRole::getUserId, user.getId()); List userRoleList = userRoleService.getBaseMapper().selectList(wrapper); List roleList = new ArrayList<>(); for (UserRole item : userRoleList) { Role role = roleService.getBaseMapper().selectById(item.getRoleId()); roleList.add(role); } return roleList; } }
完成这一步就可以登录了,在securityConfig中添加UserDetailsServiceImpl和加密方法(数据库的密码现在好像必须要加密,有兴趣可以了解下CSDN密码泄露事件)。
package com.dome.provider.config.security.config; import com.dome.provider.config.security.UserDetailsServiceImpl; import com.dome.provider.config.security.filter.JwtAuthenticationTokenFilter; import com.dome.provider.config.security.handler.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.builders.WebSecurity; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * @author YinXiaoWei * @date 2020/11/25 17:07 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { /** * 登录逻辑 */ @Autowired private UserDetailsServiceImpl userDetailsService; /** * 登录成功逻辑 */ @Autowired private MyAuthenticationSuccessHandler authenticationSuccessHandler; /** * 登出成功逻辑 */ @Autowired private AjaxLogoutSuccessHandler logoutSuccessHandler; /** * 登录失败逻辑 */ @Autowired private MyAuthenticationFailureHandler authenticationFailureHandler; /** * 无权限拦截器 */ @Autowired private RestAuthenticationEntryPoint restAuthenticationEntryPoint; /** * 无权访问 JSON 格式的数据 */ @Autowired private RestfulAccessDeniedHandler accessDeniedHandler; /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 放行静态资源 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers(HttpMethod.GET, "/swagger-resources/**", "/PearAdmin/**", "/component/**", "/admin/**", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-ui.html", "/webjars/**", "/v2/**", "/druid/**"); } /** * 开启加密 * @return */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 身份认证接口 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } /** * 核心配置 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //关闭csrf http.csrf().disable() // 基于token,所以不需要session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //未登陆时返回 JSON 格式的数据给前端 .httpBasic() .authenticationEntryPoint(restAuthenticationEntryPoint) .and() .authorizeRequests() // 对于登录login 验证码 允许匿名访问 .antMatchers("/authentication/getAuthCode", "/authentication/existAuthCode/**","/login") .anonymous() .antMatchers("/profile/**").permitAll() .antMatchers("/common/download**").anonymous() .antMatchers("/common/download/resource**").anonymous() .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() .antMatchers("/webjars/**").anonymous() .antMatchers("/*/api-docs").anonymous() .antMatchers("/doc.html").permitAll() // 其余请求都需要认证 .anyRequest().authenticated() .and() .formLogin() // 登录页面 不设限访问 // .loginPage("/login.html") // .loginProcessingUrl("/login") // 登录成功 .successHandler(authenticationSuccessHandler) // 登录失败 .failureHandler(authenticationFailureHandler) .permitAll() .and() // 注销行为 .logout() .logoutUrl("/logout") .logoutSuccessHandler(logoutSuccessHandler) .and() // 允许通过remember-me登录的用户访问 .rememberMe() .rememberMeParameter("remember-me") .and() .headers() // 禁用缓存 .cacheControl(); // 防止iframe 造成跨域 http.headers().frameOptions().disable(); // 添加的token拦截器 http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 无权访问 JSON 格式的数据 http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); } }
做到这一步可以尝试下登录,http://ip:host/login(将上一步中多余的代码删掉)
以下是自定义的一些拦截器,把这些添加代码里面基本都有注释我就不介绍了。
package com.dome.provider.config.security.handler; import com.alibaba.fastjson.JSON; import com.dome.common.utils.RedisUtil; import com.dome.common.utils.model.ResponseModel; import com.dome.provider.config.security.dto.JwtUserDto; import com.dome.provider.config.security.jwt.JwtUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; 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.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author YinXiaoWei * @date 2020/11/26 9:29 * 登录成功 */ @Slf4j @Component public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Autowired private JwtUtils jwtUtils; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Value("${jwt.expiration}") private Long expiration; @Autowired private RedisUtil redisUtil; private static final String TOKEN_KEY = "USER_TOKEN"; private static final String USER_KEY = "USER_INFO"; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { //拿到登录用户信息 JwtUserDto userDetails = (JwtUserDto)authentication.getPrincipal(); //生成token String jwtToken = jwtUtils.generateToken(userDetails.getUsername()); // 存入redis中 if (redisUtil.hasKey(TOKEN_KEY + userDetails.getUsername())) { redisUtil.delKey(TOKEN_KEY + userDetails.getUsername()); } if (redisUtil.hasKey(USER_KEY + userDetails.getUsername())) { redisUtil.delKey(USER_KEY + userDetails.getUsername()); } redisUtil.setObject(USER_KEY + userDetails.getUsername(), userDetails); redisUtil.setObjectTime(TOKEN_KEY + userDetails.getUsername(), jwtToken, this.expiration); // 返回token信息 ResponseModel success = ResponseModel.success("登录成功", jwtToken); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); //输出结果 httpServletResponse.getWriter().write(JSON.toJSONString(success)); } }
package com.dome.provider.config.security.handler; import com.alibaba.fastjson.JSON; import com.dome.common.utils.model.ResponseModel; import org.springframework.security.authentication.BadCredentialsException; 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.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author YinXiaoWei * @date 2020/11/26 10:12 * 登录失败 */ @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); if (e instanceof BadCredentialsException){ httpServletResponse.getWriter().write(JSON.toJSONString(ResponseModel.error("用户名或密码错误"))); }else { httpServletResponse.getWriter().write(JSON.toJSONString(ResponseModel.error(e.getMessage()))); } } }
package com.dome.provider.config.security.handler; import com.alibaba.fastjson.JSON; import com.dome.common.utils.model.ResponseModel; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author YinXiaoWei * @date 2020/11/30 10:39 * 退出成功 */ @Component public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSON.toJSONString(ResponseModel.success("登出成功!"))); response.getWriter().flush(); } }
package com.dome.provider.config.security.handler; import com.alibaba.fastjson.JSON; import com.dome.common.utils.model.ResponseModel; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author YinXiaoWei * @date 2020/11/26 10:19 * 当未登录或者token失效访问接口时,自定义的返回结果 */ @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSON.toJSONString(ResponseModel.error("尚未登录,或者登录过期 " + authException.getMessage()))); response.getWriter().flush(); } }
package com.dome.provider.config.security.handler; import com.alibaba.fastjson.JSON; import com.dome.common.utils.model.ResponseModel; 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.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author YinXiaoWei * @date 2020/11/26 10:23 * 当访问接口没有权限时,自定义的返回结果 */ @Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSON.toJSONString(ResponseModel.error(e.getMessage()))); response.getWriter().flush(); } }
package com.dome.provider.config.security.filter; import com.dome.common.utils.RedisUtil; import com.dome.provider.config.security.UserDetailsServiceImpl; import com.dome.provider.config.security.jwt.JwtUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 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 YinXiaoWei * @date 2020/11/26 16:54 * token过滤器 */ @Slf4j @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private JwtUtils jwtUtils; @Autowired private RedisUtil redisUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; private static final String TOKEN_KEY = "USER_TOKEN"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获取header令牌 String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(this.tokenHead)) { // token由"Bearer "开头 String authToken = authHeader.substring(this.tokenHead.length()); //解析token获取用户名 String username = jwtUtils.getUserNameFromToken(authToken); log.info("checking username:{}", username); // 判断redis有没有token if (redisUtil.hasKey(TOKEN_KEY + username) && authToken.equals(redisUtil.getObject(TOKEN_KEY + username))) { // 存放authentication到SecurityContextHolder if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (userDetails != null) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); log.info("authenticated user:{}", username); SecurityContextHolder.getContext().setAuthentication(authentication); } } } } filterChain.doFilter(request, response); } }
在接口上添加@PreAuthorize(value = "hasAnyAuthority('对应的权限')"),在SecurityConfig加上@EnableGlobalMethodSecurity(prePostEnabled = true)
在PostMan中测试如下:
测试其他接口是需要加上token才能访问,若是不需要token就在SecurityConfig中添加antMatchers()。token前要带上Bearer 字段(在swagger中配置了默认输入)