1 常见的认证机制
1.1 HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的 username 和 password,简言之,Basic Auth 是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic
Auth。
1.2 Cookie Auth
Cookie 认证机制就是为一次请求认证在服务端创建一个 Session 对象,同时在客户端的浏览器端创建了一个Cookie 对象;通过客户端带上来 Cookie 对象来与服务器端的 session 对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie 会被删除。但可以通过修改cookie 的 expire time 使 cookie 在一定时间内有效。
1.3 Oauth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一 web 服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
1.4 Token Auth
Token机制相对于Cookie 机制又有什么好处呢?
- 支持跨域访问: Cookie 是不允许垮域访问的,这一点对 Token 机制是不存在的,前提是传输的用户认证信息通过 HTTP 头传输.
- 无状态(也称:服务端可扩展行): Token 机制在服务端不需要存储 session 信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的 cookie 或本地介质存储状态信息.
- 更适用 CDN: 可以通过内容分发网络请求服务端的所有资料(如:javascript,HTML, 图片等),而服务端只要提供 API 即可.
- 去耦: 不需要绑定到一个特定的身份验证方案。Token 可以在任何地方生成,只要在你的 API 被调用的时候,你可以进行 Token 生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie 是不被支持的(你需要通过Cookie容器进行处理),这时采用 Token 认证机制就会简单得多。
- CSRF: 因为不再依赖于 Cookie,所以就不需要考虑对 CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询 session 信息)总比做一次 HMACSHA256 计算的 Token 验证和解析要费时得多
- 不需要为登录页面做特殊处理: 如果你使用 Protractor 做功能测试的时候,不再需要为登录页面做特殊处理
- 基于标准化: API 可以采用标准化的 JSON Web Token (JWT),这个标准已经存在多个后端库(.NET, Ruby, Java, Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。
2 基于JWT的Token认证机制实现
2.1 什么是 JWT?
JSON Web Token(JWT)是一个非常轻巧的规范,这个规范允许我们使用 JWT 在用
户和服务器之间传递安全可靠的信息。
2.2 JWT 组成
一个 JWT 实际上就是一个字符串,由头部、载荷和签名组成
2.2.1 头部
头部用于描述 JWT 最基本的信息,例如其类型、签名使用的压缩算法等,它可以被表示为一个 json 对象:
{"tye": "JWT", "alg": "HS256"}
在头部指定了签名算法是 HS256 算法,我们进行 Base64 编码,编码后的字符串如下:
eyJ0eWUiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9
2.2.2 载荷
载荷就是存放有效信息的地方
载荷(Payload)
{ "iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.example.com",
"sub": "[email protected]",
"GivenName": "djm",
"Surname": "DJM",
"Email": "[email protected]",
"Role": [ "Manager", "Project Administrator" ]
}
- iss: JWT 的签发者,是否使用是可选的;
- sub: JWT 所面向的用户,是否使用是可选的;
- aud: 接收 JWT 的一方,是否使用是可选的;
- exp(expires): 什么时候过期,这里是一个 Unix 时间戳,是否使用是可选的;
- iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;
- nbf (Not Before):如果当前时间在 nbf 里的时间之前,则 Token 不被接受;一般都会留一些余地,比如几分钟, 是否使用是可选的;
将上面的 JSON 对象进行 base64 编码可以得到下面的字符串,这个字符串我们将它称作JWT的Payload
eyAiaXNzIjogIk9ubGluZSBKV1QgQnVpbGRlciIsIAogICJpYXQiOiAxNDE2Nzk3NDE5LCAKICAiZXhwIjogMTQ0ODMzMzQxOSwgCiAgImF1ZCI6ICJ3d3cuZXhhbXBsZS5jb20iLCAKICAic3ViIjogImRqbUBleGFtcGxlLmNvbSIsIAogICJHaXZlbk5hbWUiOiAiZGptIiwgCiAgIlN1cm5hbWUiOiAiREpNIiwgCiAgIkVtYWlsIjogImRqbUBleGFtcGxlLmNvbSIsIAogICJSb2xlIjogWyAiTWFuYWdlciIsICJQcm9qZWN0IEFkbWluaXN0cmF0b3IiIF0gCn0=
2.2.3 签证
这个签证信息由三部分组成:header(base64)、payload(base64)、secret,中间使用 . 连接,然后使用 header 声明的的加密方式进行加言 secret 组合加密,就构成了第三部分
3 JWT 实现 JJWT
3.1 什么是 JJWT
JJWT 是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT 很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
3.2 JJWT 快速入门
3.2.1 token的创建
public String createJWT(String id, String subject, String roles) {
// 当前计算机时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setId(id)
// 设置用户
.setSubject(subject)
// 设置签约时间
.setIssuedAt(now)
// 签名和使用签名压缩算法、
.signWith(SignatureAlgorithm.HS256, key)
// 设置claims
.claim("roles", roles);
if (ttl > 0)
builder.setExpiration(new Date(nowMillis + ttl));
return builder.compact();
}
Jwts.builder() 返回了一个 DefaultJwtBuilder()
DefaultJwtBuilder 的属性,如下:
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private Header header; //头部
private Claims claims; //声明
private String payload; //载荷
private SignatureAlgorithm algorithm; //签名算法
private Key key; //签名key
private byte[] keyBytes; //签名key的字节数组
private CompressionCodec compressionCodec; //压缩算法
setHeader() 有两种参数形式,一种是 Header 接口的实现,一种是 Map, 其实 Header 接口也集成自 Map,如过以 Map 作为参数,在 setHeader 的时候会生成默认的 Header 接口实现 DefaultHeader。
如果不设置签名,也不进行压缩,alg 也应该存在,否则,对其解析就会报错,在生成 jwt 时,如果不设置签名,可将 alg 设置为 none
setPayload() 设置payload,直接赋值
setClaims() 设置claims,以参数创建一个新Claims对象,直接赋值
claim() 如果 builder 中 Claims 属性为空,则创建 DefaultClaims 对象,并把键值放入;如果 Claims 属性不为空,获取之后判断键值,存在则更新,不存在则直接放入。
当然也可以在Payload中添加一些自定义的属性claims键值对
JJWT 还提供了 JWT标准7个保留声明(Reserved claims)的设置方法,7个声明都是可选的,也就是说可以不用设置。
setIssuer()
setSubject()
setAudience()
setExpiration()
setNotBefore()
setIssuedAt()
setId()
当然也可以在Payload中添加一些自定义的属性claims键值对
compressWith() 压缩方法。当载荷过长时可对其进行压缩。可采用jjwt实现的两种压缩方法CompressionCodecs.GZIP、CompressionCodecs.DEFLATE
signWith() 签名方法:两个参数分别是签名算法和自定义的签名Key。签名 key 可以 byte[] 、String 及 Key 的形式传入,前两种形式均存入 builder 的 keyBytes 属性,后一种形式存入 builder 的 key 属性,如果是第二种 key,则将其进行 base64 解码获得 byte[] 。
compact() 生成JWT
3.2.2 token 解析
public Claims parseJWT(String jwtStr) {
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}