在任何一个系统中,几乎都离不开一个功能:登录功能。更准确的来说应该是身份验证功能。在开发过程中,有哪些常见的身份验证模式呢?

常用的身份验证模式

单一服务器模式

单一服务器模式一般由Session实现,用户在进行了登录验证后,服务器将用户的信息提取出来,存储在服务器上,再为这段信息定义一个唯一标识,将该标识通过cookies的形式返回给用户。
在单一服务器模式中,Session是有状态的,且Session由服务器进行保存,服务器可以随时销毁Session,相对来说较为安全。但是,由于Session是存储在服务器中,在分布式项目中需要Session共享方案,目前常用的Session共享方案存在性能瓶颈。

SSO模式

SSO模式全程是Single Sign On,也就是我们常说的单点登录。他的特点在多个应用系统中,只需要登录一次,就可以访问其他相互信任的系统。好比说我们登录了qq,那我们就无需再次登录就可以使用qq空间和qq邮箱。
在SSO模式中,业务流程如下:
image.png

  1. 认证系统单独与业务系统,不属于任何业务。
  2. 用户在认证中心进行认证,获取到身份认证信息。
  3. 用户访问业务系统时,将用户身份认证信息提交到认证系统进行身份认证。

在SSO模式中,身份认证中心独立于业务系统,更好的分布式管理,代码耦合性更低。但是,由于所有业务系统都依赖于认证系统,造成认证系统压力较大,且业务功能完全依赖于认证系统,当认证系统服务异常时,系统其余业务也将无法正常使用。

token模式

token验证模式是在SSO模式基础上衍生出来的一种验证模式
image
用户在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,再次运行程序,我们看到程序抛出了异常:
image
异常的内容就是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中进行过期的相应操作即可。