You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 30 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. # spring-boot-demo-rbac-security
  2. > 此 demo 主要演示了 Spring Boot 项目如何集成 Spring Security 完成权限拦截操作。本 demo 为基于**前后端分离**的后端权限管理部分,不同于其他博客里使用的模板技术,希望对大家有所帮助。
  3. ## 1. 主要功能
  4. - [x] 基于 `RBAC` 权限模型设计,详情参考数据库表结构设计 [`security.sql`](./sql/security.sql)
  5. - [x] 支持**动态权限管理**,详情参考 [`RbacAuthorityService.java`](./src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java)
  6. - [x] **登录 / 登出**部分均使用自定义 Controller 实现,未使用 `Spring Security` 内部默认的实现,适用于前后端分离项目,详情参考 [`SecurityConfig.java`](./src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java) 和 [`AuthController.java`](./src/main/java/com/xkcoding/rbac/security/controller/AuthController.java)
  7. - [x] 持久化技术使用 `spring-data-jpa` 完成
  8. - [x] 使用 `JWT` 实现安全验证,同时引入 `Redis` 解决 `JWT` 无法手动设置过期的弊端,并且保证同一用户在同一时间仅支持同一设备登录,不同设备登录会将,详情参考 [`JwtUtil.java`](./src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java)
  9. - [x] 在线人数统计,详情参考 [`MonitorService.java`](./src/main/java/com/xkcoding/rbac/security/service/MonitorService.java) 和 [`RedisUtil.java`](./src/main/java/com/xkcoding/rbac/security/util/RedisUtil.java)
  10. - [x] 手动踢出用户,详情参考 [`MonitorService.java`](./src/main/java/com/xkcoding/rbac/security/service/MonitorService.java)
  11. - [x] 自定义配置不需要进行拦截的请求,详情参考 [`CustomConfig.java`](./src/main/java/com/xkcoding/rbac/security/config/CustomConfig.java) 和 [`application.yml`](./src/main/resources/application.yml)
  12. ## 2. 运行
  13. ### 2.1. 环境
  14. 1. JDK 1.8 以上
  15. 2. Maven 3.5 以上
  16. 3. Mysql 5.7 以上
  17. 4. Redis
  18. ### 2.2. 运行方式
  19. 1. 新建一个名为 `spring-boot-demo` 的数据库,字符集设置为 `utf-8`,如果数据库名不是 `spring-boot-demo` 需要在 `application.yml` 中修改 `spring.datasource.url`
  20. 2. 使用 [`security.sql`](./sql/security.sql) 这个 SQL 文件,创建数据库表和初始化RBAC数据
  21. 3. 运行 `SpringBootDemoRbacSecurityApplication`
  22. 4. 管理员账号:admin/123456 普通用户:user/123456
  23. 5. 使用 `POST` 请求访问 `/${contextPath}/api/auth/login` 端点,输入账号密码,登陆成功之后返回token,将获得的 token 放在具体请求的 Header 里,key 固定是 `Authorization` ,value 前缀为 `Bearer 后面加空格`再加token,并加上具体请求的参数,就可以了
  24. 6. enjoy ~​ :kissing_smiling_eyes:
  25. ## 3. 部分关键代码
  26. ### 3.1. pom.xml
  27. ```xml
  28. <?xml version="1.0" encoding="UTF-8"?>
  29. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  30. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  31. <modelVersion>4.0.0</modelVersion>
  32. <artifactId>spring-boot-demo-rbac-security</artifactId>
  33. <version>1.0.0-SNAPSHOT</version>
  34. <packaging>jar</packaging>
  35. <name>spring-boot-demo-rbac-security</name>
  36. <description>Demo project for Spring Boot</description>
  37. <parent>
  38. <groupId>com.xkcoding</groupId>
  39. <artifactId>spring-boot-demo</artifactId>
  40. <version>1.0.0-SNAPSHOT</version>
  41. </parent>
  42. <properties>
  43. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  44. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  45. <java.version>1.8</java.version>
  46. <jjwt.veersion>0.9.1</jjwt.veersion>
  47. </properties>
  48. <dependencies>
  49. <dependency>
  50. <groupId>org.springframework.boot</groupId>
  51. <artifactId>spring-boot-starter-web</artifactId>
  52. </dependency>
  53. <dependency>
  54. <groupId>org.springframework.boot</groupId>
  55. <artifactId>spring-boot-starter-security</artifactId>
  56. </dependency>
  57. <dependency>
  58. <groupId>org.springframework.boot</groupId>
  59. <artifactId>spring-boot-starter-data-jpa</artifactId>
  60. </dependency>
  61. <dependency>
  62. <groupId>org.springframework.boot</groupId>
  63. <artifactId>spring-boot-starter-data-redis</artifactId>
  64. </dependency>
  65. <!-- 对象池,使用redis时必须引入 -->
  66. <dependency>
  67. <groupId>org.apache.commons</groupId>
  68. <artifactId>commons-pool2</artifactId>
  69. </dependency>
  70. <dependency>
  71. <groupId>org.springframework.boot</groupId>
  72. <artifactId>spring-boot-configuration-processor</artifactId>
  73. <optional>true</optional>
  74. </dependency>
  75. <dependency>
  76. <groupId>io.jsonwebtoken</groupId>
  77. <artifactId>jjwt</artifactId>
  78. <version>${jjwt.veersion}</version>
  79. </dependency>
  80. <dependency>
  81. <groupId>mysql</groupId>
  82. <artifactId>mysql-connector-java</artifactId>
  83. </dependency>
  84. <dependency>
  85. <groupId>org.springframework.boot</groupId>
  86. <artifactId>spring-boot-starter-test</artifactId>
  87. <scope>test</scope>
  88. </dependency>
  89. <dependency>
  90. <groupId>cn.hutool</groupId>
  91. <artifactId>hutool-all</artifactId>
  92. </dependency>
  93. <dependency>
  94. <groupId>org.projectlombok</groupId>
  95. <artifactId>lombok</artifactId>
  96. <optional>true</optional>
  97. </dependency>
  98. </dependencies>
  99. <build>
  100. <finalName>spring-boot-demo-rbac-security</finalName>
  101. <plugins>
  102. <plugin>
  103. <groupId>org.springframework.boot</groupId>
  104. <artifactId>spring-boot-maven-plugin</artifactId>
  105. </plugin>
  106. </plugins>
  107. </build>
  108. </project>
  109. ```
  110. ### 3.2. JwtUtil.java
  111. > JWT 工具类,主要功能:生成JWT并存入Redis、解析JWT并校验其准确性、从Request的Header中获取JWT
  112. ```java
  113. /**
  114. * <p>
  115. * JWT 工具类
  116. * </p>
  117. *
  118. * @author yangkai.shen
  119. * @date Created in 2018-12-07 13:42
  120. */
  121. @EnableConfigurationProperties(JwtConfig.class)
  122. @Configuration
  123. @Slf4j
  124. public class JwtUtil {
  125. @Autowired
  126. private JwtConfig jwtConfig;
  127. @Autowired
  128. private StringRedisTemplate stringRedisTemplate;
  129. /**
  130. * 创建JWT
  131. *
  132. * @param rememberMe 记住我
  133. * @param id 用户id
  134. * @param subject 用户名
  135. * @param roles 用户角色
  136. * @param authorities 用户权限
  137. * @return JWT
  138. */
  139. public String createJWT(Boolean rememberMe, Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) {
  140. Date now = new Date();
  141. JwtBuilder builder = Jwts.builder()
  142. .setId(id.toString())
  143. .setSubject(subject)
  144. .setIssuedAt(now)
  145. .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey())
  146. .claim("roles", roles)
  147. .claim("authorities", authorities);
  148. // 设置过期时间
  149. Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
  150. if (ttl > 0) {
  151. builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));
  152. }
  153. String jwt = builder.compact();
  154. // 将生成的JWT保存至Redis
  155. stringRedisTemplate.opsForValue()
  156. .set(Consts.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS);
  157. return jwt;
  158. }
  159. /**
  160. * 创建JWT
  161. *
  162. * @param authentication 用户认证信息
  163. * @param rememberMe 记住我
  164. * @return JWT
  165. */
  166. public String createJWT(Authentication authentication, Boolean rememberMe) {
  167. UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
  168. return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities());
  169. }
  170. /**
  171. * 解析JWT
  172. *
  173. * @param jwt JWT
  174. * @return {@link Claims}
  175. */
  176. public Claims parseJWT(String jwt) {
  177. try {
  178. Claims claims = Jwts.parser()
  179. .setSigningKey(jwtConfig.getKey())
  180. .parseClaimsJws(jwt)
  181. .getBody();
  182. String username = claims.getSubject();
  183. String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username;
  184. // 校验redis中的JWT是否存在
  185. Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
  186. if (Objects.isNull(expire) || expire <= 0) {
  187. throw new SecurityException(Status.TOKEN_EXPIRED);
  188. }
  189. // 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期
  190. String redisToken = stringRedisTemplate.opsForValue()
  191. .get(redisKey);
  192. if (!StrUtil.equals(jwt, redisToken)) {
  193. throw new SecurityException(Status.TOKEN_OUT_OF_CTRL);
  194. }
  195. return claims;
  196. } catch (ExpiredJwtException e) {
  197. log.error("Token 已过期");
  198. throw new SecurityException(Status.TOKEN_EXPIRED);
  199. } catch (UnsupportedJwtException e) {
  200. log.error("不支持的 Token");
  201. throw new SecurityException(Status.TOKEN_PARSE_ERROR);
  202. } catch (MalformedJwtException e) {
  203. log.error("Token 无效");
  204. throw new SecurityException(Status.TOKEN_PARSE_ERROR);
  205. } catch (SignatureException e) {
  206. log.error("无效的 Token 签名");
  207. throw new SecurityException(Status.TOKEN_PARSE_ERROR);
  208. } catch (IllegalArgumentException e) {
  209. log.error("Token 参数不存在");
  210. throw new SecurityException(Status.TOKEN_PARSE_ERROR);
  211. }
  212. }
  213. /**
  214. * 设置JWT过期
  215. *
  216. * @param request 请求
  217. */
  218. public void invalidateJWT(HttpServletRequest request) {
  219. String jwt = getJwtFromRequest(request);
  220. String username = getUsernameFromJWT(jwt);
  221. // 从redis中清除JWT
  222. stringRedisTemplate.delete(Consts.REDIS_JWT_KEY_PREFIX + username);
  223. }
  224. /**
  225. * 根据 jwt 获取用户名
  226. *
  227. * @param jwt JWT
  228. * @return 用户名
  229. */
  230. public String getUsernameFromJWT(String jwt) {
  231. Claims claims = parseJWT(jwt);
  232. return claims.getSubject();
  233. }
  234. /**
  235. * 从 request 的 header 中获取 JWT
  236. *
  237. * @param request 请求
  238. * @return JWT
  239. */
  240. public String getJwtFromRequest(HttpServletRequest request) {
  241. String bearerToken = request.getHeader("Authorization");
  242. if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
  243. return bearerToken.substring(7);
  244. }
  245. return null;
  246. }
  247. }
  248. ```
  249. ### 3.3. SecurityConfig.java
  250. > Spring Security 配置类,主要功能:配置哪些URL不需要认证,哪些需要认证
  251. ```java
  252. /**
  253. * <p>
  254. * Security 配置
  255. * </p>
  256. *
  257. * @author yangkai.shen
  258. * @date Created in 2018-12-07 16:46
  259. */
  260. @Configuration
  261. @EnableWebSecurity
  262. @EnableConfigurationProperties(CustomConfig.class)
  263. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  264. @Autowired
  265. private CustomConfig customConfig;
  266. @Autowired
  267. private AccessDeniedHandler accessDeniedHandler;
  268. @Autowired
  269. private CustomUserDetailsService customUserDetailsService;
  270. @Autowired
  271. private JwtAuthenticationFilter jwtAuthenticationFilter;
  272. @Bean
  273. public BCryptPasswordEncoder encoder() {
  274. return new BCryptPasswordEncoder();
  275. }
  276. @Override
  277. @Bean
  278. public AuthenticationManager authenticationManagerBean() throws Exception {
  279. return super.authenticationManagerBean();
  280. }
  281. @Override
  282. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  283. auth.userDetailsService(customUserDetailsService)
  284. .passwordEncoder(encoder());
  285. }
  286. @Override
  287. protected void configure(HttpSecurity http) throws Exception {
  288. http.cors()
  289. // 关闭 CSRF
  290. .and()
  291. .csrf()
  292. .disable()
  293. // 登录行为由自己实现,参考 AuthController#login
  294. .formLogin()
  295. .disable()
  296. .httpBasic()
  297. .disable()
  298. // 认证请求
  299. .authorizeRequests()
  300. // 所有请求都需要登录访问
  301. .anyRequest()
  302. .authenticated()
  303. // RBAC 动态 url 认证
  304. .anyRequest()
  305. .access("@rbacAuthorityService.hasPermission(request,authentication)")
  306. // 登出行为由自己实现,参考 AuthController#logout
  307. .and()
  308. .logout()
  309. .disable()
  310. // Session 管理
  311. .sessionManagement()
  312. // 因为使用了JWT,所以这里不管理Session
  313. .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  314. // 异常处理
  315. .and()
  316. .exceptionHandling()
  317. .accessDeniedHandler(accessDeniedHandler);
  318. // 添加自定义 JWT 过滤器
  319. http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
  320. }
  321. /**
  322. * 放行所有不需要登录就可以访问的请求,参见 AuthController
  323. * 也可以在 {@link #configure(HttpSecurity)} 中配置
  324. * {@code http.authorizeRequests().antMatchers("/api/auth/**").permitAll()}
  325. */
  326. @Override
  327. public void configure(WebSecurity web) {
  328. WebSecurity and = web.ignoring()
  329. .and();
  330. // 忽略 GET
  331. customConfig.getIgnores()
  332. .getGet()
  333. .forEach(url -> and.ignoring()
  334. .antMatchers(HttpMethod.GET, url));
  335. // 忽略 POST
  336. customConfig.getIgnores()
  337. .getPost()
  338. .forEach(url -> and.ignoring()
  339. .antMatchers(HttpMethod.POST, url));
  340. // 忽略 DELETE
  341. customConfig.getIgnores()
  342. .getDelete()
  343. .forEach(url -> and.ignoring()
  344. .antMatchers(HttpMethod.DELETE, url));
  345. // 忽略 PUT
  346. customConfig.getIgnores()
  347. .getPut()
  348. .forEach(url -> and.ignoring()
  349. .antMatchers(HttpMethod.PUT, url));
  350. // 忽略 HEAD
  351. customConfig.getIgnores()
  352. .getHead()
  353. .forEach(url -> and.ignoring()
  354. .antMatchers(HttpMethod.HEAD, url));
  355. // 忽略 PATCH
  356. customConfig.getIgnores()
  357. .getPatch()
  358. .forEach(url -> and.ignoring()
  359. .antMatchers(HttpMethod.PATCH, url));
  360. // 忽略 OPTIONS
  361. customConfig.getIgnores()
  362. .getOptions()
  363. .forEach(url -> and.ignoring()
  364. .antMatchers(HttpMethod.OPTIONS, url));
  365. // 忽略 TRACE
  366. customConfig.getIgnores()
  367. .getTrace()
  368. .forEach(url -> and.ignoring()
  369. .antMatchers(HttpMethod.TRACE, url));
  370. // 按照请求格式忽略
  371. customConfig.getIgnores()
  372. .getPattern()
  373. .forEach(url -> and.ignoring()
  374. .antMatchers(url));
  375. }
  376. }
  377. ```
  378. ### 3.4. RbacAuthorityService.java
  379. > 路由动态鉴权类,主要功能:
  380. >
  381. > 1. 校验请求的合法性,排除404和405这两种异常请求
  382. > 2. 根据当前请求路径与该用户可访问的资源做匹配,通过则可以访问,否则,不允许访问
  383. ```java
  384. /**
  385. * <p>
  386. * 动态路由认证
  387. * </p>
  388. *
  389. * @author yangkai.shen
  390. * @date Created in 2018-12-10 17:17
  391. */
  392. @Component
  393. public class RbacAuthorityService {
  394. @Autowired
  395. private RoleDao roleDao;
  396. @Autowired
  397. private PermissionDao permissionDao;
  398. @Autowired
  399. private RequestMappingHandlerMapping mapping;
  400. public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
  401. checkRequest(request);
  402. Object userInfo = authentication.getPrincipal();
  403. boolean hasPermission = false;
  404. if (userInfo instanceof UserDetails) {
  405. UserPrincipal principal = (UserPrincipal) userInfo;
  406. Long userId = principal.getId();
  407. List<Role> roles = roleDao.selectByUserId(userId);
  408. List<Long> roleIds = roles.stream()
  409. .map(Role::getId)
  410. .collect(Collectors.toList());
  411. List<Permission> permissions = permissionDao.selectByRoleIdList(roleIds);
  412. //获取资源,前后端分离,所以过滤页面权限,只保留按钮权限
  413. List<Permission> btnPerms = permissions.stream()
  414. // 过滤页面权限
  415. .filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON))
  416. // 过滤 URL 为空
  417. .filter(permission -> StrUtil.isNotBlank(permission.getUrl()))
  418. // 过滤 METHOD 为空
  419. .filter(permission -> StrUtil.isNotBlank(permission.getMethod()))
  420. .collect(Collectors.toList());
  421. for (Permission btnPerm : btnPerms) {
  422. AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod());
  423. if (antPathMatcher.matches(request)) {
  424. hasPermission = true;
  425. break;
  426. }
  427. }
  428. return hasPermission;
  429. } else {
  430. return false;
  431. }
  432. }
  433. /**
  434. * 校验请求是否存在
  435. *
  436. * @param request 请求
  437. */
  438. private void checkRequest(HttpServletRequest request) {
  439. // 获取当前 request 的方法
  440. String currentMethod = request.getMethod();
  441. Multimap<String, String> urlMapping = allUrlMapping();
  442. for (String uri : urlMapping.keySet()) {
  443. // 通过 AntPathRequestMatcher 匹配 url
  444. // 可以通过 2 种方式创建 AntPathRequestMatcher
  445. // 1:new AntPathRequestMatcher(uri,method) 这种方式可以直接判断方法是否匹配,因为这里我们把 方法不匹配 自定义抛出,所以,我们使用第2种方式创建
  446. // 2:new AntPathRequestMatcher(uri) 这种方式不校验请求方法,只校验请求路径
  447. AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri);
  448. if (antPathMatcher.matches(request)) {
  449. if (!urlMapping.get(uri)
  450. .contains(currentMethod)) {
  451. throw new SecurityException(Status.HTTP_BAD_METHOD);
  452. } else {
  453. return;
  454. }
  455. }
  456. }
  457. throw new SecurityException(Status.REQUEST_NOT_FOUND);
  458. }
  459. /**
  460. * 获取 所有URL Mapping,返回格式为{"/test":["GET","POST"],"/sys":["GET","DELETE"]}
  461. *
  462. * @return {@link ArrayListMultimap} 格式的 URL Mapping
  463. */
  464. private Multimap<String, String> allUrlMapping() {
  465. Multimap<String, String> urlMapping = ArrayListMultimap.create();
  466. // 获取url与类和方法的对应信息
  467. Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
  468. handlerMethods.forEach((k, v) -> {
  469. // 获取当前 key 下的获取所有URL
  470. Set<String> url = k.getPatternsCondition()
  471. .getPatterns();
  472. RequestMethodsRequestCondition method = k.getMethodsCondition();
  473. // 为每个URL添加所有的请求方法
  474. url.forEach(s -> urlMapping.putAll(s, method.getMethods()
  475. .stream()
  476. .map(Enum::toString)
  477. .collect(Collectors.toList())));
  478. });
  479. return urlMapping;
  480. }
  481. }
  482. ```
  483. ### 3.5. JwtAuthenticationFilter.java
  484. > JWT 认证过滤器,主要功能:
  485. >
  486. > 1. 过滤不需要拦截的请求
  487. > 2. 根据当前请求的JWT,认证用户身份信息
  488. ```java
  489. /**
  490. * <p>
  491. * Jwt 认证过滤器
  492. * </p>
  493. *
  494. * @author yangkai.shen
  495. * @date Created in 2018-12-10 15:15
  496. */
  497. @Component
  498. @Slf4j
  499. public class JwtAuthenticationFilter extends OncePerRequestFilter {
  500. @Autowired
  501. private CustomUserDetailsService customUserDetailsService;
  502. @Autowired
  503. private JwtUtil jwtUtil;
  504. @Autowired
  505. private CustomConfig customConfig;
  506. @Override
  507. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  508. if (checkIgnores(request)) {
  509. filterChain.doFilter(request, response);
  510. return;
  511. }
  512. String jwt = jwtUtil.getJwtFromRequest(request);
  513. if (StrUtil.isNotBlank(jwt)) {
  514. try {
  515. String username = jwtUtil.getUsernameFromJWT(jwt);
  516. UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
  517. UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
  518. authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
  519. SecurityContextHolder.getContext()
  520. .setAuthentication(authentication);
  521. filterChain.doFilter(request, response);
  522. } catch (SecurityException e) {
  523. ResponseUtil.renderJson(response, e);
  524. }
  525. } else {
  526. ResponseUtil.renderJson(response, Status.UNAUTHORIZED, null);
  527. }
  528. }
  529. /**
  530. * 请求是否不需要进行权限拦截
  531. *
  532. * @param request 当前请求
  533. * @return true - 忽略,false - 不忽略
  534. */
  535. private boolean checkIgnores(HttpServletRequest request) {
  536. String method = request.getMethod();
  537. HttpMethod httpMethod = HttpMethod.resolve(method);
  538. if (ObjectUtil.isNull(httpMethod)) {
  539. httpMethod = HttpMethod.GET;
  540. }
  541. Set<String> ignores = Sets.newHashSet();
  542. switch (httpMethod) {
  543. case GET:
  544. ignores.addAll(customConfig.getIgnores()
  545. .getGet());
  546. break;
  547. case PUT:
  548. ignores.addAll(customConfig.getIgnores()
  549. .getPut());
  550. break;
  551. case HEAD:
  552. ignores.addAll(customConfig.getIgnores()
  553. .getHead());
  554. break;
  555. case POST:
  556. ignores.addAll(customConfig.getIgnores()
  557. .getPost());
  558. break;
  559. case PATCH:
  560. ignores.addAll(customConfig.getIgnores()
  561. .getPatch());
  562. break;
  563. case TRACE:
  564. ignores.addAll(customConfig.getIgnores()
  565. .getTrace());
  566. break;
  567. case DELETE:
  568. ignores.addAll(customConfig.getIgnores()
  569. .getDelete());
  570. break;
  571. case OPTIONS:
  572. ignores.addAll(customConfig.getIgnores()
  573. .getOptions());
  574. break;
  575. default:
  576. break;
  577. }
  578. ignores.addAll(customConfig.getIgnores()
  579. .getPattern());
  580. if (CollUtil.isNotEmpty(ignores)) {
  581. for (String ignore : ignores) {
  582. AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method);
  583. if (matcher.matches(request)) {
  584. return true;
  585. }
  586. }
  587. }
  588. return false;
  589. }
  590. }
  591. ```
  592. ### 3.6. CustomUserDetailsService.java
  593. > 实现 `UserDetailsService` 接口,主要功能:根据用户名查询用户信息
  594. ```java
  595. /**
  596. * <p>
  597. * 自定义UserDetails查询
  598. * </p>
  599. *
  600. * @author yangkai.shen
  601. * @date Created in 2018-12-10 10:29
  602. */
  603. @Service
  604. public class CustomUserDetailsService implements UserDetailsService {
  605. @Autowired
  606. private UserDao userDao;
  607. @Autowired
  608. private RoleDao roleDao;
  609. @Autowired
  610. private PermissionDao permissionDao;
  611. @Override
  612. public UserDetails loadUserByUsername(String usernameOrEmailOrPhone) throws UsernameNotFoundException {
  613. User user = userDao.findByUsernameOrEmailOrPhone(usernameOrEmailOrPhone, usernameOrEmailOrPhone, usernameOrEmailOrPhone)
  614. .orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + usernameOrEmailOrPhone));
  615. List<Role> roles = roleDao.selectByUserId(user.getId());
  616. List<Long> roleIds = roles.stream()
  617. .map(Role::getId)
  618. .collect(Collectors.toList());
  619. List<Permission> permissions = permissionDao.selectByRoleIdList(roleIds);
  620. return UserPrincipal.create(user, roles, permissions);
  621. }
  622. }
  623. ```
  624. ### 3.7. RedisUtil.java
  625. > 主要功能:根据key的格式分页获取Redis存在的key列表
  626. ```java
  627. /**
  628. * <p>
  629. * Redis工具类
  630. * </p>
  631. *
  632. * @author yangkai.shen
  633. * @date Created in 2018-12-11 20:24
  634. */
  635. @Component
  636. @Slf4j
  637. public class RedisUtil {
  638. @Autowired
  639. private StringRedisTemplate stringRedisTemplate;
  640. /**
  641. * 分页获取指定格式key,使用 scan 命令代替 keys 命令,在大数据量的情况下可以提高查询效率
  642. *
  643. * @param patternKey key格式
  644. * @param currentPage 当前页码
  645. * @param pageSize 每页条数
  646. * @return 分页获取指定格式key
  647. */
  648. public PageResult<String> findKeysForPage(String patternKey, int currentPage, int pageSize) {
  649. ScanOptions options = ScanOptions.scanOptions()
  650. .match(patternKey)
  651. .build();
  652. RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory();
  653. RedisConnection rc = factory.getConnection();
  654. Cursor<byte[]> cursor = rc.scan(options);
  655. List<String> result = Lists.newArrayList();
  656. long tmpIndex = 0;
  657. int startIndex = (currentPage - 1) * pageSize;
  658. int end = currentPage * pageSize;
  659. while (cursor.hasNext()) {
  660. String key = new String(cursor.next());
  661. if (tmpIndex >= startIndex && tmpIndex < end) {
  662. result.add(key);
  663. }
  664. tmpIndex++;
  665. }
  666. try {
  667. cursor.close();
  668. RedisConnectionUtils.releaseConnection(rc, factory);
  669. } catch (Exception e) {
  670. log.warn("Redis连接关闭异常,", e);
  671. }
  672. return new PageResult<>(result, tmpIndex);
  673. }
  674. }
  675. ```
  676. ### 3.8. MonitorService.java
  677. > 监控服务,主要功能:查询当前在线人数分页列表,手动踢出某个用户
  678. ```java
  679. package com.xkcoding.rbac.security.service;
  680. import cn.hutool.core.util.StrUtil;
  681. import com.google.common.collect.Lists;
  682. import com.xkcoding.rbac.security.common.Consts;
  683. import com.xkcoding.rbac.security.common.PageResult;
  684. import com.xkcoding.rbac.security.model.User;
  685. import com.xkcoding.rbac.security.repository.UserDao;
  686. import com.xkcoding.rbac.security.util.RedisUtil;
  687. import com.xkcoding.rbac.security.vo.OnlineUser;
  688. import org.springframework.beans.factory.annotation.Autowired;
  689. import org.springframework.stereotype.Service;
  690. import java.util.List;
  691. import java.util.stream.Collectors;
  692. /**
  693. * <p>
  694. * 监控 Service
  695. * </p>
  696. *
  697. * @author yangkai.shen
  698. * @date Created in 2018-12-12 00:55
  699. */
  700. @Service
  701. public class MonitorService {
  702. @Autowired
  703. private RedisUtil redisUtil;
  704. @Autowired
  705. private UserDao userDao;
  706. public PageResult<OnlineUser> onlineUser(Integer page, Integer size) {
  707. PageResult<String> keys = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, page, size);
  708. List<String> rows = keys.getRows();
  709. Long total = keys.getTotal();
  710. // 根据 redis 中键获取用户名列表
  711. List<String> usernameList = rows.stream()
  712. .map(s -> StrUtil.subAfter(s, Consts.REDIS_JWT_KEY_PREFIX, true))
  713. .collect(Collectors.toList());
  714. // 根据用户名查询用户信息
  715. List<User> userList = userDao.findByUsernameIn(usernameList);
  716. // 封装在线用户信息
  717. List<OnlineUser> onlineUserList = Lists.newArrayList();
  718. userList.forEach(user -> onlineUserList.add(OnlineUser.create(user)));
  719. return new PageResult<>(onlineUserList, total);
  720. }
  721. }
  722. ```
  723. ### 3.9. 其余代码参见本 demo
  724. ## 4. 参考
  725. 1. Spring Security 官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/
  726. 2. JWT 官网:https://jwt.io/
  727. 3. JJWT开源工具参考:https://github.com/jwtk/jjwt#quickstart
  728. 4. 授权部分参考官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/#authorization
  729. 4. 动态授权部分,参考博客:https://blog.csdn.net/larger5/article/details/81063438