简介
token:指访问资源的凭据,用于检验请求的合法性。适用于项目级的前后端分离。
- 可以用数据库存储token,也可以选择放在内存当中。比如 redis 很适合对 token 查询的需求。
- token 可以避免 CSRF 攻击(因为不需要 cookie 了)。
- 完美契合移动端的需求。
session:session记录服务器和客户端会话状态的机制。session在服务端生成和保存,并转化为一个临时的Cookie(sessionId)发送给客户端,当客户端第一次请求服务器时,会检查是否携带了这个Session,如果没有则会添加Session。session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上(前后端项目协议、域名、端口号都一致,即在一个项目下)
- 将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session.
- sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办? 一般会把 sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现.
- 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token。
cookie:Cookie 是在 HTTP 协议下,维护客户工作站上信息的一种方式。Cookie 是由 Web 服务器保存在用户浏览器上的小文本数据文件,它可以包含有关用户的信息。cookie是不可跨域的,每个cookie都会绑定一个单一的域名,并只能在指定的域名下使用。
- 使用 httpOnly 在一定程度上提高安全性
- 尽量减少 cookie 的体积,能存储的数据量不能超过 4kb
- 一个浏览器针对一个网站最多存 20 个Cookie,浏览器一般只允许存放 300 个Cookie
- 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token
cookie和session产生的原因:HTTP协议是无状态的,即请求之间相互独立,下一次请求不能保存上一次的请求或响应。如果服务器处理请求时需要上次请求的信息,客户端必须重传全部信息,这样可能导致每次连接传送的数据量巨增。
session产出的原因:Cookie可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些Cookie,如果Cookie很多,这无形地增加了客户端与服务端的数据传输量。此外cookie保存在客户端,有被篡改、盗取的风险。而Session的出现正是为了解决这个问题。
Token 和 Session 的区别
- Session 记录会话信息,使服务端有状态化。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
- Token 在身份认证方面安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
- 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
用户认证的一般流程:
- 用户向服务器发送用户名和密码。
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个 session_id,写入用户的 Cookie。
- 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
JSON Web Token(JWT)
- JSON Web Token(简称 JWT)认证授权机制,是目前最流行的跨域认证解决方案。
- JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
- 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。
- 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
- 用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制。
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不加密的情况下,不能将秘密数据写入 JWT。
- 由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。
- 为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
JWT 原理
JWT 认证流程
- 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
- 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
- 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT。即:Authorization: Bearer
JWT使用方式
- 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。即:Authorization: Bearer
- 将 JWT 放在 POST 请求的数据体里。
- 通过 URL 传输。如:http://www.example.com/user?token=xxx
Token 和 JWT 的区别
- 相同点:都是访问资源的令牌;都可以记录用户的信息;都是使服务端无状态化;都是只有验证成功后,客户端才能访问服务端上受保护的资源。
- Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效;
- JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
前后端鉴权方式
- Session-Cookie
- Token 验证(包括 JWT,SSO)
- OAuth2.0(开放授权)
基于JWT的token认证实现
(1)Token插件:Auto0
- 添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
- 设置密钥和生存时间
//设置过期时间
private static final long EXPIRE_DATE=30*60*100000;
//token秘钥
private static final String TOKEN_SECRET = "ZCEQIUBFKSJBFJH2020BQWE";
- 实现签名方法
public static String token (String username,String password){
String token = "";
try {
//过期时间
Date date = new Date(System.currentTimeMillis()+EXPIRE_DATE);
//秘钥及加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
//设置头部信息
Map<String,Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256");
//携带username,password信息,生成签名
token = JWT.create()
.withHeader(header)
.withClaim("username",username)
.withClaim("password",password).withExpiresAt(date)
.sign(algorithm);
}catch (Exception e){
e.printStackTrace();
return null;
}
return token;
}
- 验证token
public static boolean verify(String token){
/**
* @desc 验证token,通过返回true
* @create 2019/1/18/018 9:39
* @params [token]需要校验的串
**/
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
(2)Token插件:jsonwebtoken
- 添加依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
- 设置密钥和生存时间
//jwt密钥 必须大于32个字符
private static String TOKEN_KEY = "54f12048-1f56-45c1-a3f1-2c546bc2bb42";
//jwt有效时间
private static long TOKEN_TIMEOUT = 60 * 30 * 1000;
- 实现签名方法
public static String create(Map<String, Object> header, Map<String, Object> claims, String issuer) {
String token;
try {
Date date = new Date(System.currentTimeMillis() + TOKEN_TIMEOUT);
SecretKey key = Keys.hmacShaKeyFor(TOKEN_KEY.getBytes());
token = Jwts.builder()
.setHeader(header)
.setClaims(claims)
.setIssuer(issuer)
.setExpiration(date)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
} catch (JwtException exception) {
System.out.println(exception.getMessage());
return null;
}
return token;
}
- 验证(解析)token
public static Jws<Claims> decode(String token) {
Jws<Claims> claimsJws;
try {
Key key = new SecretKeySpec(TOKEN_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName());
claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
} catch (JwtException exception) {
System.out.println(exception.getMessage());
return null;
}
return claimsJws;
}
public static Map<String, Object> parseToken(String token) {
Key key = new SecretKeySpec(TOKEN_KEY.getBytes(),SignatureAlgorithm.HS256.getJcaName());
return Jwts.parser() // 得到DefaultJwtParser
.setSigningKey(generateKey()) // 设置签名密钥
.parseClaimsJws(token)
.getBody();
}