赞
踩
参照链接:https://github.com/xkcoding/spring-boot-demo
参考链接:https://www.cnblogs.com/myseries/p/10871633.html
把SQL全部封装到注解的方式,或者直接使用方法拼接查询。
SpringBoot整合SpringDataJPA入门案例
该方法封装了常用的增删改查。
封装了查询一条数据、条件查询、分页查询、排序查询、计数查询。
@EntityGraph:解决懒加载的查询N+1问题,提升查询效率。
@ManyToMany:一对多
注意:防止无限递归,可以使用toString来解决。
实现自定义的sql。
SpringBoot整合Security无数据库版本入门
SpringBoot整合Security数据库版本入门
后续会更新微服务版本、整合OAuth2+JWT版本。
Redis基本命令入门
常用的获取key,删除key,以及批量删除key。
@Component @Slf4j public class RedisUtil { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 分页获取指定格式key,使用 scan 命令代替 keys 命令,在大数据量的情况下可以提高查询效率 * * @param patternKey key格式 * @param currentPage 当前页码 * @param pageSize 每页条数 * @return 分页获取指定格式key */ public PageResult<String> findKeysForPage(String patternKey, int currentPage, int pageSize) { ScanOptions options = ScanOptions.scanOptions() .match(patternKey) .build(); RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory(); RedisConnection rc = factory.getConnection(); Cursor<byte[]> cursor = rc.scan(options); List<String> result = Lists.newArrayList(); long tmpIndex = 0; int startIndex = (currentPage - 1) * pageSize; int end = currentPage * pageSize; while (cursor.hasNext()) { String key = new String(cursor.next()); if (tmpIndex >= startIndex && tmpIndex < end) { result.add(key); } tmpIndex++; } try { cursor.close(); RedisConnectionUtils.releaseConnection(rc, factory); } catch (Exception e) { log.warn("Redis连接关闭异常,", e); } return new PageResult<>(result, tmpIndex); } /** * 删除 Redis 中的某个key * * @param key 键 */ public void delete(String key) { stringRedisTemplate.delete(key); } /** * 批量删除 Redis 中的某些key * * @param keys 键列表 */ public void delete(Collection<String> keys) { stringRedisTemplate.delete(keys); } }
/** * 通用分页参数返回 * @param <T> */ @Data @NoArgsConstructor @AllArgsConstructor public class PageResult<T> implements Serializable { private static final long serialVersionUID = 3420391142991247367L; /** * 当前页数据 */ private List<T> rows; /** * 总条数 */ private Long total; public static <T> PageResult of(List<T> rows, Long total) { return new PageResult<>(rows, total); } }
实现bean 的get/set。
实现bean空构造函数。
实现可能会用到构造函数。
建造者模式下的bean。
toString()方法。
log日志。
token鉴权,token就是各种协议和加密字符串集成。权限验证的方式。
@Configuration @Slf4j public class JwtUtil { /** * redis工具类 */ @Autowired private StringRedisTemplate stringRedisTemplate; /** * 创建JWT * * @param authentication 用户认证信息 * @param rememberMe 记住我 * @return JWT */ public String createJWT(Authentication authentication, Boolean rememberMe) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities()); } /** * 创建JWT * * @param rememberMe 记住我 * @param id 用户id * @param subject 用户名 * @param roles 用户角色 * @param authorities 用户权限 * @return JWT */ public String createJWT(Boolean rememberMe, Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) { Date now = new Date(); //生成JWT的时间 long nowMillis = System.currentTimeMillis(); // 生成加密key SecretKey key = generalKey(); // 为payload添加各种标准声明和私有声明了 JwtBuilder builder = Jwts.builder() // 设置jti(JWT ID):是JWT的唯一标识,从而回避重放攻击。 .setId(id.toString()) // sub代表这个JWT的主体,即它的所有人。 .setSubject(subject) // jwt签收者 .setIssuedAt(now) // 设置签名使用的签名算法和签名使用的秘钥 .signWith(SignatureAlgorithm.HS256, key) .claim("roles", roles) // 创建Payload .claim("authorities", authorities); // 设置过期时间 Long ttlMillis = rememberMe ? CommonConstant.JWT_REMEMBER : CommonConstant.JWT_TTL; if (ttlMillis > 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); } String jwt = builder.compact(); // 将生成的JWT保存至Redis stringRedisTemplate.opsForValue() .set(CommonConstant.REDIS_JWT_KEY_PREFIX + subject, jwt, ttlMillis, TimeUnit.MILLISECONDS); return jwt; } /** * 从 request 的 header 中获取 JWT * * @param request 请求 * @return JWT */ public String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader(CommonConstant.JWT_HEADER); if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith(CommonConstant.JWT_BEARER)) { return bearerToken.substring(7); } return null; } /** * 从令牌获取用户名 * * @param token 令牌 * @return 用户名 */ public String getUsernameFromToken(String token) { // 从令牌中获取用户名 final Claims claims = getClaimsFromToken(token); // 从要求中获取主题(用户名) return claims.getSubject(); } /** * 解析JWT * * @param token JWT * @return {@link Claims} */ private Claims getClaimsFromToken(String token) { try { // 签名秘钥,和生成的签名的秘钥一模一样 SecretKey key = generalKey(); Claims claims = Jwts.parser() // 得到DefaultJwtParser .setSigningKey(key) // 设置签名的秘钥 .parseClaimsJws(token).getBody(); // 设置需要解析的jwt // 获取用户名 String username = claims.getSubject(); // 申明redis存储key String redisKey = CommonConstant.REDIS_JWT_KEY_PREFIX + username; // 校验redis中的JWT是否存在 Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); if (Objects.isNull(expire) || expire <= 0) { throw new SecurityException(HttpStatusCodeEnum.TOKEN_EXPIRED); } // 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期 String redisToken = stringRedisTemplate.opsForValue() .get(redisKey); if (!token.equals(redisToken)) { throw new SecurityException(HttpStatusCodeEnum.TOKEN_OUT_OF_CTRL); } return claims; } catch (ExpiredJwtException e) { log.error("Token 已过期"); throw new SecurityException(HttpStatusCodeEnum.TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { log.error("不支持的 Token"); throw new SecurityException(HttpStatusCodeEnum.TOKEN_PARSE_ERROR); } catch (MalformedJwtException e) { log.error("Token 无效"); throw new SecurityException(HttpStatusCodeEnum.TOKEN_PARSE_ERROR); } catch (SignatureException e) { log.error("无效的 Token 签名"); throw new SecurityException(HttpStatusCodeEnum.TOKEN_PARSE_ERROR); } catch (IllegalArgumentException e) { log.error("Token 参数不存在"); throw new SecurityException(HttpStatusCodeEnum.TOKEN_PARSE_ERROR); } } /** * 由字符串生成加密key * @return */ public SecretKey generalKey(){ // signature签证信息生成, header (base64后的)payload (base64后的) secret byte[] encodedKey = Base64.decodeBase64(CommonConstant.JWT_KEY);//本地的密码解码[B@152f6e2 // 根据给定的字节数组使用AES加密算法构造一个密钥 SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 设置JWT过期 * * @param request 请求 */ public void invalidateJWT(HttpServletRequest request) { String jwt = getJwtFromRequest(request); String username = getUsernameFromToken(jwt); // 从redis中清除JWT stringRedisTemplate.delete(CommonConstant.REDIS_JWT_KEY_PREFIX + username); } }
public class JwtUtil3 { /** * 创建一个 header 为JWT ,算法为 HS256 * payload */ @Test public void createJwtToken() { Map<String,Object> headerMap = new HashMap<>(); headerMap.put("typ","JWT"); headerMap.put("alg","HS256"); JwtBuilder builder = Jwts.builder() .setHeader(headerMap) // 设置请求头 .setIssuer("hikktn") .setAudience("xiaohong") .setId("999") //设置唯一编号 .setSubject("小白")//设置主题 可以是JSON数据 .setIssuedAt(new Date())//设置签发日期 .signWith(SignatureAlgorithm.HS256, "hahaha");//设置签名 使用HS256算法,并设置SecretKey(字符串) //构建 并返回一个字符串 System.out.println(builder.compact()); } @Test public void parseJWT(){ String compactJwt ="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJoaWtrdG4iLCJhdWQiOiJ4aWFvaG9uZyIsImp0aSI6Ijk5OSIsInN1YiI6IuWwj-eZvSIsImlhdCI6MTYyNTk5MTc4N30.wcYmhw7-OyZJYPWIMoc9AQtVhQZHdBiQaDfPsFFo50E"; JwtParser parser = Jwts.parser(); JwtParser jwtParser = parser.setSigningKey("hahaha"); Claims claims = jwtParser.parseClaimsJws(compactJwt).getBody(); System.out.println(claims); JwsHeader header = jwtParser.parseClaimsJws(compactJwt).getHeader(); System.out.println(header); String signature = jwtParser.parseClaimsJws(compactJwt).getSignature(); System.out.println(signature); } /** * Header(请求头) * {“typ”:“JWT”,“alg”:“HS256”} * 类型:JWT,算法:HS256 */ /** * Payload(负载) 参数自定义 * 例子:{“sub”:“123”,“name”:“Tom”,“admin”:true} * 可以选择使用的参数: * iss(issuer): jwt签发者 * sub(subject): jwt所面向的用户,主题 * aud(audience): 接收jwt的一方 * exp(expiration time): jwt的过期时间,这个过期时间必须要大于签发时间 * nbf(Not Before): 生效时间 * iat(Issued At): jwt的签发时间 * jti(JWT ID): jwt的唯一身份标识,主要用来作为一次性token。 * 这组数据被定为claim(要求)使用 */ /** * signature(签名) * header和payload以JSON格式转换为base64,逗号拼接后根据header里的算法加密后,得到的签名 * 组成部分: * header (base64后的) * payload (base64后的) * secret(盐) 自定义 * 盐就是你的私钥,可以进行自定义 */ }
我的学习旅程,拿到一个项目,里面没有任何文档说明,只能硬着头皮理解代码,而想要理解代码,就必须掌握很多作者的技术,关键是我没有使用或学习过的经验,这之前为了消化这个项目,我的选择是先收集一些专门争对这个项目的技术入门,完成一到二个demo。
然后按照自己的思路结合前面的demo,进行改进项目,一边思考一边学习和理解这个项目,过程中却是痛苦的。
各种因为我的代码和作者的代码不同,出现各种错误异常抛出,先后出现了不下五种异常,加之这个有bug代码移植到新的机子上,又出现了各种版本引起的环境问题。
说说我遇到卡人的问题把!
先是用WinMerge比较两个文件差异,可是并没有解决问题。
重新安装软件redis和mysql。
首先debug到源码中,一直抛出这个异常,后面一步步检查代码,发现我是用的方法获取到空值。
AbstractUserDetailsAuthenticationProvider
就是这里我之前的代码判断是没有非空,该死!这个方法替换的时候,写错了。
获取的用户一直为null,经过盘查后,发现UserPrincipal类,一旦继承了User类,那么你就得自己重新创建属性,并且类型和父类一样,而传值也是一个一个传,千万不要写一个User类,构造函数后传入UserPrincipal类,那样你就会和我一样愚蠢,一些数据就是传不进去。
没遇到这个异常前,我根本不理解为什么要这么写,遇到这个问题后,才理解了。
还有一个问题,在isEnabled()方法,我一开始写的return false,导致验证用户直接被禁用。
先放开验证。
@Override
public boolean isEnabled() {
return true;
}
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 80026 Source Host : localhost:3306 Source Schema : sso_demo Target Server Type : MySQL Target Server Version : 80026 File Encoding : 65001 Date: 23/08/2021 01:05:33 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sec_permission -- ---------------------------- DROP TABLE IF EXISTS `sec_permission`; CREATE TABLE `sec_permission` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限名', `url` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '类型为页面时,代表前端路由地址,类型为按钮时,代表后端接口地址', `type` int NOT NULL COMMENT '权限类型,页面-1,按钮-2', `permission` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限表达式', `method` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '后端接口访问方式', `sort` int NOT NULL COMMENT '排序', `parent_id` bigint NOT NULL COMMENT '父级id', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '权限表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sec_permission -- ---------------------------- INSERT INTO `sec_permission` VALUES (1072806379288399872, '测试页面', '/test', 1, 'page:test', NULL, 1, 0); INSERT INTO `sec_permission` VALUES (1072806379313565696, '测试页面-查询', '/**/test', 2, 'btn:test:query', 'GET', 1, 1072806379288399872); INSERT INTO `sec_permission` VALUES (1072806379330342912, '测试页面-添加', '/**/test', 2, 'btn:test:insert', 'POST', 2, 1072806379288399872); INSERT INTO `sec_permission` VALUES (1072806379342925824, '监控在线用户页面', '/monitor', 1, 'page:monitor:online', NULL, 2, 0); INSERT INTO `sec_permission` VALUES (1072806379363897344, '在线用户页面-查询', '/**/api/monitor/online/user', 2, 'btn:monitor:online:query', 'GET', 1, 1072806379342925824); INSERT INTO `sec_permission` VALUES (1072806379384868864, '在线用户页面-踢出', '/**/api/monitor/online/user/kickout', 2, 'btn:monitor:online:kickout', 'DELETE', 2, 1072806379342925824); -- ---------------------------- -- Table structure for sec_role -- ---------------------------- DROP TABLE IF EXISTS `sec_role`; CREATE TABLE `sec_role` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名', `description` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述', `create_time` date NOT NULL COMMENT '创建时间', `update_time` date NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `name`(`name`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sec_role -- ---------------------------- INSERT INTO `sec_role` VALUES (1072806379208708096, '管理员', '超级管理员', '2021-08-22', '2021-08-22'); INSERT INTO `sec_role` VALUES (1072806379238068224, '普通用户', '普通用户', '2021-08-22', '2021-08-22'); -- ---------------------------- -- Table structure for sec_role_permission -- ---------------------------- DROP TABLE IF EXISTS `sec_role_permission`; CREATE TABLE `sec_role_permission` ( `role_id` bigint NOT NULL COMMENT '角色主键', `permission_id` bigint NOT NULL COMMENT '权限主键', PRIMARY KEY (`role_id`, `permission_id`) USING BTREE, INDEX `FKpin61ltb1uniw17shihq1cove`(`permission_id`) USING BTREE, CONSTRAINT `FK2btquksd4tgtj9a8pn6qyeubl` FOREIGN KEY (`role_id`) REFERENCES `sec_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `FKpin61ltb1uniw17shihq1cove` FOREIGN KEY (`permission_id`) REFERENCES `sec_permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色权限关系表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sec_role_permission -- ---------------------------- INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379288399872); INSERT INTO `sec_role_permission` VALUES (1072806379238068224, 1072806379288399872); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379313565696); INSERT INTO `sec_role_permission` VALUES (1072806379238068224, 1072806379313565696); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379330342912); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379342925824); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379363897344); INSERT INTO `sec_role_permission` VALUES (1072806379208708096, 1072806379384868864); -- ---------------------------- -- Table structure for sec_user -- ---------------------------- DROP TABLE IF EXISTS `sec_user`; CREATE TABLE `sec_user` ( `id` bigint NOT NULL COMMENT '主键', `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名', `password` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码', `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称', `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机', `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱', `birthday` bigint NULL DEFAULT NULL COMMENT '生日', `sex` int NULL DEFAULT NULL COMMENT '性别,男-1,女-2', `status` int NOT NULL DEFAULT 1 COMMENT '状态,启用-1,禁用-0', `create_time` date NOT NULL COMMENT '创建时间', `update_time` date NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `username`(`username`) USING BTREE, UNIQUE INDEX `phone`(`phone`) USING BTREE, UNIQUE INDEX `email`(`email`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sec_user -- ---------------------------- INSERT INTO `sec_user` VALUES (1072806377661009920, 'admin', '$2a$10$2xC9NWUJxgCWwjJtADT3mOY3uekARVMbRiXWntKZSFsSf5ugP9K2G', '管理员', '17300000000', 'admin@xkcoding.com', 785433600000, 1, 1, '2021-08-22', '2021-08-22'); INSERT INTO `sec_user` VALUES (1072806378780889088, 'user', '$2a$10$OUDl4thpcHfs7WZ1kMUOb.ZO5eD4QANW5E.cexBLiKDIzDNt87QbO', '普通用户', '17300001111', 'user@xkcoding.com', 785433600000, 1, 1, '2021-08-22', '2021-08-22'); -- ---------------------------- -- Table structure for sec_user_role -- ---------------------------- DROP TABLE IF EXISTS `sec_user_role`; CREATE TABLE `sec_user_role` ( `user_id` bigint NOT NULL COMMENT '用户主键', `role_id` bigint NOT NULL COMMENT '角色主键', PRIMARY KEY (`user_id`, `role_id`) USING BTREE, INDEX `FKfowkd8vw5qarh8b8y9noaf4et`(`role_id`) USING BTREE, CONSTRAINT `FK835bbyiy6majrolcov7bp0yo0` FOREIGN KEY (`user_id`) REFERENCES `sec_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `FKfowkd8vw5qarh8b8y9noaf4et` FOREIGN KEY (`role_id`) REFERENCES `sec_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色关系表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sec_user_role -- ---------------------------- INSERT INTO `sec_user_role` VALUES (1072806377661009920, 1072806379208708096); INSERT INTO `sec_user_role` VALUES (1072806378780889088, 1072806379238068224); SET FOREIGN_KEY_CHECKS = 1;
接下来,只讲解代码的核心思路,不会把全部代码贴出来,当然代码还是会分享出来,并且这么测试也会放出来。
首先我想玩玩JPA多对多,所以尝试了@ManyToMany。
上面就是我改动的代码,下面就是原本的代码。
这里是上面代码的主要逻辑。
下面写Security主要的业务逻辑。
以及重要的url校验过程
链接:https://pan.baidu.com/s/1w6Ui4zogDZagfYM4mSemlA
提取码:21y4
使用Postman测试
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。