1、前言
1.1、Token流程
先来回顾一下利用 token 进行用户身份验证的流程:
- 客户端使用用户名和密码请求登录
- 服务端收到请求,验证用户名和密码
- 验证成功后,服务端会签发一个 token,再把这个 token 返回给客户端
- 客户端收到 token 后可以把它存储起来,比如放到 cookie 中
- 客户端每次向服务端请求资源时需要携带服务端签发的 token,可以在 cookie 或者 header 中携带
- 服务端收到请求,然后去验证客户端请求里面带着的 token,如果验证成功,就向客户端返回请求数据
单点登录:一次登录多处使用
前提:单点登录多使用在分布式系统中
京东:单点登录,是将 token 放入到 cookie 中
案例:将浏览器的 cookie 禁用,则在登录京东则失效,无论如何登录不了
1.2、Token的优点
基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:
支持跨域访问:cookie 默认是不支持跨域的,而将 token 放在 HTTP 请求头中可以有效支持跨域请求。这是因为 HTTP 头是可以自定义的,而且标准的跨域资源共享(CORS)策略允许指定哪些头信息可以跨域传输。
无状态:Token 是无状态的,存储了所有必要的用户信息。这使得后端服务可以不存储状态信息,从而使架构更简单,更易于扩展。这种方式尤其适用于分布式系统和微服务架构,其中各个服务需要轻量级并独立处理请求。
更适用CDN:使用 token 认证的无状态特性意味着请求可以由任何服务器处理,包括通过 CDN 分发的服务器。这样可以减少对原始服务器的直接请求,提高响应速度并降低延迟。
更适用于移动端:在非浏览器环境,如移动应用或桌面应用中,cookie 支持可能有限或不稳定,使用 token 可以简化认证过程,因为开发者可以更灵活地控制如何存储和传输 token(例如存储在本地存储或内存中)。
无需考虑CSRF:由于 token 通常不会自动随请求发送,与 cookie 不同,它需要显式地附加到 HTTP 请求的头部。这种特性意味着不会自动发送到创建它们的同一域,因此大大降低了 CSRF 攻击的风险。
而JWT就是上述流程当中token的一种具体实现方式,其全称是 JSON Web Token
1.3、JWT Token
JWT(JSON Web Tokens)的本质是一个字符串,它通过一定的方式对用户信息进行封装、编码和安全处理。下面我会用一个更加生活化的比喻来解释JWT的工作原理和特性:
想象JWT是一封信件。信封上写着寄信人(头部Header),信的内容是关于寄信人的某些信息(载荷Payload),比如姓名、权限等。为了确保这封信在送达时内容没有被篡改,寄信人在信封上封了一层蜡,并在蜡上压上了自己的印章(签名Signature)。这样,收信人收到信后,只需检查印章是否完整,就能确定信件是否在途中被人篡改过。
在技术层面:
- 头部(Header):包含了使用的加密算法与令牌的类型,类似于“信封”的标识。
- 载荷(Payload):包含了要传输的信息,这些信息被称为“声明”(Claims),可以包含用户的身份信息以及其他任何需要的数据。
- 签名(Signature):服务器利用一个密钥对头部和载荷进行加密,加密后的字符串就是“印章”,用以确保数据在传输过程中未被篡改。
当JWT在用户和服务器之间传递时,服务器可以通过检查签名来验证信息是否被篡改,以及用户的合法性。这个机制使得JWT非常适合用于身份验证与信息交换,而且由于JWT的自包含性,服务器无需连接数据库即可完成验证,提高了处理速度和减少了系统开销。
JWT的认证流程如下:
1、前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探。
2、后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个 JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
3、后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的 JWT Token 即可
4、前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
5、后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
6、验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果
二、为什么要使用JWT
2.1、传统Session认证的弊端
1、用户认证的初次请求
用户提交认证信息:用户在客户端(如Web浏览器)输入用户名和密码,然后提交这些信息到服务器进行认证。
服务器处理认证:服务器接收到用户名和密码后,验证这些信息。如果认证成功,服务器需要创建一个会话来跟踪用户的状态,因为HTTP本身不会记住用户的状态或身份。
2、会话创建和会话ID的分发
创建会话和会话ID:认证成功后,服务器创建一个session,这个session包含有关用户的信息,如用户ID、权限等。每个session都会被分配一个唯一的session ID。
发送会话ID至客户端:一旦session创建,服务器通过HTTP响应将session ID作为cookie发送给用户的浏览器。服务器通过设置响应头中的
Set-Cookie
来实现这一点,cookie中会包含session ID。3、客户端存储和后续请求
客户端存储Cookie:用户的浏览器接收到这个cookie后,将其保存在本地。此后,每当浏览器向服务器发送请求时,它会自动在请求头中包含这个cookie。
服务器识别并处理请求:服务器每次接收请求时,会首先检查请求头中是否包含有效的session ID。服务器通过session ID查找相应的session对象,如果找到,就可以识别用户并获取用户的登录状态和其他相关信息。
4、维护用户会话状态
- 会话状态的持续性:通过这种方式,即使HTTP协议是无状态的,服务器也能“记住”用户在多个请求之间的状态。这意味着用户在浏览不同页面或进行不同操作时无需重复登录。
5、会话结束
- 结束会话:当用户选择登出或者session过期时(由服务器决定何时过期),服务器将终止session,并可能要求浏览器删除包含session ID的cookie。
通过这种机制,session认证允许Web应用跟踪用户的状态和身份,提供连贯的用户体验。然而,正如先前讨论的,这种方法在处理分布式系统、大量用户、移动应用或跨域请求时可能会面临一些挑战和限制。
这就是为什么在许多现代应用中,特别是需要弹性扩展和微服务架构的场景中,越来越倾向于使用基于token的认证方式,如JWT。
通过一个生活中例子来类比传统基于session的认证过程:
想象你去一个大型音乐会,入场时你需要出示购票凭证(这就像是用户在网站上输入用户名和密码)。一旦检票员验证了你的票是有效的,他们会在你的手腕上贴一个不易脱落的贴纸或戴上手环(这相当于服务器为用户创建session并发放一个session ID)。
一旦你有了这个手环,你就可以自由地进出会场的不同区域,不需要每次进入新区域都重新出示你的购票凭证。只要你的手上戴着那个手环,工作人员就可以通过看手环确认你的入场资格(这就像Web服务器通过检查cookie中的session ID来确认用户的身份和状态)。
每当你离开音乐会场地时,你可能会被要求剪掉手环,这表示你的访问权限已经结束,再次进入需要重新购买门票或验证(这与用户登出或session过期后服务器终止session相似)。
这个例子生动地说明了session的工作原理:一次验证,多次使用,直到会话结束。同样地,在Web环境中,用户通过一次登录验证后,可以在不需要重新验证的情况下访问多个页面和服务,直到session结束。
2.2、传统的session认证的问题
1. 服务器资源消耗大
每个用户登录后,服务器都必须为其创建并维护一个session。这些session信息通常存储在服务器的内存中。随着在线用户数量的增加,这将显著增加服务器的内存消耗。在用户规模较大的系统中,这会导致显著的性能瓶颈和成本增加。
2. 不利于分布式系统
在现代的云基础设施和微服务架构中,应用通常被部署在多个服务器或容器上以实现负载均衡和高可用性。由于session默认存储在一个服务器上,用户的后续请求若路由到不同的服务器,其session信息将不可用,除非通过集中式的session管理,如使用Redis等。这不仅增加了架构的复杂性,还可能引入新的性能瓶颈。
3. 移动端兼容性差
移动设备上的应用可能不支持cookie,或者对cookie的处理方式与桌面浏览器不同。这使得基于cookie的session认证方法在移动端应用中不那么有效,尤其是在原生应用中。
4. 安全隐患
Session认证依赖于cookie来存储session ID,如果cookie被截获(例如通过XSS攻击或者用户在不安全的网络环境中使用应用),攻击者可以利用这些信息冒充用户。此外,基于cookie的机制也容易受到CSRF攻击,尽管可以通过其他措施(如使用CSRF token)来缓解这一问题。
5. 前后端分离的挑战
在前后端分离的架构中,前端通常通过API与后端通信。每次API请求都需要携带用户的身份信息。由于cookie和session信息需要在多个中间件之间传递,这增加了维护和调试的复杂性,特别是在跨域场景下。
6. 跨域问题
基于cookie的session无法轻松处理跨域请求,因为浏览器出于安全考虑默认不会发送cookie到其他域。这限制了基于session的应用在单点登录和跨服务集成中的能力。
2.3、JWT认证的优势
1. 简洁和高效
JWT是一种非常紧凑的令牌格式。因为它只是一个由三部分组成的字符串——头部(Header)、载荷(Payload)和签名(Signature),每部分之间通过点(
.
)分隔。由于其紧凑的格式,JWT在网络中的传输效率非常高,适用于各种网络环境,尤其是带宽有限的情况。2. 跨平台和跨语言支持
JWT基于JSON格式,这使得它非常容易在不同的编程语言和平台中处理。几乎所有现代编程语言都支持JSON解析和生成,这样JWT可以轻松地在各种客户端和服务器端技术之间使用,无论是Web、桌面还是移动应用。
3. 无状态和适用于分布式微服务
JWT不需要在服务器端存储会话状态,因此它是无状态的。这一特性非常适合分布式系统和微服务架构,因为服务间的通信不需要每次都进行复杂的会话同步。每个服务只需要能够验证JWT的签名并解析其内容,就可以独立进行身份验证,大大简化了系统架构。
4. 单点登录友好
JWT非常适合实现单点登录(SSO),因为它可以生成一次并由多个不同的系统验证。由于JWT不依赖于cookie,它可以通过前端存储(如LocalStorage或SessionStorage)在客户端进行管理,也可以作为URL参数安全地在不同应用间传递,这使得跨域身份验证成为可能。
5. 适合移动端应用
在移动应用中,使用基于cookie的session认证往往不是最佳选择,因为移动平台对cookie的支持不如Web浏览器。JWT则可以在应用内部安全存储,如在设备的本地存储中,或者每次请求时通过HTTP头部发送,这使得它非常适合移动设备和原生应用。
三、JWT结构
其实是上面为什么使用JWT也有提到的它的结构 -->(标头(Header)、有效载荷(Payload)和签名(Signature))
确实,JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。
3.1、Header
JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
{
"alg": "HS256", // 指明签名使用的哈希算法,通常是HMAC SHA256或RSA。
"typ": "JWT" //表示令牌的类型,标准中规定为“JWT”
}
3.2、Payload
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:
{ "sub": "1234567890", "name": "zNuyoah", "admin": true }
注意:默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。
JWT只是适合在网络中传输一些非敏感的信息。
3.3、Signature
签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
总结:JWT由 Header加密.Payload加密.(Header加密.Payload加密)再加密三部分组成
注意:JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:
1、header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据。
2、signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意:secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值。
四、总结
4.1、什么是JWT
JSON Web Token,通过数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全的传输信息。
4.2、JWT有什么用
JWT最常用的场景就是授权认证,一旦用户登录,后续每个请求都将包含JWT,系统每次处理用户请求之前,都要进行JWT安全校验,通过之后再进行处理
4.3、JWT组成
JWT由三部分组成,用 . 拼接
1.Header
{
"alg": "HS256",
"typ": "JWT"
}
2.Payload
{
"sub": "1234567890",
"name": "zNuyoah",
"admin": true
}
3.Signature
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var Signature = HMACSHA256(encodedString, 'secret');
五、简单代码演示
1、POM
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、测试 - 加密(重新生成的都不一样的,因为里面包含超时时间,每次执行的时间都不是同一时刻)
@Test
public void test() {
JwtBuilder builder = Jwts.builder();
String jwtToken = builder
// header
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
// payload
.claim("username", "zNuyoah")
.claim("role", "admin")
// 设置主题
.setSubject("admin-test")
// 设置过期时间 - 一天
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 60 * 24)))
.setId(UUID.randomUUID().toString())
// signature
.signWith(SignatureAlgorithm.HS256, "admin")
.compact();
System.out.println(jwtToken);
}
3、测试 - 解密
@Test
public void parse() {
String token = "*************";
JwtParser parser = Jwts.parser();
Jws<Claims> claimsJws = parser.setSigningKey("admin").parseClaimsJws(token);
Claims claims = claimsJws.getBody();
System.out.println(claims.get("username")); // zNuyoah
System.out.println(claims.get("role")); // admin
System.out.println(claims.getId());
System.out.println(claims.getSubject()); // admin-test
System.out.println(claims.getExpiration());
}
六、实际开发应用
在Spring Boot项目中,结合JWT和Redis进行用户身份验证和会话管理是一种非常高效和安全的做法。
步骤 1: 生成并存储会话信息
在用户成功登录后,服务器生成一个随机的Token(如使用UUID)。这个Token作为用户会话的唯一标识,并将其与用户的详细信息一起存入Redis中。这里,Redis起到了存储用户会话状态的作用,有效地替代了传统的session机制。设置适当的过期时间后,这个Token将在一定时间后自动失效,保障了安全性。
实现提示:
- 使用
UUID.randomUUID().toString()
生成随机Token。- 使用Spring Data Redis或Jedis等库将Token和用户信息存储在Redis中。
- 设置Token的过期时间以控制会话有效期。
步骤 2: 生成JWT
将步骤1中生成的Token放入JWT的payload中,然后生成JWT。这个JWT将作为用户的身份凭证,传送给前端。
实现提示:
- 在JWT的payload中加入Token作为自定义声明(claim),例如
{ "token": "<random_token>" }
- 使用库如
jjwt
或java-jwt
来生成JWT。- 确保选择合适的加密算法和秘钥保护JWT的完整性和安全性。
步骤 3: 前端存储及传输JWT
前端在收到JWT后,通常会存储在本地存储(如localStorage或sessionStorage)中。每次发送请求到服务器时,将JWT添加到HTTP请求的Authorization头部中,通常采用
Bearer <token>
格式。实现提示:
- 确保前端在发送请求时正确地设置Authorization头部。
- 前端应该处理JWT过期或无效的情况,可能需要重新登录或刷新Token。
步骤 4: 后端验证JWT和会话
后端需要验证每个请求中的JWT的合法性。这包括解析JWT,验证签名,检查Token有效性等。验证通过后,从JWT解析出随机Token,并使用它从Redis中查询用户信息。
实现提示:
- 实现一个Spring Security的过滤器或者使用
@RestControllerAdvice
来处理所有进入的请求,从中解析和验证JWT。- 如果Redis中可以查询到与Token对应的用户信息,则认为用户已经登录,继续处理请求;如果查不到,返回错误响应,如401 Unauthorized。
- 考虑JWT或Token在Redis中过期的情况,合理处理这些异常情况。
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String JWT = request.getHeader("Authorization");
try {
// 1.校验JWT字符串
DecodedJWT decodedJWT = JWTUtils.decode(JWT);
// 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
...
return true;
}catch (SignatureVerificationException e){
System.out.println("无效签名");
e.printStackTrace();
}catch (TokenExpiredException e){
System.out.println("token已经过期");
e.printStackTrace();
}catch (AlgorithmMismatchException e){
System.out.println("算法不一致");
e.printStackTrace();
}catch (Exception e){
System.out.println("token无效");
e.printStackTrace();
}
return false;
}
}
七、小记
在实际开发中需要用下列手段来增加JWT的安全性:
1. 使用HTTPS传输
使用HTTPS可以有效防止中间人攻击(MITM),这种攻击方式包括但不限于网络监听和数据劫持。HTTPS通过对所有传输数据进行加密,确保数据在传输过程中的机密性和完整性。即使数据被拦截,攻击者也无法直接读取或篡改加密的内容。
实施方法:
- 在服务器配置SSL/TLS证书,强制使用HTTPS连接。
- 确保所有API端点只通过HTTPS提供服务。
- 配置HTTP严格传输安全(HSTS)来防止SSL剥离攻击。
2. 保证服务器的安全
服务器的安全是保护JWT安全的关键,因为JWT的签名密钥必须保持绝对的安全。如果服务器被攻破,攻击者可能获得签名密钥,从而可以伪造有效的JWT。
实施方法:
- 定期更新和打补丁操作系统和应用软件。
- 使用防火墙、入侵检测系统(IDS)和入侵防御系统(IPS)来防护服务器。
- 对服务器进行安全配置,限制不必要的服务和访问。
- 定期进行安全审计和渗透测试。
3. 定期更换哈希签名密钥
定期更换密钥可以降低密钥被破解的风险。如果密钥泄露或者被破解,定期更换密钥可以限制攻击者使用旧密钥造成的损害。
实施方法:
- 设计一个密钥轮换计划,例如每三个月更换一次密钥。
- 确保密钥更换过程中,旧密钥和新密钥在一段时间内共存,以确保在过渡期间不会因密钥更新而影响用户体验。
- 使用密钥管理系统来保护和管理密钥,确保密钥的安全性和更换的自动化。
4. 增加JWT的复杂性和安全性
除了上述措施,还可以通过增加JWT自身的复杂性和安全性来提高整体的安全水平。
实施方法:
- 使用较长的密钥长度,比如使用256位以上的密钥,以提高破解的难度。
- 对于非常敏感的应用,可以考虑对JWT的payload部分进行加密。
- 限制JWT的有效期,设置较短的过期时间,减少JWT被滥用的风险。