本文翻译自:http://www.svlada.com/jwt-token-authentication-with-spring-boot/
场景介绍
软件安全是一件很负责的问题,由于微服务系统中每个服务都要处理安全问题,所以在微服务场景下会更加复杂,一般我们会四种面向微服务系统的身份验证方案。
在传统的单体架构中,单个服务保存所有的用户数据,可以校验用户,并在认证成功后创建HTTP会话。在微服务架构中,用户是在和服务集合交互,每一个用户都有可能需要知道请求的用户是谁。一种简单的方案是在微服务中,采用与单体系统中相同的模式,但问题是如何让所有的服务访问用户的数据
解决这个问题大致2个思路:(1)使用共享数据库时,更新数据库表会成为一个难题,因为所有的服务必须同时升级以便能够对接修改后的表解构;(2)将相同的数据分发给所有的服务时,当某个用户已经被认证,如何让每个服务都知晓这个状态是个问题。
方案1:单点登录(SSO)方案, 采用单点登录方案,意味着每个面向用户的服务都必须与认证服务交互,这会产生大量非常琐碎的网络流量,同时这个防范实现起来也相当的复杂。在其他方面,选择SSO方案安全性会很好,用户登录状态是不透明的,可防止攻击者从状态中推断任何有用的信息。
方案2:分布式会话方案,原理主要是将关于用户信息存储在共享内存中,并通常由用户会话作为key来实现简单的分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。该方案的另外一个优点就是用户登录状态不是透明的。当使用分布式数据库时,它也是一个高度可用且可可扩展的解决方案。这种方案的优点是在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的负责性了。
方案3:token客户端令牌方案,此令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每一个请求上,为微服务提供身份验证。这种解决方案安全性相对较好,但身份验证注销是一个大大的问题,缓解这种情况的方法可以使用短期令牌access_token 和频繁检查认证服务器等。对于客户端令牌的编码方案,可以使用 JSON Web Tokens( JWT), 它足够简单且支持程度也比较好。
方案4:客户端令牌与API网关结合,这个方案意味着所有的请求都通过网关,从而有效地隐藏了微服务。在请求时,网关将原始用户令牌转换为内部会话(session)ID令牌。在这种情况下,注销就不在是个大大的问题, 因为网关在注销时可以撤销用户的令牌。这种方案虽然支持程度比较好,但是实现起来还是可能相当的复杂。
个人推荐方案:客户端令牌(JWT) + API网关结合的方案,因为这个方案通常使用起来比较容易,且性能也不错。SSO方案虽然能满足需求,但尽量避免使用,若分布式会话方案所需要的相关技术已经应用在你的场景中,那么这个方案也是比较有趣的。在考虑方案的时候,应该考虑注销的重要性。
api网关,参考这篇,
http://geek.csdn.net/news/detail/104715
http://www.dockerinfo.net/773.html
JWT介绍
这篇文章将会知指导你如何用spring boot实现JWT的授权。
文章将会涉及到下面2个方面:
1. Ajax 授权
2. JWT token 授权
前提
请在你细读本篇文章的时候,先看看Github 上的简单项目:https://github.com/svlada/springboot-security-jwt 。
这个项目是使用H2 内存数据库来存储简单的用户信息。为了让事情变的更加简单,我已经配置了spring boot在自动加载Application自动启动的时候,已经创建了数据设备和配置了spring boot的相关配置(/ jwt-demo / src / main /resources/ data.sql)。
先预览一下下面的项目结构:
+—main
| +—java
| | —com
| | —svlada
| | +—common
| | +—entity
| | +—profile
| | | —endpoint
| | +—security
| | | +—auth
| | | | +—ajax
| | | | —jwt
| | | | +—extractor
| | | | —verifier
| | | +—config
| | | +—endpoint
| | | +—exceptions
| | | —model
| | | —token
| | —user
| | +—repository
| | —service
| —resources
| +—static
| —templates
Ajax 授权
当我们在谈论Ajax授权的时候,我们通常会联想到的是用户是通过JSON 的方式提供凭证,并以XMLHttpRequest 的方式发送的场景。
在本教程的第一部分,Ajax实现身份验证是遵循Spring Security 框架的标准模式。
下面列表的东西,将是要我们去实现的:
1. AjaxLoginProcessingFilter
2. AjaxAuthenticationProvider
3. AjaxAwareAuthenticationSuccessHandler
4. AjaxAwareAuthenticationFailureHandler
5. RestAuthenticationEntryPoint
6. WebSecurityConfig
在我们实现这些细节的时候,让我们细看一下,下面的 request/response 的授权流程。
Ajax 授权请求的例子
身份验证API允许用户传入凭据,来获得身份验证令牌token。
在我们的例子中,客户端启动验证过程通过调用身份验证API(/API/auth/login)。
我们可以写这样的Http 请求:
POST /api/auth/login HTTP/1.1
Host: localhost:9966
X-Requested-With: XMLHttpRequest
Content-Type: application/json
Cache-Control: no-cache
{
"username": "[email protected]",
"password": "test1234"
}
终端可以用CURL:
curl -X POST -H "X-Requested-With: XMLHttpRequest" -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{
"username": "[email protected]",
"password": "test1234"
}' "http://localhost:9966/api/auth/login"
Ajax 授权相应的例子
如果客户端请求的凭证被通过,授权API将会返回Http响应包括下面的一些细节:
1. Http 状态 "200 OK"
2. 带有 JWT的access_toke 和 refresh_token 都会在 response body中被包含了。
JWT Refresh token
用来获取新的 access_token, 刷新token 可以用这样的API来处理:/api/auth/token.(刷新可以用来防止access_token 的过期)
获取的HTTP 响应格式:
{
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMDMzMzA4LCJleHAiOjE0NzIwMzQyMDh9.41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ",
"refreshToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfUkVGUkVTSF9UT0tFTiJdLCJpc3MiOiJodHRwOi8vc3ZsYWRhLmNvbSIsImp0aSI6IjkwYWZlNzhjLTFkMmUtNDg2OS1hNzdlLTFkNzU0YjYwZTBjZSIsImlhdCI6MTQ3MjAzMzMwOCwiZXhwIjoxNDcyMDM2OTA4fQ.SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg"
}
JWT Access Token
JWT访问令牌可用于身份验证和授权:
1. 身份验证是由验证JWT访问令牌签名。如果签名是有效的,访问API请求的资源是理所当然。
2. 授权是通过查找特权JWT scope属性的值来判断。(译者:scope,一般会是版本号或平台,安卓,ios,wap等或不同的系统的id, 具体看自家的场景和需求).
解码JWT token有三个部分:Header(请求头), Claims(要求) and Signature(签名)
Header
{
"alg": "HS512"
}
Claims(要求):
{
"sub": "[email protected]",
"scopes": [
"ROLE_ADMIN",
"ROLE_PREMIUM_MEMBER"
],
"iss": "http://svlada.com",
"iat": 1472033308,
"exp": 1472034208
}
签名base64 encoded)
41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ
JWT Refresh Token
Refresh token 是长寿令牌用于请求新的访问令牌。Refresh token过期时间是超过access_token的过期时间。
在本次教程中,我们将使用 jti 声称来维持黑名单,或撤销令牌列表。JWT ID(jti) 声称被 RFC7519 定义了,目的是为了唯一地标识单个刷新令牌。
解码刷新令牌有三个部分: Header(请求头), Claims(要求), Signature(签名) 如下所示:
Header:
{
"alg": "HS512"
}
Claims:
{
"sub": "[email protected]",
"scopes": [
"ROLE_REFRESH_TOKEN"
],
"iss": "http://svlada.com",
"jti": "90afe78c-1d2e-4869-a77e-1d754b60e0ce",
"iat": 1472033308,
"exp": 1472036908
}
Signature (base64 encoded)
SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg
AjaxLoginProcessingFilter( Ajax 登录处理过滤器)
首先,需要继承AbstractAuthenticationProcessingFilter, 目的是为了提供一般常用的Ajax 身份验证请求。反序列化JSON和基本验证的主要任务都是在的。AjaxLoginProcessingFilter#attemptAuthentication这个方法里完成的。在成功验证JSON的主要检验逻辑是委托给AjaxAuthenticationProvider类实现。
在一个成功校验中, AjaxLoginProcessingFilter#successfulAuthentication 方法会被调用。
在一个失败的检验中,AjaxLoginProcessingFilter#unsuccessfulAuthentication 方法被调用。
public class AjaxLoginProcessingFilter