Skip to content

02. 在用户服务中添加认证(0.0.2)

zhengyangyong edited this page Jul 2, 2018 · 2 revisions

0.0.1-SNAPSHOT中我们完成了注册和登录这两个基本功能的用户服务;众所周知任何一个完整的系统都必须对请求做认证,但HTTP是一个无状态协议,我们需要将用户登录成功后的认证信息与后继的请求关联起来。

我们非常熟悉的做法是使用Session或Cookie:

  • Session存储在服务端,因此具备良好的防篡改能力,但弊端是使服务有状态,微服务系统中,同一个微服务会依据系统压力的大小弹性伸缩出多个运行实例负载均衡,跨实例访问会状态丢失。
  • Cookie存储在客户端,它正好与Session相反,优势是服务不必保持状态,但弊端是客户比较容易的篡改Cookie信息,例如修改过期时间以逃避验证,而且浏览器对Cookie也有较多限制。

那么,如何兼顾这两方面的需求呢?Token就是一个比较好的解决方案。

Token中文翻译为令牌,它将登录认证后的信息签名后返回,服务端不保存,客户端请求的时候将认证的完整信息附带上提供给服务端验签,签名可以保证信息不被篡改。

JWT (Java Web Token)规范

了解Token的原理,自然要关注Token的格式,JWT就是这样一个基于JSON的开放标准RFC-7519,简而言之它由三部分构成:

  1. Header: 声明Token的类型也就是jwt,以及加密算法;
  2. Playload:存放有效信息,既包含标准签发者、用户、签发时间、过期时间,唯一标识等信息;也可以存放用户自定义的声明信息,例如权限控制相关的内容;
  3. Signature:签名信息,包含Header和Playload的原始信息(Base64编码过)以及签名过后的信息。

增加基于JWT的认证服务

实现JWT基础能力

我们在user-service中使用io.jsonwebtoken:jjwt开源项目添加jwt基础能力,在pom中增加它的依赖:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.8.0</version>
</dependency>

之后定义一个通用的TokenStore接口:

public interface TokenStore {
  String generate(String userName);
  String validate(String token);
}

generate用于为指定的userName生成一个新的token,validate则用于验证token是否正确。

最后在JwtTokenStore中完成具体实现,注意:

  private final String secretKey;
  private final int secondsToExpire;

  public JwtTokenStore() {
    this.secretKey = "someSecretKeyForAuthentication";
    this.secondsToExpire = 60 * 60 * 24;
  }

我们使用HMAC SHA-512算法进行签名,secretKey是使用的密钥,这个字符串非常重要,最好能做到定期更换;secondsToExpire为签发的Token有效期,我们设置为1天(60秒乘以60分乘以24小时)。

增加认证Endpoint

我们先在common中增加AuthenticationService接口:

public interface AuthenticationService {
  String validate(String token);
}

validate方法用于传入token String并验证token是否正确。

然后在user-service的AuthenticationServiceImpl中完成具体实现。

增强用户 Endpoint

目前UserServiceImpl中的login只是简单的验证了用户名和密码,我们增加创建Token的代码,注入进应答的HttpHeaders中:

String token = tokenStore.generate(user.getName());
HttpHeaders headers = generateAuthenticationHeaders(token);
//add authentication header
return new ResponseEntity<>(new LoginResponseDTO(), headers, HttpStatus.OK);

之后客户端的请求必须带上Token并验证通过后才能够正常调用服务。