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

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

一个用来深度学习并实战 spring boot 的项目,目前总共包含 66 个集成demo,已经完成 55 个。