什么是JWT
Json web token(JWT)是为了网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC 7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
为什么使用JWT?
随着技术的发展,分布式web应用的普及,通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息,随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。手机端app是没有cookie的所以通过Token机制来解决登录后身份验证的问题。
优点:
- 因为json的通用性,所以JWT是可以跨语言支持的,像C#,JavaScript,NodeJS,PHP等许多语言都可以使用
- 因为由了payload部分,所以JWT可以在自身存储一些其它业务逻辑所必要的非敏感信息
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的
- 它不需要在服务端保存会话信息,所以它易于应用的扩展
1、支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
2、无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.。服务器无需保存token,减小服务器压力,通过算法解析客户端传来的token获得数据
4、更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
5、去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
6、更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
7、CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
8、性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
9、不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
10、基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
缺点:
1. 因为JWT是无状态的,因此服务端无法控制已经生成的Token失效,是不可控的,
2.获取到jwt也就拥有了登录权限,因此jwt是不可泄露的,网站最好使用https,防止中间攻击偷取jwt
传统的session认证
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发送的请求,所以为了让我们的应用能识别是哪个用户发出的,我们只能在服务器存储一份用户登陆的信息,这份登陆信息会在响应时传递给服务器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的英哟个就能识别请求来自哪个用户了,这就是传统的基于sessino认证
但是这种基于session的认证使应用本身很难得扩展,随着不用客户端的增加,独立的服务器已无法承载更多的用户,而这个时候基于session认证应用的问题就会暴露出来
基于session认证所显露的问题
Session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,响应的限制了负载均衡器的能力,也意味着限制了应用的扩展性
CSRF:因为是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或会话信息。这也就意味着机遇tokent认证机制的应用不需要去考虑用户在哪一台服务器登陆了,这就为应用的扩展提供了便利
流程是这样的
- 用户使用用户名密码请求服务器
- 服务器进行验证用户信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附加这个token值
- 服务器验证token,并返回数据
token必须要在每次请求时发送给服务器,它应该保存在请求头中,另外,服务器要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了 Access-Control-Allow-Origin:*
流程
1、用户认证。认证方式可能很多,自己认证或者sso。
2、认证后,服务器构造JWT。
3、把JWT返回客户端,客户端存储。
4、客户端访问服务器,带上JWT。
5、服务器端判断JWT是否正确并且没有超时,正常,向下流转;否则,转到授权。
服务器端
1、生成jwt
1.1、生成jwt的时机,认证之后,返回认证结果之前。
1.2、Payload(很多翻译是直译,对于理解没有任何帮助)中如何设计属性,这个对于可读性比较重要。
根据JWT的标准,这些claims可以分为以下三种类型:
a. Reserved claims(保留),它的含义就像是编程语言的保留字一样,属于JWT标准里面规定的一些claim。JWT标准里面定好的claim有:
iss(Issuser):代表这个JWT的签发主体;
sub(Subject):代表这个JWT的主体,即它的所有人;
aud(Audience):代表这个JWT的接收对象;
exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
iat(Issued at):是一个时间戳,代表这个JWT的签发时间;
jti(JWT ID):是JWT的唯一标识。
b. Public claims
c. Private claims,这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证;而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
java代码
1.添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
2.jwt工具类
@ConfigurationProperties("jwt.config")
public class JwtUtil {
//盐
private String key ;
//过期时间
private long ttl ;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
/**
* 生成JWT
* @param id 用户id
* @param subject 用户信息
* @param roles 自定义key-value 角色
* @return
*/
public String createJWT(String id, String subject, String roles) {
//signWith(SignatureAlgorithm.HS256, key) 设置加密方式,和盐 。盐自己规定
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key)
.claim("roles", roles);
if (ttl > 0) {
//设置过期时间
builder.setExpiration( new Date( nowMillis + ttl));
}
return builder.compact();
}
/**
* 解析JWT
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
2.测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Autowired JwtUtil jwtUtil;
@Test
public void testJwt() {
//生产token
String createJWT = jwtUtil.createJWT("1", "sj", "admin");
System.out.println(createJWT);
//
}
@Test
public void testgetJwt() {
//获取token信息
String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoic2oiLCJpYXQiOjE1NDgyMTc4ODgsInJvbGVzIjoiYWRtaW4ifQ.FnOVnfz69gH9ovTkw2AEvHvKSBKxdvAZvGV4nYFLKGk";
try {
Claims parseJWT = jwtUtil.parseJWT(token);
System.out.println(parseJWT.getId()+parseJWT.getSubject()+parseJWT.get("roles"));
} catch (Exception e) {
System.out.println("验证失败!");
}
}
}