什么是SSO

SSO(Single Sign On): 单点登录。
意思是讲,在多个应用系统中,用户只要登录一次,就可以访问所有相互信任的应用。
就比如天猫和淘宝。

什么是JWT

JWT(Json Web Tokens): 是一种规范。
其实我更愿意把它认为是一种规范的原因是,它本身是一种Token生成机制。
但是内部采用了更为结构化的格式来进行认证(Authentication)。

JWT的数据结构

它是一个很长的字符串,中间用点(.)分隔成三个部分。

JWT 的三个部分依次如下:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)
    写成一行,就是下面的样子。

Header.Payload.Signature

JWT数据结构

使用JWT实现SSO的原理

这里盗用一幅网上很易懂的漫画。

  • 首先,服务器应用(下面简称“应用”)让用户通过Web表单将自己的用户名和密码发送到服务器的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
    jauth1
  • 接下来,应用和数据库核对用户名和密码。
    jauth2
  • 核对用户名和密码成功后,应用将用户的id(图中的user_id)作为JWT Payload的一个属性,将其与头部分别进行Base64URL编码拼接后签名,形成一个JWT。这里的JWT就是一个形同lll.zzz.xxx的字符串。
    jauth3
  • 应用将JWT字符串作为该请求Cookie的一部分返回给用户。注意,在这里必须使用HttpOnly属性来防止Cookie被JavaScript读取,从而避免跨站脚本攻击(XSS攻击)。
    jauth4
  • 在Cookie失效或者被删除前,用户每次访问应用,应用都会接受到含有jwt的Cookie。从而应用就可以将JWT从请求中提取出来。
    jauth5
  • 应用通过一系列任务检查JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
    jauth6
  • 应用在确认JWT有效之后,JWT进行Base64解码(可能在上一步中已经完成),然后在Payload中读取用户的id值,也就是user_id属性。这里用户的id为1025。
    jauth7
  • 应用从数据库取到id为1025的用户的信息,加载到内存中,进行ORM之类的一系列底层逻辑初始化。
    jauth8
  • 应用根据用户请求进行响应。
    jauth9

Tips

  • 注意Base64算法与Base64URL算法是不一致的。
  • 这里可以加入两个token: token和refreshToken。如果用户在refreshToken过期之前继续进行了操作,可以延长token和refreshToken的有效期。
  • JWT可以储存在 Cookie 里面(可以指定 httponly,来防止被Javascript读取,也可以指定secure,来保证token只在HTTPS下传输,并且xsrf随机数有完善的应用机制),
    也可以储存在 localStorage(容易受到XSS攻击),
    或者浏览器Header的Authorization字段里面放置Token(适用于ajax请求或者api请求,可以方便的设置auth头,方便跨域请求)。
  • 客户端每次使用的时候都带上这个token进行认证。

源码层次分析JWT如何生成

在io.jsonwebtoken包的具体实现类DefaultJwtBuilder中,提供一个完整的JWT生成方法compact()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Header header = ensureHeader();

Key key = this.key;
if (key == null && !Objects.isEmpty(keyBytes)) {
key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
}

JwsHeader jwsHeader;

if (header instanceof JwsHeader) {
jwsHeader = (JwsHeader)header;
} else {
jwsHeader = new DefaultJwsHeader(header);
}

if (key != null) {
// 生成信息,"alg"->"HS256"
jwsHeader.setAlgorithm(algorithm.getValue());
} else {
//no signature - plaintext JWT:
jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());
}
// jwsHeader进行Base64URL加密,生成Header部分
String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json.");

String base64UrlEncodedBody;

if (compressionCodec != null) {

byte[] bytes;
try {
bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Unable to serialize claims object to json.");
}

base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes));

} else {
// claims包含了用户信息,{id,obj.str_time,end_time},由于是可逆算法,最好不要讲隐私信息存放到payload中。
// 进行Base64URL加密,生成Payload部分。
base64UrlEncodedBody = this.payload != null ?
TextCodec.BASE64URL.encode(this.payload) :
base64UrlEncode(claims, "Unable to serialize claims object to json.");
}

// 定义拼接规则
String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody;

// 对前两个部分数据进行签名,防止篡改,key密钥是事先约定好的,保存在服务端。
if (key != null) { //jwt must be signed:

JwtSigner signer = createSigner(algorithm, key);

String base64UrlSignature = signer.sign(jwt);

// 最终生成JWT
jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature;
} else {
// no signature (plaintext), but must terminate w/ a period, see
// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1
jwt += JwtParser.SEPARATOR_CHAR;
}

其他问题总结

  • 关于Cookie的设置

    1
    2
    3
    4
    5
    6
    7
    Cookie cookie = new Cookie(name, value);
    cookie.setSecure(false); // 设置是否只能通过https来传递此条cookie,默认是false
    cookie.setHttpOnly(true);// 防御XSS攻击
    cookie.setMaxAge(-1);// 规定cookie多长时间之后过期.负值(默认值)表明cookie仅仅用于当前浏览会话,并不存储到磁盘上.
    cookie.setDomain("localhost");// 可以访问此cookie的域名
    cookie.setPath("/");// 可在同一应用服务器内共享cookie
    response.addCookie(cookie);
  • Ajax请求,后台重定向失效的问题。
    每次发起ajax请求, ajax请求的结果就是这个页面(具体的说,这次请求返回的就是登录页面的源代码),
    所以浏览器不会发生跳转.
    还是只能从responseText中获取跳转信息使用js跳转
    这个需要深入了解一下Ajax请求和response.sendRedirect的机制。

  • 用户退出登录的时候,设置Cookie失效即可。

源码,欢迎Star

基于JWT实现单点登录