1.引子
我们知道,http是一种无状态协议,即对于服务端应用来说,两次http请求之间相互独立,你不知道我,我不知道你。
那么问题来了,比如说一个电商网站,购物的时候,需要浏览商品,将商品添加到购物车,需要下单结算。这就提出了要求:我们需要知道是谁选择了商品a,是谁将商品a添加到了购物车,购物车又是谁的......等一系列问题,都指向了“谁”,即将多次不同http请求关联起来。
如何关联呢?
你应该已经反应过来了,登录认证啊!用户访问购物网站,首先要做的是提供用户名称、密码登录到购物网站,然后在网站导航的地方,通常会提示:欢迎xxx!对吧,这样以来,购物网站自然就知道了上面我们提到的一系列“谁”,到底是谁
这是从一个普通用户的角度来描述的,我们是程序员,需要从技术的角度来进行描述。你说这也不难!不就是
-
当用户访问登录接口的时候,根据用户提供的用户名称、密码查询数据库
-
如果用户存在,购物网站应用服务器,比如说tomcat,生成一个session,并且存储到购物网站应用服务器内存中
-
将session的唯一标识sessionId,返回给客户端(浏览器),浏览器将sessionId存储到cookie中
-
浏览器再登录成功后,每次发起http请求,比如选择商品、加入购物车、下订单、结算都带上sessionId,依据sessionId告诉服务器,一系列的谁,就是谁
-
购物网站应用服务端,根据sessionId,自然就知道了是谁在选择商品,谁在将商品加入购物车,谁在下订单.......
通过session、cookie将多次不同的http请求关联了起来,解决了一系列谁是谁的问题,完美!不能再完美了!
这也就是我们熟悉的传统session会话的解决方案,但是这种方案,其实是有瑕疵的,我们来看一下
-
会话session存储在服务端,需要消耗应用服务器内存,如果网站生意比较好,动不动就有几千万、上亿用户,是不是单存储会话session就需要消耗不少内存?
-
网站太受欢迎,高并发、大流量,一台应用服务器扛不住,部署集群方案吧,比如说增加网站应用服务器a、b、c。会话session都是应用服务器内部生成的,用户在服务节点a登录了,下次请求如果打到服务节点b,节点b如何知道用户已经登录过?
-
会话sessionId存储在浏览器cookie中的对吧,CSRF(跨站请求伪造)怎么办?
-
移动互联网时代,大家都用智能手机,玩着各种app,没有浏览器不支持cookie怎么办?
-
分布式、微服务时代,不同应用之间相互调用,服务a,不认识服务b的session啊,怎么办?
综合上述总结一下,传统会话session方案最大的不足在于消耗应用服务器内存、难以支撑应用弹性扩容缩容。
这才有了业界当前流行的token方案,接下来让我们一起来看token方案,在这里关于token方案,我将结合jwt给你分享。
2.案例
2.1.token登录认证方案
token登录认证方案,实现思路上事实上与传统session方案差不多,毕竟token与sessionId一样,都是字符串嘛,我们从普通用户操作流程进行分析
-
用户打开购物网站,提供用户名称、密码进行登录请求
-
网站服务器,接收到用户登录请求,根据用户名称、密码查询数据库,检查用户是否存在
-
如果用户存在,根据某种规则,生成一个token(一个字符串)
-
将token响应给浏览器,用户在之后的选择商品、加入购物车、下订单等请求中,都带上token
-
网站服务器,校验处理token即知道谁是谁了
你看,这就是token方案,对比传统session方案,它不再需要
-
token不需要在服务器存储,即不消耗服务器内存资源
-
服务继续无状态,支持按需随意扩容缩容
2.2.什么是jwt
jwt的全称是(json web token)。 是为了在网络应用环境间传递声明的一种基于json的开放标准,通过jwt实现的token被设计为紧凑且安全,适用于分布式站点单点登录(sso)场景。
jwt实现的token主要由三部分组成,以.符号进行分割
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiI2NjYiLCJpYXQiOjE2MTg2NDYyNjUsImV4cCI6MTYxOTI1MTA2NX0.uVeXiEhqfhpWAnkiX8glIBE4nOG6o2zaQfRBOC-EiuY
-
头header:标记token令牌类型,加密算法
-
载荷payload:token数据,放置一些用户非敏感信息,比如说用户id,用户名称(切记:jwt是可以解密,密码、手机号码等用户敏感信息,千万不要放在其中)
-
签名sign:用于验证token是否有效,放置篡改
2.3.jwt实现案例
2.3.1.导入依赖
<!--jjwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
2.3.2.jwt工具类
/**
* token工具
*
* @author ThinkPad
* @version 1.0
* @date 2021/4/17 14:31
*/
@Slf4j
@Data
public class TokenJwtUtil {
/**
* 秘钥,默认:aaabbbcccdddeeefffggghhhiiijjjkkklllmmm
*/
private String secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmm";
/**
* 有效时间,默认一周,单位秒
*/
private Long expirationTime = 604800L;
/**
* 生成token
* @param claims token 数据
* @return
*/
public String generateToken(Map<String, Object> claims){
// 生成时间,过期时间
Date createdTime = new Date();
Date expirationTime = new Date(System.currentTimeMillis() + this.expirationTime * 1000);
// 秘钥
byte[] keyBytes = this.secret.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
// 生成token
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdTime)
.setExpiration(expirationTime)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 校验token是否有效
* @param token
* @return 有效返回true,无效返回false
*/
public Boolean validateToken(String token){
Date expirationTime = getExpirationDateFromToken(token);
return !expirationTime.before(new Date());
}
/**
* 获取token过期时间
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
/**
* 解析获取token数据
* @param token
* @return
*/
public Claims getClaimsFromToken(String token) {
try{
return parser()
.setSigningKey(this.secret.getBytes())
.parseClaimsJws(token)
.getBody();
}catch (ExpiredJwtException
| UnsupportedJwtException
| MalformedJwtException
| IllegalArgumentException e){
log.error("解析token发生异常", e);
throw new IllegalArgumentException("Token invalided.");
}
}
}
2.3.3.测试使用
public static void main(String[] args) {
TokenJwtUtil tokenJwtUtil = new TokenJwtUtil();
// 1.生成token
Map<String, Object> claims = new HashMap<>();
claims.put("userId","666");
String token = tokenJwtUtil.generateToken(claims);
log.info("准备token数据:{}", claims);
log.info("生成token={}",token);
// 2.解析token头
String[] split = token.split("\\.");
byte[] headBytes = Base64.decodeBase64(split[0]);
log.info("解析token头:{}", new String(headBytes));
// 3.解析token数据
byte[] bodyBytes = Base64.decodeBase64(split[1]);
log.info("解析token数据:{}", new String(bodyBytes));
// 4.解析token签名
byte[] signatureBytes = Base64.decodeBase64(split[2]);
log.info("解析token签名:{}", new String(signatureBytes));
// 5.解析token过期时间
Date expirationDate = tokenJwtUtil.getExpirationDateFromToken(token);
log.info("解析token过期时间:{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expirationDate));
// 6.校验token是否有效
Boolean isValidate = tokenJwtUtil.validateToken(token);
log.info("校验token是否有效:{}",isValidate);
}
[com.anan.edu.common.util.Test] - 准备token数据:{userId=666}
[com.anan.edu.common.util.Test] - 生成token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiI2NjYiLCJpYXQiOjE2MTg2NDYyNjUsImV4cCI6MTYxOTI1MTA2NX0.uVeXiEhqfhpWAnkiX8glIBE4nOG6o2zaQfRBOC-EiuY
[com.anan.edu.common.util.Test] - 解析token头:{"alg":"HS256"}
[com.anan.edu.common.util.Test] - 解析token数据:{"userId":"666","iat":1618646265,"exp":1619251065}
[com.anan.edu.common.util.Test] - 解析token签名:�W��Hj~Vy"_�% 8�ảl�A�A8/���
[com.anan.edu.common.util.Test] - 解析token过期时间:2021-04-24 15:57:45
[com.anan.edu.common.util.Test] - 校验token是否有效:true