1.我们为什么要是用token
Token, 令牌,代表执行某些操作的权利,也就是我们进行某些操作的通行证。
1.1 在很久很久以前,我们使用什么做身份认证?
我们都知道 HTTP 是无状态(stateless)的协议:HTTP 对于事务处理没有记忆能力,不对请求和响应之间的通信状态进行保存。
使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。
可是,随着 Web 的发展,早期这种无状态的特性却带来了很多不方便性,比如说用户登录新浪微博,在登录页输入用户名、密码之后进入首页,但是由于 HTTP 是无状态的,HTTP 并不知道上一次的 HTTP 请求是否通过了验证,更无法得知当前用户的具体信息。
最简单的解决方案就是在所有的请求里面都带上用户名和密码,这样虽然可行,但是大大加重了服务器的负担(对于每个 request 都需要到数据库验证),而且用户也要每进入一个页面输入一次密码,毫无用户体验可言。
为此,引入了各种身份认证机制
1.1.1 Cookie & session
Cookie 是由 HTTP 服务器设置的,保存在浏览器中的小型文本文件,其内容为一系列的键值对
相对于保存在浏览器中的 Cookie,Session 是存储在服务器端的
Cookie 传递过程:
- 浏览器向某个 URL 发送请求
- 对应的服务器收到该 HTTP 请求,生成要发给浏览器的 HTTP 响应
- 在响应头中加入 Set-Cookie 字段,值为要设置的的Cookie
- 浏览器收到来自服务器的 HTTP 响应
- 浏览器在响应头中发现了 Set-Cookie 字段,就会将该字段的值保存在内存或者是硬盘中。
- 当下一次向该服务器发送 HTTP 请求时,会将服务器设置的 Cookie 附加在 HTTP 请求的字段 Cookie 中。
- 服务器收到这个 HTTP 请求之后,发现请求头中有 Cookie 字段,就知道了已经处理过这个用户的请求了。
- 过期的 Cookie 会被删除
基于Cookie-session的认证过程
- 用户输入登录信息
- 服务端验证登录信息是否正确,如果正确就在服务器端为这个用户创建一个 Session,并把 Session 存入数据库
- 服务器端会向客户端返回带有 sessionID 的 Cookie
- 客户端接收到服务器端发来的请求之后,看见响应头中的 Set-Cookie 字段,将 Cookie 保存起来
- 接下来的请求中都会带上这个 Cookie,服务器将 sessionID 和 数据库中的相匹配,如果有效则处理该请求
- 如果用户登出,Session 会在客户端和服务器端都被销毁
1.1.2 Cookie & session 的弊端
- 由于Cookie 以及 session 需要存储在浏览器内存以及服务器内存中,请求增大时,资源消耗会很大
- 基于cookie做身份认证,若浏览器屏蔽cookie,则造成很麻烦,需要另辟蹊径,使用url重写等方式
- 对于分布式系统支持不好
- cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
1.2 基于token的鉴权方式
token在服务器时可以不用存储用户信息的,token传递的方式也不限于cookie传递。
当用户第一次访问服务器时,服务端通过算法,加密钥,生成一个token。通过BASE64编码后将token发送给客户端。
客户端将token保存起来,下次请求带着token,服务器收到请求会用相同的算法取验证toekn,如果通过就继续执行。
此时服务器变成无状态了,从而比较容易实现扩展
session为会话,token为令牌。
token解决了session扩展性差的问题
2. JWT
2.1 JWT结构
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名依顺序用点号(".")链接而成:1.header,2.payload,3.signature。
- Header(头部)
- Payload(负载)
- Signature(签名)
如下为一个 JWT字符串:
eyJhbGciOiJIUzI1NiJ9.eyJleHBpcmVUaW1lIjoxNTg5MDE0MzA2OTU0LCJkZXB0Ijoi5Yas6KW_55Oc5ZywIiwidXNlcm5hbWUiOiLlpKfopb_nk5wifQ.QSv0FcvNheiA3FW6OEah7jJKG4SG0ver3q67F0980rY
2.1.1 Header(头部)
eyJhbGciOiJIUzI1NiJ9
使用base64解密后得到:
{"alg":"HS256"}
2.1.2 Payload(负载)
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用:、
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
也可以自定义 :
- username:大西瓜
JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
2.1.3 Signature(签名)
Signature 部分是对前两部分的签名,防止数据篡改。
使用指定加密算法以及仅服务器可知的 密钥(secret)对前两部分进行加密。
2.1.4 拼接
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以生成完整的JWT token了
2.2 JWT 的几个特点
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
3. java中使用 JWT
本文使用 JJWT 来实现java环境 JWT 生成,解密操作
3.1 引入 JJWT maven依赖
目前最新版本 0.9.1
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
3.2 配置 key
private static final String SALT = "0142add7c2664198863943f24bf4b8b9";
private static Key getKeyInstance() {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
String apiKey = DatatypeConverter.printBase64Binary(SALT.getBytes());
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(apiKey);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
return signingKey;
}
3.3 生成token
public static String createJavaWebToken4JwtAuth(Map<String, Object> claims) {
logger.info("生成的token为开始");
String toekn = Jwts.builder().setClaims(claims).setExpiration(DateUtil.addSeconds(new Date(), 50))
.signWith(SignatureAlgorithm.HS256, getKeyInstance()).compact();
logger.info("生成的token为:" + toekn);
return toekn;
}
其中 官方Payload 中的属性都有对应api来配置,🌰中配置了 .setExpiration(DateUtil.addSeconds(new Date(), 50))
3.4 获取body部分
public static Map<String, Object> verifyJavaWebToken(String jwt) {
try {
Map<String, Object> jwtClaims =
Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwt).getBody();
return jwtClaims;
} catch (Exception e) {
logger.info(e.getMessage());
return null;
}
}
当当前时间超过配置的过期时间时,会后抛出异常
3.5 校验有效性方法
public static boolean isTokenEffect(String jwt) {
if (StringUtils.isEmpty(jwt)) {
return false;
}
Map<String, Object> claims = verifyJavaWebToken(jwt);
if (null == claims) {
logger.info("转换jwt失败!");
return false;
}
return true;
}
3.6 使用
public static void main(String[] args) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("username", "大西瓜");
paramMap.put("dept", "冬西瓜地");
String tokens = JwtUtil.createJavaWebToken4JwtAuth(paramMap);
System.out.println(tokens);
System.out.println(isTokenEffect(tokens));
}
本文JwtUtil 已提交github到我的工具集 ytooo-util
我的工具集包含 常用字符串处理、日期处理、http请求封装、文件处理、请求体封装等常用工具
欢迎使用
<dependency>
<groupId>ml.ytooo</groupId>
<artifactId>ytooo-util</artifactId>
<version>3.6.5</version>
</dependency>
更多好玩好看的内容,欢迎到我的博客交流,共同进步 WaterMin