spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
为了提供一些背景知识,我们有一个精彩的介绍,由jwt.io提供!我们来看一下:
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。 JWT 可以使用秘密(使用 HMAC 算法)或使用 RSA 的公钥/私钥对进行签名。
让我们进一步解释这个定义的一些概念。
紧凑:由于 JWT 尺寸较小,因此可以通过 URL、POST 参数或在 HTTP 标头内发送。此外,较小的尺寸意味着传输速度更快。
自包含:有效负载包含有关用户的所有必需信息,避免多次查询数据库。
以下是 JSON Web 令牌有用的一些场景:
身份验证:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够轻松地跨不同域使用。
信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。因为 JWT 可以进行签名(例如,使用公钥/私钥对),所以您可以确定发送者就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
JSON Web Token 由点(.)分隔的三部分组成,它们是:
因此,JWT 通常如下所示。
xxxxx
。 yyyyy
。 zzzzz
让我们分解不同的部分。
标头
标头通常由两部分组成:令牌的类型(JWT)和所使用的哈希算法(例如 HMAC SHA256 或 RSA)。
例如:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
然后,对该 JSON 进行 Base64Url 编码以形成 JWT 的第一部分。
有效载荷
令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和附加元数据的声明。索赔分为三种类型:保留索赔、公开索赔和私人索赔。
请注意,声明名称只有三个字符长,因为 JWT 旨在紧凑。
公共声明:这些可以由使用 JWT 的人随意定义。但为了避免冲突,它们应该在 IANA JSON Web 令牌注册表中定义,或者定义为包含防冲突命名空间的 URI。
私人声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明。
有效负载的示例可以是:
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
然后对有效负载进行 Base64Url 编码以形成 JSON Web 令牌的第二部分。
签名
要创建签名部分,您必须获取编码的标头、编码的有效负载、秘密、标头中指定的算法,然后对其进行签名。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证 JWT 的发送者是否是其所说的发送者,并确保消息在此过程中没有被更改。将所有内容放在一起
输出是三个由点分隔的 Base64 字符串,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(例如 SAML)相比更加紧凑。
下面显示了一个 JWT,它具有先前的标头和有效负载编码,并且使用密钥进行签名。编码 JWT
在身份验证中,当用户使用其凭据成功登录时,将返回一个 JSON Web Token,并且必须保存在本地(通常在本地存储中,但也可以使用 cookie),而不是传统的在 Web 中创建会话的方法。服务器并返回一个cookie。
每当用户想要访问受保护的路由或资源时,用户代理应该发送 JWT,通常使用承载模式在授权标头中发送。标头的内容应如下所示:
Authorization: Bearer <token>
这是一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。服务器的受保护路由将检查授权标头中是否存在有效的 JWT,如果存在,则将允许用户访问受保护的资源。由于 JWT 是独立的,所有必要的信息都在那里,减少了多次查询数据库的需要。
这使得您可以完全依赖无状态的数据API,甚至可以向下游服务发出请求。哪些域为您的 API 提供服务并不重要,因此跨源资源共享 (CORS) 不会成为问题,因为它不使用 cookie。
下图展示了这个过程:
基于令牌的身份验证模式最近变得非常流行,因为与会话/cookie 相比,它们提供了重要的好处:
使用这种方法必须做出一些权衡:
JWT 身份验证流程非常简单
请务必注意,授权声明将包含在访问令牌中。为什么这很重要?好吧,假设授权声明(例如数据库中的用户权限)在访问令牌的生命周期内发生了更改。在颁发新的访问令牌之前,这些更改不会生效。在大多数情况下,这不是大问题,因为访问令牌是短暂的。否则就采用不透明的令牌模式。
让我们看看如何使用 Java 和 Spring 实现基于 JWT 令牌的身份验证,同时尝试尽可能重用 Spring 安全默认行为。 Spring Security 框架附带了已经处理授权机制的插件类,例如:会话 cookie、HTTP Basic 和 HTTP Digest。然而,它缺乏对 JWT 的原生支持,我们需要亲自动手才能使其工作。
该演示当前使用名为test_db的 H2 数据库,因此您可以快速且开箱即用地运行它,而无需进行太多配置。如果要连接到不同的数据库,则必须在资源目录内的application.yml
文件中指定连接。请注意, hibernate.hbm2ddl.auto=create-drop
将在每次部署时删除并创建一个干净的数据库(如果您在实际项目中使用它,您可能需要更改它)。以下是项目中的示例,看看如何轻松地交换url
和dialect
属性上的注释以使用您自己的 MySQL 数据库:
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtToken过滤器
JwtTokenFilter
过滤器应用于每个 API ( /**
),但登录令牌端点 ( /users/signin
) 和 singup 端点 ( /users/signup
) 除外。
该过滤器具有以下职责:
JwtTokenProvider
否则抛出身份验证异常请确保在身份验证成功后调用chain.doFilter(request, response)
。您希望处理请求前进到下一个过滤器,因为最后一个过滤器FilterSecurityInterceptor#doFilter负责实际调用控制器中处理请求的 API 资源的方法。
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilter配置器
将JwtTokenFilter
添加到 spring boot security 的DefaultSecurityFilterChain
中。
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
JwtTokenProvider
JwtTokenProvider
具有以下职责:
我的用户详细信息
实现UserDetailsService
以定义我们自己的自定义loadUserbyUsername函数。 UserDetailsService
接口用于检索与用户相关的数据。它有一个名为loadUserByUsername的方法,该方法根据用户名查找用户实体,并且可以重写以自定义查找用户的过程。
DaoAuthenticationProvider
使用它在身份验证期间加载有关用户的详细信息。
网络安全配置
WebSecurityConfig
类扩展WebSecurityConfigurerAdapter
以提供自定义安全配置。
在此类中配置并实例化了以下 bean:
JwtTokenFilter
PasswordEncoder
此外,在WebSecurityConfig#configure(HttpSecurity http)
方法中,我们将配置模式来定义受保护/不受保护的 API 端点。请注意,我们已禁用 CSRF 保护,因为我们不使用 Cookie。
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
确保您已安装 Java 8 和 Maven
分叉此存储库并克隆它
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
以检查一切是否正常工作。您可以更改application.yml
文件中的默认端口 server :
port : 8080
/users/me
发出 GET 请求以检查您是否未通过身份验证。由于您尚未设置有效的 JWT 令牌,您应该会收到403
响应,其中包含Access Denied
消息 $ curl -X GET http://localhost:8080/users/me
/users/signin
发出 POST 请求,以获取有效的 JWT 令牌 $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
发出初始 GET 请求 $ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}