JavaWeb 中,客户端通常是浏览器,通过浏览器向服务器发送请求,所以有时候称客户端为浏览器。
Cookie
Cookie 是服务器生成,存储在浏览器的一段字符串,可以记录用户的身份信息等数据,但是这些信息是明文存储的,很不安全。
浏览器把 cookie 以 k-v 形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该 cookie 发送给服务器。由于 cookie 是存在于浏览器上的,所以浏览器加入了一些限制来确保 cookie 不会被恶意使用,同时不会占据太多磁盘空间,单个 cookie 保存的数据不能超过 4KB,而且每个域的 cookie 数量是有限的。
Cookie 缺陷:
cookie 存在本地浏览器,浏览器之间的 cookie 不共享,所以用户换了浏览器就要重新登录
Session
Session 是基于 Cookie 实现的,是一种 HTTP 存储机制,目的是为无状态的 HTTP 协议提供持久机制 ,服务器会为每个用户创建的一个临时会话对象,存储在服务器,它保存了每个用户的会话信息。相较于 Cookie,session 容量更大,可以保存 Object。
一次 session 会话可以有多个请求也就有多个 cookie 的交互,服务器为了区分当前是谁发送的请求,给每个浏览器分配了不同的"身份标识"JSESSIONID,JSESSIONID 放在 cookie 中,然后浏览器每次向服务器发请求的时候,都带上 JSESSIONID,服务器就可以根据 JSESSIONID 查找用户对应的 session 会话,所以 session 是有状态的(因为服务器要保存 session)。
服务器使用 session 把用户的信息临时保存在了服务器上,这种用户信息存储方式相对 cookie 来说更安全。
Session 在两种情况下会自动销毁:
客户端关闭浏览器程序
Session 超时(会话超时):客户端一段时间内没有访问过该 Session 内存,服务器会清理。回话超时的时间,默认是 30 分钟,只要发起请求,会话时间从 0 重新计算。 Session 缺陷:
如果 web 服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候 session 就会丢失,也就是每个服务器的 session 不共享。
服务器要保存所有人的 session,如果服务器访问量大,那么对服务器来说有很大的内存开销,限制了服务器的扩展能力,是空间换时间
误解: 关闭浏览器 ,服务器的 session 不会立刻消失!
对 session 来说,除非浏览器通知服务器删除 session,否则服务器会一直保留。
然而浏览器从来不会在关闭之前主动通知服务器它将要关闭,大部分 session 机制都使用会话 cookie 来保存 JSESSIONID,而关闭浏览器后这个 JSESSIONID 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 JSESSIONID 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器需要为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就可以以为客户端已经停止了活动,才会把 session 删除以节省存储空间。
CSRF 跨站请求伪造
用户已登录网站 A,Cookie 中存有网站 A 的会话凭证,攻击者诱导用户访问恶意网站 B,此时请求自动携带了网站 A 的 Cookie,攻击者就可以通过 Cookie 做违法操作。
核心问题:浏览器请求会自动携带 Cookie,简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。CSRF 并不能够拿到用户的任何信息,只是以用户浏览器身份进行操作。
XSS 跨站脚本攻击
攻击者向网页中注入恶意脚本(通常是 JavaScript),当其他用户浏览该页面时,脚本在用户浏览器中执行,从而窃取信息、冒充用户操作等。
核心问题:信任用户输入,未对输出进行安全转义。
假如论坛允许用户发帖,但未对内容做 HTML 转义:
|
|
当用户 B(已登录银行网站)浏览该帖子时,浏览器会执行这段脚本,自动向银行发起转账请求(因为 Cookie 自动附带),导致资金被盗。
Token
基于 session 的缺陷,我们的解决方案是怎么让服务器不保存 session,而让客户端去保存,可是如果不保存 JSESSIONID, 怎么验证客户端发给服务器 JSESSIONID 的确是服务器生成的呢?
关键点就在于验证,比如说, A 已经登录了系统, 服务器给 A 发一个令牌(token), 里边包含了 A 的 user id 等信息, 下一次 A 再次通过 Http 请求访问服务器的时候, 把这个 token 通过 Http header 带过来就可以了。 不过这样 token 和 session id 没有本质区别,任何人都可以可以伪造 token, 所以得想办法, 让别人伪造不了 token,那就对数据做一个签名吧! 比如说服务器用 HMAC-SHA256 算法,加上一个只有服务器才知道的密钥,对数据做一个加密签名, 把这个签名和数据一起作为 token ,由于密钥别只有服务器知道, 那么伪造 token 就能被识别出来了。
这个 token 服务器不保存, 当 A 把这个 token 发给服务器的时候,服务器再用同样的 HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名验证。
这样一来, 服务器就不保存 session 了, 服务器只生成 token , 然后验证 token , 服务器用 CPU 计算时间获取了 session 存储空间 !
解除了 session 这个负担, 服务器机器集群可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。 这种无状态(服务器不用存储 session,只负责签名和验证)的感觉实在是太好了!

Token 缺陷:
- Token 中的数据是明文保存的(虽然会用 Base64 编码, 但那不是加密), 还是可以被别人看到的, 所以不能在其中保存像密码这样的敏感信息。
- 如果一个人的 token 被别人偷走了, 那服务器也会认为小偷是合法用户, 这其实和一个人的 session id 被别人偷走是一样的。
Token 特点:
- 无状态、可扩展
- 支持移动设备
- 跨域调用
JWT
Json Web Token JSON Web Token Introduction - jwt.io
JWT 实现登录原理图

上面的 token 算是一种理论技术,而 JWT 算是对 token 的一种落地实现,jwt 只通过加密算法实现对 Token 合法性的验证,不依赖数据库等存储系统(无状态),因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的 Token 可以互相验证。
用途
- 授权:这是 JWT 最广泛的应用场景。一次用户登录,后续请求将会包含 JWT,对于那些合法的 token,允许用户连接路由,服务和资源。目前 JWT 广泛应用在 SSO(Single Sign On 单点登录)上。因为他们开销很小并且可以在不同领域轻松使用。
- 信息交换:JWT 是一种在各方面之间安全信息传输的好的方式 ,因为 JWT 可以签名,例如,使用公钥/私钥对,可以确定发件人是否正确。 此外,使用标头和有效负载计算签名,可以验证内容是否未被篡改。
JWT 组成
一个 JWT 由三部分组成,各部分以点分隔
Header(头部) —–base64Url 编码的 Json 字符串
Payload(载荷)—–base64url 编码的 Json 字符串
Signature(签名)—–使用指定算法,通过 Header 和 Playload加盐计算的字符串
Header
此部分有两部分组成:
- 一部分是 token 的类型,目前只能是 JWT
- 另一部分是签名算法,比如 HMAC 、 SHA256 、 RSA
示例:
|
|
Payload
payload 包含 claims(声明)
claims 是关于一个实体(通常是用户)和其他数据类型的声明,有三种类型:registered、public、private claims
Registered(已注册的声明):这些是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。
JWT 规定了 7 个官方字段,供选用:
iss (issuer):签发人
exp (expiration time)**:过期时间**
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At)**:签发时间**
jti (JWT ID):编号
除了官方字段,可以在 payload 部分定义私有字段,下面就是一个例子。
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把**秘密信息(密码,手机号等)**放在这个部分。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret**)**。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
base64Url 编码是为了 jwt 方便在网络中传输
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用 “.” 分隔,就可以返回给用户。
除了 HS256,还有RS256,RS256 是使用 RSA 私钥进行签名,使用 RSA 公钥进行验证。相比 HS256 更加安全。私钥只能被认证服务器拥有,只用来签名 JWT 不能用来校验,公钥是应用服务器使用来校验 JWT,但不能用来给 JWT 签名。
公钥一般不需要严密保管,因为即便黑客拿到了,也无法使用它来伪造签名。
JWT 使用方式
客户端收到服务器返回的 JWT ,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面,如 Authorization: Bearer jwt,其中 Bearer 算是一种 JWT 的默认声明,传输的时候要手动加上,在验证 JWT 的时候要把 Bearer 手动去掉。另一种做法是在跨域的时候把 JWT 放在 POST 请求的数据体里面。
JWT 缺陷:
服务器无法在使用过程中废止某个 token 或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑(JWT 的登出问题)
解决:
在用户登录成功后,把 jwt 存入 redis 并设置过期时间,用户再次访问时只需要在拦截器中验证 jwt 签名是否合法以及 redis 是否存在该 jwt,这两个条件都满足才能继续访问,如果 redis 不存在则说明 jwt 已过期删除,就需要重新登陆。
当用户退出登录时同样验证 jwt 签名是否合法,合法就把 redis 的 jwt 删除,此时用户退出成功!
这样就算用户退出登录时 jwt 还没过期,但是退出登录时已把 redis 的 jwt 删除,再次登录时 redis 中没有 jwt 就无法登录!
JWT 代码
导入 JWT 依赖
|
|
JWTUtils
|
|
JWTTest
|
|