最为传统的做法,客户端储存 cookie 一般是 Session id ,服务器存储 Session
内存中创建一条记录保存用户信息(存储在HttpSession中,相当于一个Map(也可以放redis中)),,然后通过Cookie的形式给客户端返回一个jsessionid,然后每次访问的时候都需要从HttpSession中根据jsessionid来获取,通过这个逻辑来判断是否是认证的状态。
服务器的在这里的开销就会越来越大跨域问题

存在的问题:
用户信息。然而Session是在服务器端的,而JWT是在客户端的客户端中,可以明显减轻请服务器的内存压力,服务端只需要用算法解析客户端的token就可以得到信息

认证的流程:
jwt的优势:
适合分布式环境。JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案
1、客户端向服务端发送用户名和密码。
2、服务端验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务端向客户端返回一个 session_id,写入用户的 Cookie。
4、客户端随后的每一次请求,都会通过 Cookie,将 session_id 传回服务端。
5、服务端收到 session_id,找到前期保存的数据,由此得知客户端的身份。
该模式在单机部署下问题,如果是服务器集群,就要求 session 数据共享,每台服务器都能够读取 session。
假设:A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"username": "张三",
"role": "管理员",
"expired": "2019年8月1日0点0分"
}
客户端与服务端通信的时候,都要透传这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器变成无状态了,从而比较容易实现扩展。JWT是长的字符串,中间用点(.)分隔成三个部分。
没有换行的,这里只是为了便于展示,将它写成了几行。如下所示:
JWT 的三个部分依次如下。
写成一行,就是下面的样子。

Header 部分是一个 JSON 对象, 常由两部分组成:令牌的类型【JWT】和所使用的签名算法【如HMAC、SHA256或者RSA】。
{
"alg": "HS256",//签名或摘要算法
"typ": "JWT"//token类型
}
alg表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)typ表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
Base64URL 算法(详见后文)转成字符串然后以"点"(.)拼接
使用Base64URL 加密上面JSON对象得到第一部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload 部分也是一个JSON对象,用来存放实际需要传递的数据。通常是用户信息,比如用户名,用户角色,过期时间等JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
sub (subject):主题,jwt所面向的用户
aud (audience):用户,接收jwt的一方
exp (expiration time):失效时间戳
nbf (Not Before):生效时间时间戳(在此之前不可用)
iat (Issued At):签发时间
jti (JWT ID):JWT ID用于标识该JWT
{
"iss": "token-server",//签发者
"exp ": "Mon Nov 13 15:28:41 CST 2017",//过期时间
"sub ": "wangjie",//用户名
"aud": "web-server-1"//接收方,
"nbf": "Mon Nov 13 15:40:12 CST 2017",//这个时间之前token不可用
"jat": "Mon Nov 13 15:20:41 CST 2017",//签发时间
"jti": "0023",//令牌id标识
"claim": {“auth”:”ROLE_ADMIN”}//访问主张
}
重要信息放在Payload。
Base64URL 算法(详见后文)转成字符串然后以"点"(.)拼接
使用Base64URL 加密上面JSON对象得到第二部分
eyJuYW1lIjoiemhhbmdzYW4iLCJjb2RlIjoiMjEyMzQxMCJ9
Signature 部分是对前两部分(Header/Payload)的签名,防止数据篡改。
密钥(secret)。这个密钥只有服务端才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),签名公式如下:HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload) , secret)
算出签名以后,把 base64UrlEncode(header)、base64UrlEncode(payload)、Signature 3个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
//base64Url编码后的header和payload使用"点"(.)`拼接
String encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
//密钥
Strubg secet="aaa";
//通过header中声明的加密方式进行加盐secret组合加密
String signature = HMACSHA256(encodedString, '密钥');
加密之后,得到第三步部分signature签名信息。
xHtGAnyhgrD_FcIu3xzunVQjzThByjBF2GF5iA2ezOY
将这三部分用"点"(.)连接成一个完整的字符串,就构成了最终的JWT生成公式:
base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." +
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload) , secret)
执行结果
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:
secret用来进行jwt的签发和jwt的验证,所以,在任何场景都不应该流露出去。
前面提到,Header 和 Payload 序列化算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义, 在Base64URL 算法会被替换掉:=被省略、+替换成-,/替换成_ 签名signature 部分了
头部header和载荷payload的base64编码,几乎是透明的,毫无安全性可言,token安全性就落在了加入的盐secet上面了,为了避免生成token所用的盐secet与解析token时加入的盐secet是一样的,我们需要对盐secet采用非对称加密,以达到生成token与校验token方所用的盐不一致的安全效果!注意:
加盐的意思就是让味道改变,也就是让通过加盐来提高token的复杂度,让token更加安全,这个盐你可以任意指定,全凭自己和项目需求。
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器交互,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息Authorization字段里面。
Authorization: Bearer <token>
POST 请求的数据体里面。JWT 默认是不加密,不能将重要数据写入 JWT。因此生成原始 Token 以后,可以加密算法再加密一次。
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
JWT 包含了认证信息,泄露后任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
https://github.com/auth0/java-jwt
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>4.2.1version>
dependency>
产生加密Token
HashMap<String, Object> map = new HashMap<>();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,60);
String token = JWT.create()
.withHeader(map) //head
.withClaim("userId", 21) //payload
.withClaim("username", "xizi")
.withAudience("user1") //设置接受方信息,一般时登录用户
.withExpiresAt(instance.getTime()) //过期时间 20秒之后
.sign(Algorithm.HMAC256("111111"));//Signature,使用HMAC算法,111111作为密钥加密
解密Token获取负载信息并验证Token是否有效
//创建验证对象
Verification verification = JWT.require(Algorithm.HMAC256("111111"));//签名
JWTVerifier jwtVerifier = verification.build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Claim userId = decodedJWT .getClaim("userId");
Claim username = decodedJWT .getClaim("username");
System.out.println("过期时间:"+decodedJWT .getExpiresAt());
常见的异常
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMi smatchException: 算法不匹配异常
InvalidClaimException: 失效的payload异常
工具类
public class JWTUtils {
//自定义签名
private static final String SIGN="XIZI";
/**
* 生成token 三部分组成header.payload.sign
*
*/
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7); //七天 过期时间
//创建jwt builder
JWTCreator.Builder builder = JWT.create();
//payload 遍历map中的键值对
map.forEach((k,v)->{
builder.withClaim(k,v );
});
//指定过期时间 设置编码方式
String token = builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(SIGN));
return token;
}
/**
* 验证token 并且放回DecodedJWT 获取token信息方法
*/
public static DecodedJWT verify(String token){
//没有抛出异常 验证通过
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
/**
* 获取token信息方法
*/
public static DecodedJWT getTokenInfo(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
}
引入依赖
https://github.com/jwtk/jjwt#install-jdk-maven
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.11.2version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.11.2version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.11.2version>
<scope>runtimescope>
dependency>
或者
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import com.lss.jwt_test.rest.TestController;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JWTUtils {
/**
* token 过期时间, 单位: 秒. 这个值表示 30 天
*/
private static final long TOKEN_EXPIRED_TIME = 30 * 24 * 60 * 60;
public static final String jwtId = "tokenId";
/**
* jwt 加密解密密钥(可自行填写)
*/
private static final String JWT_SECRET = "1234567890";
/**
* 创建JWT
*/
public static String createJWT(Map<String, Object> claims, Long time) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
Date now = new Date(System.currentTimeMillis());
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();//生成JWT的时间
//下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body
.setClaims(claims) //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId(jwtId) //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(now) //iat: jwt的签发时间
.signWith(signatureAlgorithm, secretKey);//设置签名使用的签名算法和签名使用的秘钥
if (time >= 0) {
long expMillis = nowMillis + time;
Date exp = new Date(expMillis);
builder.setExpiration(exp); //设置过期时间
}
return builder.compact();
}
/**
* 验证jwt
*/
public static Claims verifyJwt(String token) {
//签名秘钥,和生成的签名的秘钥一模一样
SecretKey key = generalKey();
Claims claims;
try {
claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}//设置需要解析的jwt
return claims;
}
/**
* 由字符串生成加密key
*
* @return
*/
public static SecretKey generalKey() {
String stringKey = JWT_SECRET;
byte[] encodedKey = Base64.decodeBase64(stringKey);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 根据userId和openid生成token
*/
public static String generateToken(String openId, Integer userId) {
Map<String, Object> map = new HashMap<>();
map.put("userId", userId);
map.put("openId", openId);
map.put("sub", openId);
return createJWT(map, TOKEN_EXPIRED_TIME);
}
public static void main(String[] args) {
// 生成token
String s = generateToken("111", 20);
System.out.println(s);
// 验证
String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMTEiLCJvcGVuSWQiOiIxMTEiLCJleHAiOjE1OTI1NTc3ODMsInVzZXJJZCI6MjAsImlhdCI6MTU5MjU1NTE5MSwianRpIjoidG9rZW5JZCJ9.X7JHnx3Y5wtb-n3pT9tft2I4hENJdeRxW2QWaI4jv2E";
Claims claims = verifyJwt(token);
String subject = claims.getSubject();
String userId = (String)claims.get("userId");
String openId = (String)claims.get("openId");
String sub = (String)claims.get("sub");
System.out.println("subject:" + subject);
System.out.println("userId:" + userId);
System.out.println("openId:" + openId);
System.out.println("sub:" + sub);
}
}