在任何一个系统中,几乎都离不开一个功能:登录功能。更准确的来说应该是身份验证功能。在开发过程中,有哪些常见的身份验证模式呢?
常用的身份验证模式
单一服务器模式
单一服务器模式一般由Session实现,用户在进行了登录验证后,服务器将用户的信息提取出来,存储在服务器上,再为这段信息定义一个唯一标识,将该标识通过cookies的形式返回给用户。
在单一服务器模式中,Session是有状态的,且Session由服务器进行保存,服务器可以随时销毁Session,相对来说较为安全。但是,由于Session是存储在服务器中,在分布式项目中需要Session共享方案,目前常用的Session共享方案存在性能瓶颈。
SSO模式
SSO模式全程是Single Sign On
,也就是我们常说的单点登录。他的特点在多个应用系统中,只需要登录一次,就可以访问其他相互信任的系统。好比说我们登录了qq,那我们就无需再次登录就可以使用qq空间和qq邮箱。
在SSO模式中,业务流程如下:
- 认证系统单独与业务系统,不属于任何业务。
- 用户在认证中心进行认证,获取到身份认证信息。
- 用户访问业务系统时,将用户身份认证信息提交到认证系统进行身份认证。
在SSO模式中,身份认证中心独立于业务系统,更好的分布式管理,代码耦合性更低。但是,由于所有业务系统都依赖于认证系统,造成认证系统压力较大,且业务功能完全依赖于认证系统,当认证系统服务异常时,系统其余业务也将无法正常使用。
token模式
token验证模式是在SSO模式基础上衍生出来的一种验证模式
用户在Client服务上进行token申请,Client在验证用户身份后,将token颁发给用户,并将token和相关信息分发给业务服务器。用户在访问业务服务时,将token提交给业务服务器便可以验证身份。但由于其无状态的特性,使其无法在服务端进行销毁。
分布式/微服务中的身份验证 - JWT
JWT全称是Json Web Token,他是token模式的一种实现。因为存在数字签名,因此JWT存储的信息是安全的。
JWT的组成部分
一个JWT由三段端字符使用两个.
拼接构成,其中,这三段字符分别是:
JWT头
JWT头是一个JSON对象,存储着JWT的基本信息。
json{"alg":"HS256","typ":"JWT"}
在上面JWT头中,type表示这是一个JWT
令牌。alg定义了前面算法为HS256
。
有效载荷
有效载荷是JWT的主体部分,也是一个JSON对象。他包含三个部分:
标准注册声明 部分
标准注册声明一般包括以下部分:
iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共 部分
我们可以在巩公共部分添加任何信息,例如用户名等信息。公共部分可以在客户端进行解密,因此不建议添加敏感信息。
私有 部分
私有部分由客户端和服务端共同声明,可以存放一些常用的数据,但是不宜存放私密数据。
Hash签名
JWT的签发流程
- 定义JWT头
- 定义有效载荷
- 加密JWT和有效载荷
- 生成Hash签名
- Base64Url算法拼接 JWT头、有效载荷、Hash签名
通过以上步骤,我们就生成了一个JWT。
Java中生成JWT以及JWT解密
在这里,我们使用jjwt
来进行JWT的签发以及效验。
导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
签发JWT和解析JWT
/**
* @author : 刘欣的代码笔记
* @date : 2020/8/5 14:38
*/
public class JWT {
// 定义用于HS256的秘钥
public static final String APP_SECRET = "16dc664b9efa97a555fc1fd1ac6ecd3a";
public static String generateJwt(){
return Jwts.builder()
// 设置JWT头信息
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("刘欣的代码笔记")
// 签发时间
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 30)))
// 私有部分,可以通过链式添加多个
.claim("name", "刘欣的代码笔记")
// 加密
.signWith(SignatureAlgorithm.HS256, APP_SECRET).compact();
}
public static Claims checkJwt(String token){
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
return claimsJws.getBody();
}
public static void main(String[] args) {
String token = generateJwt();
System.out.println("jwt:" + token);
Claims claims = checkJwt(token);
System.out.println(claims.get("name"));
}
}
运行程序,打印出签发的JWT以及通过JWT解析出的name
字段:
jwt:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLliJjmrKPnmoTku6PnoIHnrJTorrAiLCJpYXQiOjE1OTY2MTE2NzQsImV4cCI6MTU5NjYxMzQ3NCwibmFtZSI6IuWImOaso-eahOS7o-eggeeslOiusCJ9.pYENO4scw8XldUxRwFpJNNzoTP0laVSmhzqbWiIotNM
刘欣的代码笔记
JWT过期处理
在上面的代码中,我们设置了JWT的过期时间为30分钟,那如果JWT过期了,我们该如何处理?我们还可以正常去解析JWT吗?我们将JWT的过期时间设置为1
,再次运行程序,我们看到程序抛出了异常:
异常的内容就是JWT已过期,那我们只需要解析JWT时捕获这个异常.
public static Claims checkJwt(String token){
try{
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
return claimsJws.getBody();
}catch (ExpiredJwtException ex){
System.err.println("jwt过期:" + ex.getLocalizedMessage());
return null;
}
}
这样当JWT过期时,将会被catch所捕获,我们在catch中进行过期的相应操作即可。