Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案

转载:Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案arrow-up-right

此前我已经写过一篇类似的教程,但那时候使用了投机的方法,没有尊重 Spring Security 的官方设计,自己并不感到满意。这段时间比较空,故重新研究了一遍。

项目 GitHub:https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPAarrow-up-right

老版本:https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA/blob/master/README_OLD.mdarrow-up-right

1. 特性

  • 使用 JWT 进行鉴权,支持 token 过期

  • 使用 Ehcache 进行缓存,减少每次鉴权对数据库的压力

  • 尽可能贴合 Spring Security 的设计

  • 实现注解权限控制

2. 准备

开始本教程的时候希望对下面知识点进行粗略的了解。

  • 知道 JWT 的基本概念

  • 了解过 Spring Security

我之前写过两篇关于安全框架的问题,大家可以大致看一看,打下基础。

Shiro+JWT+Spring Boot Restful简易教程arrow-up-right

Spring Boot+Spring Security+Thymeleaf 简单教程arrow-up-right

本项目中 JWT 密钥是使用用户自己的登入密码,这样每一个 token 的密钥都不同,相对比较安全。

2.1 大体思路:

登入:

  1. POST 用户名密码到 \login

  2. 请求到达 JwtAuthenticationFilter 中的 attemptAuthentication() 方法,获取 request 中的 POST 参数,包装成一个 UsernamePasswordAuthenticationToken 交付给 AuthenticationManagerauthenticate() 方法进行鉴权。

  3. AuthenticationManager 会从 CachingUserDetailsService 中查找用户信息,并且判断账号密码是否正确。

  4. 如果账号密码正确跳转到 JwtAuthenticationFilter 中的 successfulAuthentication() 方法,我们进行签名,生成 token 返回给用户。

  5. 账号密码错误则跳转到 JwtAuthenticationFilter 中的 unsuccessfulAuthentication() 方法,我们返回错误信息让用户重新登入。

请求鉴权:

请求鉴权的主要思路是我们会从请求中的 Authorization 字段拿取 token,如果不存在此字段的用户,Spring Security 会默认会用 AnonymousAuthenticationToken() 包装它,即代表匿名用户。

  1. 任意请求发起

  2. 到达 JwtAuthorizationFilter 中的 doFilterInternal() 方法,进行鉴权。

  3. 如果鉴权成功我们把生成的 AuthenticationSecurityContextHolder.getContext().setAuthentication() 放入 Security,即代表鉴权完成。此处如何鉴权由我们自己代码编写,后序会详细说明。

3. 准备 pom.xml

pom.xml 配置文件这块没有什么好说的,主要说明下面的几个依赖:

因为 ehcache 读取 xml 配置文件时使用了这几个依赖,而这几个依赖从 JDK 9 开始时是选配模块,所以高版本的用户需要添加这几个依赖才能正常使用。

4. 基础工作准备

接下来准备下几个基础工作,就是新建个实体、模拟个数据库,写个 JWT 工具类这种基础操作。

4.1 UserEntity.java

关于 role 为什么使用 GrantedAuthority 说明下:其实是为了简化代码,直接用了 Security 现成的 role 类,实际项目中我们肯定要自己进行处理,将其转换为 Security 的 role 类。

4.2 ResponseEntity.java

前后端分离为了方便前端我们要统一 json 的返回格式,所以自定义一个 ResponseEntity.java。

4.3 Database.java

这里我们使用一个 HashMap 模拟了一个数据库,密码我已经预先用 Bcrypt 加密过了,这也是 Spring Security 官方推荐的加密算法(MD5 加密已经在 Spring Security 5 中被移除了,不安全)。

用户名
密码
权限

jack

jack123 存 Bcrypt 加密后

ROLE_USER

danny

danny123 存 Bcrypt 加密后

ROLE_EDITOR

smith

smith123 存 Bcrypt 加密后

ROLE_ADMIN

4.4 UserService.java

这里再模拟一个 service,主要就是模仿数据库的操作。

4.5 JwtUtil.java

自己编写的一个工具类,主要负责 JWT 的签名和鉴权。

5. Spring Security 改造

登入这块,我们使用自定义的 JwtAuthenticationFilter 来进行登入。

请求鉴权,我们使用自定义的 JwtAuthorizationFilter 来处理。

也许大家觉得两个单词长的有点像,😜。

5.1 UserDetailsServiceImpl.java

我们首先实现官方的 UserDetailsService 接口,这里主要负责一个从数据库拿数据的操作。

后序我们还需要对其进行缓存改造,不然每次请求都要从数据库拿一次数据鉴权,对数据库压力太大了。

5.2 JwtAuthenticationFilter.java

这个过滤器主要处理登入操作,我们继承了 UsernamePasswordAuthenticationFilter,这样能大大简化我们的工作量。

private void handleResponse() 此处处理的方法不是很好,我的想法是跳转到控制器中进行处理,但是这样鉴权成功的 token 带不过去,所以先这么写了,有点复杂。

5.3 JwtAuthorizationFilter.java

这个过滤器处理每个请求鉴权,我们选择继承 BasicAuthenticationFilter ,考虑到 Basic 认证和 JWT 比较像,就选择了它。

5.4 SecurityConfiguration.java

此处我们进行 Security 的配置,并且实现缓存功能。缓存这块我们使用官方现成的 CachingUserDetailsService ,唯独的缺点就是它没有 public 方法,我们不能正常实例化,需要曲线救国,下面代码也有详细说明。

5.5 Ehcache 配置

Ehcache 3 开始,统一使用了 JCache,就是 JSR107 标准,网上很多教程都是基于 Ehcache 2 的,所以大家可能在参照网上的教程会遇到很多坑。

JSR107:emm,其实 JSR107 是一种缓存标准,各个框架只要遵守这个标准,就是现实大一统。差不多就是我不需要更改系统代码,也能随意更换底层的缓存系统。

在 resources 目录下创建 ehcache.xml 文件:

application.properties 中开启缓存支持:

5.6 统一全局异常

我们要把异常的返回形式也统一了,这样才能方便前端的调用。

我们平常会使用 @RestControllerAdvice 来统一异常,但是它只能管理 Controller 层面抛出的异常。Security 中抛出的异常不会抵达 Controller,无法被 @RestControllerAdvice 捕获,故我们还要改造 ErrorController

6. 测试

写个控制器试试,大家也可以参考我控制器里面获取用户信息的方式,推荐使用 @AuthenticationPrincipal 这个注解!!!

我这里还使用了 @IsAdmin 注解,@IsAdmin 注解如下:

这样能省去每次编写一长串的 @PreAuthorize() ,而且更加直观。

7. FAQ

7.1 如何解决JWT过期问题?

我们可以在 JwtAuthorizationFilter 中加点料,如果用户快过期了,返回个特别的状态码,前端收到此状态码去访问 GET /re_authentication 携带老的 token 重新拿一个新的 token 即可。

7.2 如何作废已颁发未过期的 token?

我个人的想法是把每次生成的 token 放入缓存中,每次请求都从缓存里拿,如果没有则代表此缓存报废。

最后更新于

这有帮助吗?