了解token并用JWT实现
一、Token
1.1 定义
token是服务器端生成的一串字符串,作为客户端访问服务端的一个令牌。第一次登录时为客户端生成一个token,后期用户登录时只需要传递token用于验证,无需带上用户的用户的帐号和密码。
1.2 流程
APP登录的时候发送加密的用户名和密码到服务器,服务器验证用户名和密码,如果成功,以某种方式比如随机生成32位的字符串作为token,存储到服务器中,并返回token到APP,以后APP请求时,凡是需要验证的地方都要带上该token,然后服务器端验证token,成功返回所需要的结果,失败返回错误信息,让他重新登录。其中服务器上token设置一个有效期,每次APP请求的时候都验证token和有效期。
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
1.3 优势
无状态、可扩展: 在客户端存储的Token是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载均衡器能够将用户信息从一个服务传到其他服务器上。如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性),用户量大时,可能会造成 一些拥堵。然而tokens的无状态性完美解决了这个问题。
安全性: 请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效。
可扩展性: Tokens能够创建与其它程序共享权限的程序。个人理解就是自己可以提供一个类似第三方登录的功能,其他程序集成自己的登录,通过token授权
多平台跨域: CORS(跨域资源共享),对应用程序和服务进行扩展的时候,需要介入各种各样的设备和应用程序。只要用户有一个通过了验证的token,数据和资源就能够在任何域上被请求到。
二、JWT(Json Web Token)
2.1 原理
服务端认证之后生成一个json对象,返回给客服端如
{"姓名","test","角色":"学生"} // 返回一个json串 pC5jRyXsdYWwpC5jRyXsdYWw== // 或者一串AES加密串保存用户姓名等
后面每次访问服务器的时候都将这段数据传给服务器,服务器只依靠这个就判断认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
服务器不保存任何session信息,这样服务器就是无状态的了,后面添加负载也更容易
2.2 结构
JWT返回的token分为3段,Header(头部),Payload(负载),Signature(签名)。分别用 . 隔开(Header.Payload.Signature)。如(点后面没有回车):
eyJhbGciOiJIUzI1NiJ9.
eyJqdGkiOiJpbnNwdXIiLCJpYXQiOjE1NzMwMzAwNjQsInN1YiI6InFCMmNiNUFuMkg5WGovWEF5SVhucWc9PVxyXG4iLCJleHAiOjE1NzMxMTY0NjR9.
eUIzHeBqYKtWM9owo36FzFaJByn0K1MP2n_rXSm4Xa4
JSON 对象使用 Base64URL 算法解密后
Header:描述 JWT 的元数据(JSON 对象使用 Base64URL 算法)。
{ "alg": "HS256", "typ": "JWT" }
lg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
Payload: 用来存放实际需要传递的数据(JSON 对象也要使用 Base64URL 算法转成字符串)。JWT 规定了7个官方字段:
iss (issuer):签发人 exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (Issued At):签发时间 jti (JWT ID):编号
除了官方字段,也可以在这个部分定义私有字段。但是JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
Signature: 是对前两部分的签名,防止数据篡改。
- 需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
- 算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法
2.3 特点
- 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- 不加密的情况下,不能将秘密数据写入 JWT
- 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
- 最大的缺点是由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑
- 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
三、SpringBoot使用
3.1 maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
3.2 工具类
import javax.crypto.SecretKey;
import java.util.Date;
import javax.crypto.spec.SecretKeySpec;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @ClassName JwtUtil
* @Author gaoqisen
* @Date 2019-11-05
* @Version 1.0
*/
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(ApacheHttpClient.class);
/**
* 由字符串生成加密key
* @return
*/
public SecretKey generalKey(){
//本地配置文件中加密的密文7786df7fc3a34e26a61c034d5ec8245d
String stringKey = TokenConstant.JWT_SECRET;
//本地的密码解码[B@152f6e2786df7fc3a34e26a61c034d5ec8245d
byte[] encodedKey = Base64.decodeBase64(stringKey);
// 根据给定的字节数组使用AES加密算法构造一个密钥,使用 encodedKey中的始于且包含 0 到前 leng 个字节这是当然是所有。
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 创建jwt
* @param id
* @param subject
* @param ttlMillis
* @return
* @throws Exception
*/
public String createJWT(String id, String subject, long ttlMillis) throws Exception{
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
//Map<String,Object> claims = new HashMap<String,Object>();
//claims.put("uid", "CVLm6KSSSkeNRg5pMqop2w");
//claims.put("user_name", "admin");
//claims.put("nick_name","DASDA121");
SecretKey key = generalKey();
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
//.setClaims(claims)
.setId(id)
.setIssuedAt(now)
.setSubject(subject)
.signWith(signatureAlgorithm, key);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
//就开始压缩为xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx这样的jwt
return builder.compact();
}
/**
* 解密jwt
* @param jwt
* @return
* @throws Exception
*/
public Claims parseJWT(String jwt) throws Exception{
SecretKey key = generalKey();
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwt).getBody();
return claims;
}
/**
* 校验jwt
* @param jwt
* @return
* @throws
*/
public boolean validateJWT(String jwt) {
boolean flag = false;
try {
parseJWT(jwt);
flag = true;
} catch (ExpiredJwtException e) {
logger.error("token过期");
} catch (SignatureException e) {
logger.error("签名校验失败");
} catch (Exception e) {
logger.error("其它错误");
}
return flag;
}
}
3.3 常量
public static final String JWT_ID = "jwt";
public static final String JWT_SECRET = "26e4b9wqrfe444568f24232108460891";
public static final int JWT_TTL = 1*60*60*1000; // 60*60*1000; //过期时间(毫秒)
3.4 接口开发
public JSONObject getToken(String msg) {
JSONObject json = new JSONObject();
JwtUtil util = new JwtUtil();
try {
String token = util.createJWT(TokenConstant.JWT_ID, msg TokenConstant.JWT_TTL);
json.put("state", "ok");
json.put("token", token);
} catch (Exception e) {
json.put("state","error");
json.put("msg", "Token处理失败");
}
return json;
}
public JSONObject checkToken(String token) {
JSONObject json = new JSONObject();
JwtUtil jwt = new JwtUtil();
boolean rs = jwt.validateJWT(token);
json.put("state","ok");
json.put("token", rs);
return json;
}
public JSONObject getUserInfoByToken(String token) {
JSONObject json = new JSONObject();
JwtUtil jwt = new JwtUtil();
boolean rs = jwt.validateJWT(token);
if (!rs) {
json.put("state", "error");
json.put("msg", "Token验证失败");
return json;
}
try {
// 通过用户帐号获取用户信息
Claims claims = jwt.parseJWT(token);
String id = claims.getSubject();
// 处理业务逻辑
} catch (Exception e) {
e.printStackTrace();
}
return json;
}
}
注: 网上说jwt只能抗2w并发, 如果并发过大会出现问题。(待验证)