本文作者是Mauricio Urraco,全栈开发者 @ XOOR
你可以在这里找到本教程的代码。
让我们先来了解一下这个项目的文件结构:
文件结构
spring-boot-jwt/
│
├─ src/main/java/
│ └─ murraco
│ ├─ configuration
│ │ └─ SwaggerConfig.java
│ │
│ ├─ controller
│ │ └── UserController.java
│ │
│ ├─ dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├─ exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├─ model
│ │ ├── Role.java
│ │ └── User.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
│
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
简介 (https://jwt.io)
首先,我们先来看一下来自jwt.io的简介:
什么是 JSON Web Token?
JSON Web Token (JWT) 是一个开放标准 (RFC 7519),定义了一种紧凑且自包含的方式,用于安全地在各方之间传输 JSON 对象。这些信息可以被验证和信任,因为它们是数字签名的。JWT 可以使用秘密 (使用 HMAC 算法) 或使用 RSA 的公钥/私钥对进行签名。
接下来我们来详细解释一下这个定义中的一些概念。
紧凑 (Compact):由于它们更小的大小,JWT 可以通过 URL、POST 参数或 HTTP 头部内发送。此外,由于其更小的大小,传输速度也更快。
自包含 (Self-contained):有效载荷包含有关用户的所有所需信息,避免了多次查询数据库的需求。
什么时候应该使用 JSON Web Token?
下面是一些使用 JSON Web Token 有用的场景:
身份验证 (Authentication):这是使用 JWT 最常见的场景。一旦用户登录成功,每个后续请求都将包含 JWT,允许用户访问允许使用该令牌的路由、服务和资源。现在,单点登录是一个广泛使用 JWT 的功能,因为它的开销小且易于在不同域之间使用。
信息交换 (Information Exchange):JSON Web Token 是在各方之间安全传输信息的好方法。由于 JWT 可以被签名 - 例如,使用公钥/私钥对 - 你可以确定发送方是其所说的人。此外,由于签名是使用头部和有效载荷计算的,因此你还可以验证内容未被篡改。
JSON Web Token 的结构是什么?
JSON Web Token 由三部分组成,由点 (.) 分隔,它们是:
- 头部 (Header)
- 有效载荷 (Payload)
- 签名 (Signature)
因此,一个 JWT 通常如下所示。
xxxxx
.yyyyy
.zzzzz
接下来,我们将分解不同的部分。
头部 (Header)
头部通常由两部分组成:令牌类型(JWT)和使用的哈希算法,例如 HMAC SHA256 或 RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,将此 JSON 进行 Base64Url 编码,以形成 JWT 的第一部分。
有效载荷 (Payload)
令牌的第二部分是有效载荷,其中包含声明 (Claims)。声明是关于实体 (通常是用户) 和其他元数据的陈述。声明有三种类型:保留 (Reserved)、公共 (Public) 和私有 (Private) 声明。
- 保留声明 (Reserved Claims):这些是一组预定义的声明,它们不是强制性的,但建议提供一组有用的、可互操作的声明。其中一些是:iss (发行者),exp (过期时间),sub (主题),aud (受众) 等。
请注意,声明名称仅为三个字符长,因为 JWT 应该是紧凑的。
- 公共声明 (Public Claims):这些可以由使用 JWT 的人自行定义。但为了避免冲突,它们应在 IANA JSON Web Token 注册表中定义,或者定义为包含冲突防止命名空间的 URI。
- 私有声明 (Private Claims):这些是为共享信息而创建的自定义声明,由同意使用它们的各方创建。
有效载荷的一个示例可能是:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后,将有效载荷进行 Base64Url 编码,以形成 JSON Web Token 的第二部分。
签名 (Signature)
要创建签名部分,必须使用编码的头部、编码的有效载荷、一个秘密、头部中指定的算法进行签名。
例如,如果要使用 HMAC SHA256 算法,则将创建如下签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证 JWT 的发送方是其所说的人,并确保消息在传输过程中没有被更改。将所有内容放在一起
输出是由三个由点分隔的 Base64 字符串组成的,可以在 HTML 和 HTTP 环境中轻松传递,而与基于 XML 的标准(例如 SAML)相比更为紧凑。
以下显示了一个已编码头部和有效载荷,并使用秘密签名的 JWT。已编码 JWT:
JSON Web Token 是如何工作的?
在身份验证中,当用户成功使用其凭据登录时,将返回 JSON Web Token 并必须在本地保存(通常在本地存储中,但也可以使用 cookie),而不是创建服务器上的会话并返回 cookie 的传统方法。
每当用户想要访问受保护的路由或资源时,用户代理应发送 JWT,通常在 Authorization 头部中使用 Bearer 模式。头部的内容应如下所示:
Authorization: Bearer <token>
这是一种无状态的身份验证机制,因为用户状态从未保存在服务器内存中。服务器的受保护路由将检查 Authorization 头部中是否存在有效的 JWT,如果存在,则允许用户访问受保护的资源。由于 JWT 是自包含的,所有必要的信息都在那里,因此减少了多次查询数据库的需要。``` 这使你能够完全依赖无状态的数据API,甚至向下游服务发出请求。无论哪个域正在提供你的API,跨域资源共享(CORS)都不是问题,因为它不使用cookie。
下图展示了这个过程:
JWT身份验证摘要
基于令牌的身份验证模式近来变得非常流行,因为与会话/cookie相比,它们提供了重要的好处:
- CORS
- 无需CSRF保护
- 更好的移动端集成
- 减少授权服务器负载
- 无需分布式会话存储
这种方法需要做出一些权衡:
- 更容易受到XSS攻击
- 访问令牌可能包含过时的授权声明(例如,当撤销某些用户权限时)
- 访问令牌在声明数量增加时可能会增大
- 文件下载API可能难以实现
- 真正的无状态和吊销是相互排斥的
JWT身份验证流程非常简单
- 用户通过向授权服务器提供凭据来获取刷新和访问令牌
- 用户在每个请求中发送访问令牌以访问受保护的API资源
- 访问令牌被签名并包含用户身份(例如,用户ID)和授权声明。 需要注意的是,授权声明将与访问令牌一起包含。为什么这很重要?假设授权声明(例如,数据库中的用户特权)在访问令牌的生命周期内发生更改。这些更改在新的访问令牌发行之前不会生效。在大多数情况下,这不是大问题,因为访问令牌的寿命很短。否则,请使用不透明令牌模式。
实现细节
让我们看看如何使用Java和Spring实现基于JWT令牌的身份验证,并尝试重用Spring安全默认行为。Spring Security框架提供了插件类,可以处理授权机制,如:会话cookie,HTTP Basic和HTTP Digest。尽管如此,它缺乏原生支持JWT,我们需要自己动手让它工作。
MySQL数据库
此演示目前使用由Spring Boot自动配置的名为user_db的MySQL数据库。如果要连接到另一个数据库,你必须在资源目录中的application.yml
文件中指定连接。请注意,hibernate.hbm2ddl.auto=create-drop
将在每次部署时删除并创建一个干净的数据库(如果你在实际项目中使用此功能,则可能需要更改它)。以下是项目中的示例:
代码
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtTokenFilter
JwtTokenFilter
过滤器应用于每个API(/**
),但不包括signin token端点(/users/signin
)和singup端点(/users/signup
)。
此过滤器具有以下职责:
- 检查Authorization头中的访问令牌。如果在头中找到访问令牌,则委托身份验证给
JwtTokenProvider
,否则抛出身份验证异常。 - 根据JwtTokenProvider执行的身份验证过程的结果调用成功或失败策略
请确保在成功身份验证时调用
chain.doFilter(request, response)
。你希望请求的处理进入下一个过滤器,因为最后一个过滤器_FilterSecurityInterceptor#doFilter_实际上负责调用处理所请求的API资源的控制器中的方法。
JwtTokenFilterConfigurer
将JwtTokenFilter
添加到spring boot security的DefaultSecurityFilterChain
中。
JwtTokenProvider
JwtTokenProvider
具有以下职责:
- 验证访问令牌的签名
- 从访问令牌中提取身份和授权声明,并使用它们创建UserContext
- 如果访问令牌格式不正确,过期或仅仅是未使用适当的签名密钥签名,则会抛出身份验证异常。 MyUserDetails
实现UserDetailsService
以定义我们自己的自定义loadUserbyUsername函数。UserDetailsService接口用于检索与用户相关的数据。它有一个名为loadUserByUsername()的方法,该方法根据用户名查找用户实体,并可以重写以自定义查找用户的过程。
在身份验证期间,DaoAuthenticationProvider
使用它来加载有关用户的详细信息。
WebSecurityConfig
WebSecurityConfig
类扩展WebSecurityConfigurerAdapter
以提供自定义安全配置。
在此类中配置和实例化以下bean:
JwtTokenFilter
PasswordEncoder
此外,在WebSecurityConfig#configure(HttpSecurity http)
方法中,我们将配置模式以定义受保护/未受保护的API端点。请注意,我们已禁用CSRF保护,因为我们不使用Cookies。
结论
正如本文所示,借助Spring Boot和Spring Security,我们可以在短时间内拥有一个JWT身份验证服务。如果利用Spring Cloud和Netflix Stack:Eureka,Zuul,Ribbon和Hystrix,这也非常容易集成到微服务生态系统中(我们将在未来的文章中看到更多信息)。同时,请记得检查GitHub上的代码以尝试它,或将JWT身份验证添加到你自己的项目中!
译自:https://medium.com/@xoor/jwt-authentication-service-44658409e12c
评论(0)