单点登录问题
在单体架构中的登录以及登录身份认证过程

在微服务架构中,按照相同的方式,再来分析下登录,以及登录身份认证过程:

假设用户在没有登录的情况下,发起查看购物车的请求:
- 当请求转发给订服务的时候,订单服务,订单服务发现用户的请求中没有包含JsessionId的Cookie,认为用户没登录,于是给前端返回需要登录的响应码,前端自动跳转到登录页面。
- 在登录界面,用户提交用户名和密码,向登录服务发起请求,登录服务验证用户发送的用户名和密码无误后,创建session,并返回包含JsessionId的Cookie
- 登录成功后,用户再次发起查看订单的请求,此时虽然携带了包含JsessionId的Cookie,但是这个JsessionId对应却并不是订单服务中的某一个Session的id,即在订单服务中找不到与此JssionId对应的Session,于是订单服务还是认为用户未登录。
经过以上分析,我们发现,在微服务架构中适用于单体应用的登录以及登录验证逻辑,在微服务架构中不适用了。这就引出了微服务架构(分布式系统的单点登录问题)。所以,在微服务架构中,我们就需要解决单点登录问题——Single Sign On,简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
如何解决单点登录问题呢?有如下两种思路
- 共享会话: 在用户登录之后,将用户登录数据放在Redis中,只需判断用户在Redis中是否有登录数据,就可判断其是否登录
- 基于JWT
但是共享会话有自己的弊端:1)用户的登录会话数据存储在Redis中,如果同时登录人数较多占用大量内存;2)访问Redis还要经过底层的网络通信,需要通信时间。而相比较之下,JWT方案不需要在服务器端存储用户登录数据,而且验证登录只需在自己的java进程内存中验证JWT无需经过网络通信。所以我们选择使用JWT
JWT介绍
JWT全称 Json·Web·Token,是一个开放标准(RFC·7519),它定义了一种紧凑的,自包含的方式,用于作为JSON对象在各方之间安全的传输信息。该信息可以被验证和信任,因为它是数字签名的。JWT是目前最流行的分布式系统登录身份认证解决方案。
使用场景
下列场景中使用JWT是很有用的:
- Information Exchange(信息交换):对于安全的在各方之间传输信息而言,Json·Web·Token无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥配对,你可以确定发送人就是他们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容有没有被篡改。
- 基于JWT实现数据传输是一种比较成熟的机制(标准化,轻量级)且相对安全,所以我们基于JWT实现单点登录功能。我们实现单点登录的思路如下:

基本格式
JSON·Web·Token由三部分组成,他们之间用圆点(·)连接,这三部分分别是:
- Header
- Payload
- Signature
Header由两部分信息组成:
- type:声明类型,这里是jwt
- alg:声明加密的算法 通常直接使用 HMAC、RSA
Payload就是存放有效信息的地方(不强制)
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
- claim:jwt存放信息的地方
Signature就是签名信息
因此,一个典型的JWT看起来是这个样子的:xxxxxxxx·yyyyyyyyyy·zzzzzzzzzzz
具体如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3bGd6cyIsImV4cCI6MTU4Nzk3MzY1NywidXNlciI6Ijk2MkYxODkwNTVFMzRFNzVERjVGMzQ0QTgxODNCODdGIn0.APehq9dxRiilgTOGyuz9qtZxvPDIJ5QIIVUCLYeX1QE
API
引入如下依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.18</version>
</dependency>
使用如下项目中的工具类:
JwtTool
public class JwtTool {
private static final Duration JWT_TOKEN_TTL = Duration.ofMinutes(24 * 60 * 30);
private static final String PAYLOAD_USER_KEY = "user";
private static final String UserType = "userType";
// 生成数字签名使用的秘钥
private byte[] key;
public JwtTool(String keyStr) {
key = keyStr.getBytes();
}
/**
* 创建 jwttoken
*
* @param currentUserId 用户id
* @param name 用户姓名/昵称
* @param avatar 用户头像
* @param userType 1: 普通用户 2: 服务人员 3: 后台人员
* @return jwt token
*/
public String createToken(Long currentUserId, String name, String avatar, int userType) {
// 名称base64编码,防止token无法解析
String encodeName = StringUtils.isEmpty(name) ? null : Base64Utils.encodeStr(name);
// 1.生成jws
return JWT.create()
// 向jwt中放入数据,对应的json字段名称为user
.setPayload(PAYLOAD_USER_KEY, new CurrentUserInfo(currentUserId, encodeName, avatar, userType))
// 设置过期时间
.setExpiresAt(new Date(System.currentTimeMillis() + JWT_TOKEN_TTL.toMillis()))
.setCharset(Charset.forName("UTF-8"))
// 设置签名所使用的秘钥
.setKey(key)
.sign();
}
/**
* 从访问token中获取用户信息
*
* @param token 访问token
* @return 用户信息
*/
public CurrentUserInfo parseToken(String token) {
try {
JWT jwt = JWT.of(token).setKey(key);
// 获取jwt中的用户数据,这里会自动校验数字签名
JSONObject payload = (JSONObject) jwt.getPayload(PAYLOAD_USER_KEY);
// 获取并封装数据
CurrentUserInfo userInfo = new CurrentUserInfo();
userInfo.setId(payload.getLong("id"));
String name = payload.getStr("name");
String avatar = payload.getStr("avatar");
String userType = payload.getStr(UserType);
if (userType != null) {
userInfo.setUserType(Integer.parseInt(userType));
}
if (StringUtils.hasText(name)) {
String decodeName = Base64Utils.decodeStr(name);
userInfo.setName(decodeName);
}
userInfo.setAvatar(avatar);
return userInfo;
} catch (Exception e) {
return null;
}
}
生成jwt字符串
@Test
public void createJWT() {
String key = "jwt-test";
JwtTool jwtTool = new JwtTool(key);
/**
* 第一个参数表示用户id
* 第二个参数表示用户名
* 第三个参数表示头像
* 第四个参数表示用户类型: 因为我们有不同的类型的用户,后端工作人员,普通用户,服务提供人员
* 这四个参数代表用户登录后要放入到JWT中的数据
*/
String admin = jwtTool.createToken(1L, "admin", "http://xxx", 1);
System.out.println(admin);
}
解析jwt中的数据
@Test
public void parseJWT() {
String key = "jwt-test";
JwtTool jwtTool = new JwtTool(key);
String jwtStr = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7ImlkIjoxLCJuYW1lIjoiWVdSdGFXND0iLCJhdmF0YXIiOiJodHRwOi8veHh4IiwidXNlclR5cGUiOjF9LCJleHAiOjE3NDk1NTA4OTN9.Tjh1leMEf4iReYe4rO_L1yQXitMHY_Zwh74Xw4ZneYY";
CurrentUserInfo currentUserInfo = jwtTool.parseToken(jwtStr);
System.out.println(currentUserInfo);
}