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攻击
跨站请求攻击,攻击者冒充用户的浏览器去访问一个用户曾经认证过的网站并运行一些操作,如发邮件、甚至财产操作。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。csrf并不能够拿到用户的任何信息,只是以用户浏览器身份进行操作。
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
示例:
{ "alg":"HS256", "typ":"JWT" } |
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依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
JWTUtils
public class JwtUtils {
private static final String secret = "secret888"; //密钥
public String createJwt(Integer userId, String userName, List<String> authList) {
Date issDate = new Date(); //签发时间时间
// Date expireDate = new Date(issDate.getTime() + 1000 *30); //过期时间30秒
Date expireDate = new Date(issDate.getTime() + 1000 * 60 * 60 * 2); //当前时间加上两个小时
//头部
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("alg", "HS256");
headerClaims.put("typ", "JWT");
return JWT.create().withHeader(headerClaims)
.withIssuer("thomas") //设置签发人
.withIssuedAt(issDate) //签发时间
.withExpiresAt(expireDate)
.withClaim("userId", userId) //自定义声明
.withClaim("userName", userName)//自定义声明
.withClaim("userAuth", authList)//自定义声明
.sign(Algorithm.HMAC256(secret)); //使用HS256进行签名,使用secret作为密钥
}
public boolean verifyToken(String jwtToken){
//创建校验器
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
//校验token
DecodedJWT decodedJwt = jwtVerifier.verify(jwtToken);
System.out.println("token验证正确");
// Integer userId = decodedJwt.getClaim("userId").asInt();
// String userName = decodedJwt.getClaim("userName").asString();
// List<String> userAuth = decodedJwt.getClaim("userAuth").asList(String.class);
return true;
} catch (Exception e) {
System.out.println("token验证不正确!!!");
return false;
}
}
/**
* 从jwt的payload里获取声明,获取的用户id
* @param jwt
* @return
*/
public Integer getUserIdFromToken(String jwt){
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
return decodedJWT.getClaim("userId").asInt();
} catch (IllegalArgumentException e) {
return -1;
} catch (JWTVerificationException e) {
return -1;
}
}
/**
* 从jwt的payload里获取声明,获取的用户名
* @param jwt
* @return
*/
public String getUserNameFromToken(String jwt){
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
return decodedJWT.getClaim("userName").asString();
} catch (IllegalArgumentException e) {
return "";
} catch (JWTVerificationException e) {
return "";
}
}
/**
* 从jwt的payload里获取声明,获取的用户的权限
* @param jwt
* @return
*/
public List<String> getUserAuthFromToken(String jwt){
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
return decodedJWT.getClaim("userAuth").asList(String.class);
} catch (IllegalArgumentException e) {
return null;
} catch (JWTVerificationException e) {
return null;
}
}
}
JWTTest
public class JwtTest {
public static void main(String[] args) {
JwtUtils jwtUtils=new JwtUtils();
List<String> authList= Arrays.asList("student:query","student:add","student:update");
String myCreateJwt = jwtUtils.createJwt(1, "admin", authList);
System.out.println(myCreateJwt);
boolean verifyResult = jwtUtils.verifyToken(myCreateJwt);
if(verifyResult){
//从token获取权限
System.out.println(jwtUtils.getUserAuthFromToken(myCreateJwt));
}
}
}