diff --git a/spring-boot-demo-rbac-security/sql/security.sql b/spring-boot-demo-rbac-security/sql/security.sql index 244a1b0..1ed2240 100644 --- a/spring-boot-demo-rbac-security/sql/security.sql +++ b/spring-boot-demo-rbac-security/sql/security.sql @@ -11,7 +11,7 @@ Target Server Version : 50718 File Encoding : 65001 - Date: 12/12/2018 01:52:06 + Date: 12/12/2018 18:52:51 */ SET NAMES utf8mb4; @@ -40,16 +40,19 @@ CREATE TABLE `sec_permission` -- ---------------------------- BEGIN; INSERT INTO `sec_permission` -VALUES (1072548676754345984, '测试页面', '/test', 1, 'page:test', NULL, 1, 0); +VALUES (1072806379288399872, '测试页面', '/test', 1, 'page:test', NULL, 1, 0); INSERT INTO `sec_permission` -VALUES (1072548676771123200, '测试页面-查询', '/**/test', 2, 'btn:test:query', 'GET', 1, 1072548676754345984); +VALUES (1072806379313565696, '测试页面-查询', '/**/test', 2, 'btn:test:query', 'GET', 1, 1072806379288399872); INSERT INTO `sec_permission` -VALUES (1072548676783706112, '测试页面-添加', '/**/test', 2, 'btn:test:insert', 'POST', 2, 1072548676754345984); +VALUES (1072806379330342912, '测试页面-添加', '/**/test', 2, 'btn:test:insert', 'POST', 2, 1072806379288399872); INSERT INTO `sec_permission` -VALUES (1072548676792094720, '监控在线用户页面', '/monitor', 1, 'page:monitor:online', NULL, 2, 0); +VALUES (1072806379342925824, '监控在线用户页面', '/monitor', 1, 'page:monitor:online', NULL, 2, 0); INSERT INTO `sec_permission` -VALUES (1072548676800483328, '在线用户页面-查询', '/**/api/monitor/online/user/**', 2, 'btn:monitor:online:query', 'POST', 1, - 1072548676792094720); +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); COMMIT; -- ---------------------------- @@ -73,9 +76,9 @@ CREATE TABLE `sec_role` -- ---------------------------- BEGIN; INSERT INTO `sec_role` -VALUES (1072548676695625728, '管理员', '超级管理员', 1544550506172, 1544550506172); +VALUES (1072806379208708096, '管理员', '超级管理员', 1544611947239, 1544611947239); INSERT INTO `sec_role` -VALUES (1072548676716597248, '普通用户', '普通用户', 1544550506177, 1544550506177); +VALUES (1072806379238068224, '普通用户', '普通用户', 1544611947246, 1544611947246); COMMIT; -- ---------------------------- @@ -95,19 +98,21 @@ CREATE TABLE `sec_role_permission` -- ---------------------------- BEGIN; INSERT INTO `sec_role_permission` -VALUES (1072548676695625728, 1072548676754345984); +VALUES (1072806379208708096, 1072806379288399872); +INSERT INTO `sec_role_permission` +VALUES (1072806379208708096, 1072806379313565696); INSERT INTO `sec_role_permission` -VALUES (1072548676695625728, 1072548676771123200); +VALUES (1072806379208708096, 1072806379330342912); INSERT INTO `sec_role_permission` -VALUES (1072548676695625728, 1072548676783706112); +VALUES (1072806379208708096, 1072806379342925824); INSERT INTO `sec_role_permission` -VALUES (1072548676695625728, 1072548676792094720); +VALUES (1072806379208708096, 1072806379363897344); INSERT INTO `sec_role_permission` -VALUES (1072548676695625728, 1072548676800483328); +VALUES (1072806379208708096, 1072806379384868864); INSERT INTO `sec_role_permission` -VALUES (1072548676716597248, 1072548676754345984); +VALUES (1072806379238068224, 1072806379288399872); INSERT INTO `sec_role_permission` -VALUES (1072548676716597248, 1072548676771123200); +VALUES (1072806379238068224, 1072806379313565696); COMMIT; -- ---------------------------- @@ -139,11 +144,11 @@ CREATE TABLE `sec_user` -- ---------------------------- BEGIN; INSERT INTO `sec_user` -VALUES (1072548675445723136, 'admin', '$2a$10$YNfKodirWWpUOQruzXEq9.sy3G9tATDlonLsPx2si7oVj6InP2KX2', '管理员', - '17300000000', 'admin@xkcoding.com', 785433600000, 1, 1, 1544550505995, 1544550505995); +VALUES (1072806377661009920, 'admin', '$2a$10$64iuSLkKNhpTN19jGHs7xePvFsub7ZCcCmBqEYw8fbACGTE3XetYq', '管理员', + '17300000000', 'admin@xkcoding.com', 785433600000, 1, 1, 1544611947032, 1544611947032); INSERT INTO `sec_user` -VALUES (1072548676288778240, 'user', '$2a$10$xAZBKSftV9/ZkkR2TEZUUeYPkn1yTXElmPE2.K/DmU1FrR4AR.Ggu', '普通用户', - '17300001111', 'user@xkcoding.com', 785433600000, 1, 1, 1544550506168, 1544550506168); +VALUES (1072806378780889088, 'user', '$2a$10$OUDl4thpcHfs7WZ1kMUOb.ZO5eD4QANW5E.cexBLiKDIzDNt87QbO', '普通用户', + '17300001111', 'user@xkcoding.com', 785433600000, 1, 1, 1544611947234, 1544611947234); COMMIT; -- ---------------------------- @@ -163,9 +168,9 @@ CREATE TABLE `sec_user_role` -- ---------------------------- BEGIN; INSERT INTO `sec_user_role` -VALUES (1072548675445723136, 1072548676695625728); +VALUES (1072806377661009920, 1072806379208708096); INSERT INTO `sec_user_role` -VALUES (1072548676288778240, 1072548676716597248); +VALUES (1072806378780889088, 1072806379238068224); COMMIT; SET FOREIGN_KEY_CHECKS = 1; diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java index ca6437e..8e97373 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java @@ -47,4 +47,19 @@ public interface Consts { * 邮箱符号 */ String SYMBOL_EMAIL = "@"; + + /** + * 默认当前页码 + */ + Integer DEFAULT_CURRENT_PAGE = 1; + + /** + * 默认每页条数 + */ + Integer DEFAULT_PAGE_SIZE = 10; + + /** + * 匿名用户 用户名 + */ + String ANONYMOUS_NAME = "匿名用户"; } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java index 7b06bfd..23a194e 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java @@ -90,7 +90,12 @@ public enum Status implements IStatus { /** * 当前用户已在别处登录,请尝试更改密码或重新登录! */ - TOKEN_OUT_OF_CTRL(5003,"当前用户已在别处登录,请尝试更改密码或重新登录!"); + TOKEN_OUT_OF_CTRL(5003, "当前用户已在别处登录,请尝试更改密码或重新登录!"), + + /** + * 无法手动踢出自己,请尝试退出登录操作! + */ + KICKOUT_SELF(5004, "无法手动踢出自己,请尝试退出登录操作!"); /** * 状态码 diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/MonitorController.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/MonitorController.java index 0bca3ac..c254f77 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/MonitorController.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/MonitorController.java @@ -1,15 +1,20 @@ package com.xkcoding.rbac.security.controller; +import cn.hutool.core.collection.CollUtil; import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.PageResult; +import com.xkcoding.rbac.security.common.Status; +import com.xkcoding.rbac.security.exception.SecurityException; +import com.xkcoding.rbac.security.payload.PageCondition; import com.xkcoding.rbac.security.service.MonitorService; +import com.xkcoding.rbac.security.util.PageUtil; +import com.xkcoding.rbac.security.util.SecurityUtil; import com.xkcoding.rbac.security.vo.OnlineUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; /** *

@@ -34,13 +39,29 @@ public class MonitorController { /** * 在线用户列表 * - * @param page 当前页码 - * @param size 每页条数 + * @param pageCondition 分页参数 */ - @PostMapping("/online/user/{page}/{size}") - public ApiResponse onlineUser(@PathVariable Integer page, @PathVariable Integer size) { - PageResult pageResult = monitorService.onlineUser(page, size); + @GetMapping("/online/user") + public ApiResponse onlineUser(PageCondition pageCondition) { + PageUtil.checkPageCondition(pageCondition, PageCondition.class); + PageResult pageResult = monitorService.onlineUser(pageCondition); return ApiResponse.ofSuccess(pageResult); } + /** + * 批量踢出在线用户 + * + * @param names 用户名列表 + */ + @DeleteMapping("/online/user/kickout") + public ApiResponse kickoutOnlineUser(@RequestBody List names) { + if (CollUtil.isEmpty(names)) { + throw new SecurityException(Status.PARAM_NOT_NULL); + } + if (names.contains(SecurityUtil.getCurrentUsername())){ + throw new SecurityException(Status.KICKOUT_SELF); + } + monitorService.kickout(names); + return ApiResponse.ofSuccess(); + } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/PageCondition.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/PageCondition.java new file mode 100644 index 0000000..bcdf201 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/PageCondition.java @@ -0,0 +1,30 @@ +package com.xkcoding.rbac.security.payload; + +import lombok.Data; + +/** + *

+ * 分页请求参数 + *

+ * + * @package: com.xkcoding.rbac.security.payload + * @description: 分页请求参数 + * @author: yangkai.shen + * @date: Created in 2018-12-12 18:05 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Data +public class PageCondition { + /** + * 当前页码 + */ + private Integer currentPage; + + /** + * 每页条数 + */ + private Integer pageSize; + +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/MonitorService.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/MonitorService.java index 38a4f27..739a683 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/MonitorService.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/service/MonitorService.java @@ -5,9 +5,12 @@ import com.google.common.collect.Lists; import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.common.PageResult; import com.xkcoding.rbac.security.model.User; +import com.xkcoding.rbac.security.payload.PageCondition; import com.xkcoding.rbac.security.repository.UserDao; import com.xkcoding.rbac.security.util.RedisUtil; +import com.xkcoding.rbac.security.util.SecurityUtil; import com.xkcoding.rbac.security.vo.OnlineUser; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -27,6 +30,7 @@ import java.util.stream.Collectors; * @version: V1.0 * @modified: yangkai.shen */ +@Slf4j @Service public class MonitorService { @Autowired @@ -38,12 +42,11 @@ public class MonitorService { /** * 在线用户分页列表 * - * @param page 当前页 - * @param size 每页条数 + * @param pageCondition 分页参数 * @return 在线用户分页列表 */ - public PageResult onlineUser(Integer page, Integer size) { - PageResult keys = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, page, size); + public PageResult onlineUser(PageCondition pageCondition) { + PageResult keys = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, pageCondition.getCurrentPage(), pageCondition.getPageSize()); List rows = keys.getRows(); Long total = keys.getTotal(); @@ -60,4 +63,27 @@ public class MonitorService { return new PageResult<>(onlineUserList, total); } + + /** + * 踢出在线用户 + * + * @param names 用户名列表 + */ + public void kickout(List names) { + // 清除 Redis 中的 JWT 信息 + List redisKeys = names.parallelStream() + .map(s -> Consts.REDIS_JWT_KEY_PREFIX + s) + .collect(Collectors.toList()); + redisUtil.delete(redisKeys); + + // 获取当前用户名 + String currentUsername = SecurityUtil.getCurrentUsername(); + names.parallelStream() + .forEach(name -> { + // TODO: 通知被踢出的用户已被当前登录用户踢出, + // 后期考虑使用 websocket 实现,具体伪代码实现如下。 + // String message = "您已被用户【" + currentUsername + "】手动下线!"; + log.debug("用户【{}】被用户【{}】手动下线!", name, currentUsername); + }); + } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/PageUtil.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/PageUtil.java new file mode 100644 index 0000000..fecd3c3 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/PageUtil.java @@ -0,0 +1,53 @@ +package com.xkcoding.rbac.security.util; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import com.xkcoding.rbac.security.common.Consts; +import com.xkcoding.rbac.security.payload.PageCondition; +import org.springframework.data.domain.PageRequest; + +/** + *

+ * 分页工具类 + *

+ * + * @package: com.xkcoding.rbac.security.util + * @description: 分页工具类 + * @author: yangkai.shen + * @date: Created in 2018-12-12 18:09 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +public class PageUtil { + /** + * 校验分页参数,为NULL,设置分页参数默认值 + * + * @param condition 查询参数 + * @param clazz 类 + * @param {@link PageCondition} + */ + public static void checkPageCondition(T condition, Class clazz) { + if (ObjectUtil.isNull(condition)) { + condition = ReflectUtil.newInstance(clazz); + } + // 校验分页参数 + if (ObjectUtil.isNull(condition.getCurrentPage())) { + condition.setCurrentPage(Consts.DEFAULT_CURRENT_PAGE); + } + if (ObjectUtil.isNull(condition.getPageSize())) { + condition.setPageSize(Consts.DEFAULT_PAGE_SIZE); + } + } + + /** + * 根据分页参数构建{@link PageRequest} + * + * @param condition 查询参数 + * @param {@link PageCondition} + * @return {@link PageRequest} + */ + public static PageRequest ofPageRequest(T condition) { + return PageRequest.of(condition.getCurrentPage(), condition.getPageSize()); + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/RedisUtil.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/RedisUtil.java index a4acd0f..b8e1422 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/RedisUtil.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/RedisUtil.java @@ -12,6 +12,7 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; +import java.util.Collection; import java.util.List; /** @@ -71,4 +72,22 @@ public class RedisUtil { 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 keys) { + stringRedisTemplate.delete(keys); + } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/SecurityUtil.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/SecurityUtil.java new file mode 100644 index 0000000..402e5f8 --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/SecurityUtil.java @@ -0,0 +1,47 @@ +package com.xkcoding.rbac.security.util; + +import cn.hutool.core.util.ObjectUtil; +import com.xkcoding.rbac.security.common.Consts; +import com.xkcoding.rbac.security.vo.UserPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +/** + *

+ * Spring Security工具类 + *

+ * + * @package: com.xkcoding.rbac.security.util + * @description: Spring Security工具类 + * @author: yangkai.shen + * @date: Created in 2018-12-12 18:30 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +public class SecurityUtil { + /** + * 获取当前登录用户用户名 + * + * @return 当前登录用户用户名 + */ + public static String getCurrentUsername() { + UserPrincipal currentUser = getCurrentUser(); + return ObjectUtil.isNull(currentUser) ? Consts.ANONYMOUS_NAME : currentUser.getUsername(); + } + + /** + * 获取当前登录用户信息 + * + * @return 当前登录用户信息,匿名登录时,为null + */ + public static UserPrincipal getCurrentUser() { + Object userInfo = SecurityContextHolder.getContext() + .getAuthentication() + .getPrincipal(); + if (userInfo instanceof UserDetails) { + return (UserPrincipal) userInfo; + } + return null; + } +} diff --git a/spring-boot-demo-rbac-security/src/main/resources/application.yml b/spring-boot-demo-rbac-security/src/main/resources/application.yml index 4415e53..88264ee 100644 --- a/spring-boot-demo-rbac-security/src/main/resources/application.yml +++ b/spring-boot-demo-rbac-security/src/main/resources/application.yml @@ -44,3 +44,6 @@ jwt: key: xkcoding ttl: 600000 remember: 604800000 +logging: + level: + com.xkcoding.rbac.security: debug diff --git a/spring-boot-demo-rbac-security/src/test/java/com/xkcoding/rbac/security/repository/DataInitTest.java b/spring-boot-demo-rbac-security/src/test/java/com/xkcoding/rbac/security/repository/DataInitTest.java index 8068414..3d27d96 100644 --- a/spring-boot-demo-rbac-security/src/test/java/com/xkcoding/rbac/security/repository/DataInitTest.java +++ b/spring-boot-demo-rbac-security/src/test/java/com/xkcoding/rbac/security/repository/DataInitTest.java @@ -68,7 +68,8 @@ public class DataInitTest extends SpringBootDemoRbacSecurityApplicationTests { Permission testBtnPermInsert = createPermission("/**/test", "测试页面-添加", 2, "btn:test:insert", "POST", 2, testPagePerm.getId()); Permission monitorOnlinePagePerm = createPermission("/monitor", "监控在线用户页面", 1, "page:monitor:online", null, 2, 0L); - Permission monitorOnlineBtnQueryPerm = createPermission("/**/api/monitor/online/user/**", "在线用户页面-查询", 2, "btn:monitor:online:query", "POST", 1, monitorOnlinePagePerm.getId()); + Permission monitorOnlineBtnQueryPerm = createPermission("/**/api/monitor/online/user", "在线用户页面-查询", 2, "btn:monitor:online:query", "GET", 1, monitorOnlinePagePerm.getId()); + Permission monitorOnlineBtnKickoutPerm = createPermission("/**/api/monitor/online/user/kickout", "在线用户页面-踢出", 2, "btn:monitor:online:kickout", "DELETE", 2, monitorOnlinePagePerm.getId()); createRolePermissionRelation(roleAdmin.getId(), testPagePerm.getId()); createRolePermissionRelation(roleUser.getId(), testPagePerm.getId()); @@ -77,6 +78,7 @@ public class DataInitTest extends SpringBootDemoRbacSecurityApplicationTests { createRolePermissionRelation(roleAdmin.getId(), testBtnPermInsert.getId()); createRolePermissionRelation(roleAdmin.getId(), monitorOnlinePagePerm.getId()); createRolePermissionRelation(roleAdmin.getId(), monitorOnlineBtnQueryPerm.getId()); + createRolePermissionRelation(roleAdmin.getId(), monitorOnlineBtnKickoutPerm.getId()); } private void createRolePermissionRelation(Long roleId, Long permissionId) {