常见的认证机制
一、HTTP Basic Auth
HTTP Basic Auth 就是每次请求API时都需要提供用户的账号和密码来验证用户身份,是配合 RESTful API 使用的最简单的认证方式(服务端不保存用户状态),但由于有把账号和密码暴露给第三方客户端的风险,因此在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth。
二、Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端
(浏览器)创建了一个Cookie对象,通过客户端发送请求时携带的Cookie对象与服务端的
session对象匹配来实现状态管理。Cookie认证机制是一种传统非 RESTful API 风格的的认证方式。当关闭浏览器的时候,客户端的Cookie会被删除。也可以通过修改Cookie的到期时间使Cookie在一定时间内有效。
优点:信息存储在服务端,相对安全。实现简单,session技术属于servlet规范。
缺点:不符合 RESTful API 风格,在服务端存储了用户状态,维护session需要消耗资源。分布式环境中需要处理session共享问题。
三、OAuth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密资源(如照片,视频,联系人列表),而无需将账号和密码提供给第三方应用。
OAuth允许用户提供一个令牌来访问用户存放在特定服务提供者的数据,而不是使用账号和密码来访问。每一个令牌授权一个特定的第三方系统(如视频编辑网站)在特定的时段内(如接下来的2小时内)访问特定的资源(如某一相册中的视频)。这样OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。OAuth 认证机制不符合 RESTful API 风格,需要额外的认证服务器,成本高。
基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应
用,但是不太适合拥有自有认证权限管理的企业应用。
四、Token Auth
1、Token Auth认证机制
Token Auth是一种符合 RESTful API 风格的认证机制。使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。
Token Auth认证机制的流程:
(1)客户端使用账号和密码向服务端发送登录请求。
(2)服务端收到请求后去验证账号和密码。
(3)账号和密码验证成功,服务端会签发一个Token令牌,并将这个Token令牌发送到客户端。
(4)客户端收到Token令牌后会将其保存在Cookie中。
(5)客户端每次向服务端请求资源时,都要携带服务端签发的Token令牌(放在请求头)。
(6)服务端收到请求后会去验证客户端请求携带的Token令牌,如果Token令牌验证成功,就向客户端返回请求的数据。
Token Auth认证机制相对于Cookie Auth认证机制的好处:
(1)支持跨域访问:Cookie是不允许跨域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输。
(2)无状态(服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token令牌自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储用户状态信息。
(3)更适用CDN:可以通过内容分发网络请求服务端的所有资料(如javascript,HTML,图片等),而服务端只要提供API即可。
(4)去耦:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在API被调用的时候,可以进行Token生成调用即可。更适用于分布式环境。
(5)更适用于移动应用:当客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
(6)CSRF:因为不再依赖于Cookie,所以不需要考虑对CSRF(跨站请求伪造)的防范。
(7)性能:一次网络往返时间(通过数据库查询session信息)比做一次MACSHA256计算的Token验证和解析要费时得多。
(8)不需要为登录页面做特殊处理:如果使用Protractor做功能测试的时候,不再需要为登录页面做特殊处理。
(9)基于标准化:API可以采用标准化的 JSON Web Token (JWT),这个标准已经存在多个后端库(如.NETR,uby,Java,Python,PHP)和多家公司的支持(如Firebase,Google,Microsoft)。
2、基于JWT的Token认证机制实现
JWT(JSON Web Token)是一个非常轻巧的规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
2.1、加密方式
1、不可逆的加密方式:只能通过加密算法将明文加密成密文。
(1)MD5加密:使用一个未知数(明文)通过加密算法生成加密字符串。
(2)BCRYPT强哈希加密:使用两个未知数(明文和时间戳)通过加密算法生成加密字符串。
2、可逆的加密方式:明文能通过加密算法加密生成密文,密文也可以通过加密算法解密生成明文。
JWT加密方式:使用可逆的加密方式,为了服务端能解析得到客户端发送Token令牌中的用户信息。(使用明文和一个自定义的密钥通过加密算法生成Token令牌,只要别人不知道这个密钥,即使知道加密算法,没有这个密钥的话也无法解析或者生成Token令牌)。
2.2、JWT组成
一个JWT实际上就是一个字符串,它由三部分组成:头部、载荷与签名。
1、头部 Header
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"} // 类型为JWT,使用HS256的加密算法
在头部指明了签名算法是HS256算法,将其进行BASE64编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码。
2、载荷 playload
载荷是存放有效信息的地方,这些有效信息包含三个部分:
(1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
私有的声明指的就是自定义的claim。自定义的claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证。而自定义的claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然后将其进行base64编码,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
3、签证 signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:header (base64后) 、payload (base64后)、secret。
签证信息需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密。
签证信息:
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将头部、载荷与签名这三个部分用.
连接成一个完整的字符串,构成了最终的JWT。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt。
2.3、Java的JWT依赖
(1)需要在项目的pom.xml文件中导入JWT依赖。
<!-- JWT工具依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
(2)创建JWTTest类进行测试。
- 使用明文和密钥(自定义的一个字符串,如abcd123456789)生成Token令牌。进行权限认证时需要使用密钥从Token令牌中解析得到用户相关信息。每次运行会发现每次运行的结果是不一样的,因为载荷中包含了时间。
setIssuedAt方法:用于设置签发时间;
signWith方法:用于设置签名秘钥;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTTest {
@Test
//生成token
public void fun1() {
String token = Jwts
.builder() //获得JwtToken的构建工具
.setId("1") //设置载荷部分的键值对,一般用来设置用户id
.setSubject("tom") //设置载荷部分的键值对,一般用来设置用户名
.setIssuedAt(new Date()) // 设置载荷部分的键值对,token创建时间
.signWith(SignatureAlgorithm.HS256, "abcd123456789")//设置密钥签名方式 以及密钥的值
.compact();//返回Token
System.out.println(token);
}
@Test
//解析token
public void fun2() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoidG9tIiwiaWF0IjoxNjQ3NzAyNDY5fQ.cEXpWAUM1izLN2PBMRfgc7z1xb-Qqk5uuFHz110QzJE";
Claims body = Jwts.parser() //获得token解析器
.setSigningKey("abcd123456789") //指定解析密钥
.parseClaimsJws(token) //解析token
.getBody();//获得载荷部分
//从载荷中提取信息
String id = body.getId(); //用户id
String name = body.getSubject(); //用户名
Date date = body.getIssuedAt(); //token创建时间
System.out.println("用户id: "+id+",用户名: "+name+",token创建时间: "+date);
}
}
- 刚才只是存储了id和subject两个信息,如果想存储更多的信息(例如用户信息)可以定义自定义claims,然后生成有自定义claims的Token。
在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个Token,那服务端收到这个Token并解析出Token中包含的信息(例如用户id、用户角色等,这些信息需要定义在自定义claims中),根据这些信息查询数据库进行验证。
setClaims方法:用于设置自定义载荷,参数是Map类型;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTTest {
@Test
//生成有自定义载荷的token
public void fun3() {
//将自定义数据放入map中
Map<String,Object> map = new HashMap<>();
map.put("roles","admin");
String token = Jwts
.builder() //获得JwtToken的构建工具
.setClaims(map) //设置自定义载荷,自定义载荷需要放置到最前面
.setId("1") //设置载荷部分的键值对,一般用来设置用户id
.setSubject("tom") //设置载荷部分的键值对,一般用来设置用户名
.setIssuedAt(new Date()) // 设置载荷部分的键值对,token创建时间
.signWith(SignatureAlgorithm.HS256, "abcd123456")//设置密钥签名方式以及密钥的值
.compact();//返回Token
System.out.println(token);
}
@Test
// 解析有自定义载荷的token
public void fun4() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJyb2xlcyI6ImFkbWluIiwiaWF0IjoxNjQ3NzAyNDk0LCJqdGkiOiIxIn0.Q63XySamNeCxPqTBJrmd-U1YrGXZkd5J0I9y5u3TIg8";
Claims body = Jwts.parser() //获得token解析器
.setSigningKey("abcd123456") //指定解析密钥
.parseClaimsJws(token) //解析token
.getBody();//获得载荷部分
//从载荷中提取信息
System.out.println("用户id: " + body.getId());//用户id
System.out.println("用户名: " + body.getSubject());//用户名
System.out.println("token创建时间: " + body.getIssuedAt());//token创建时间
System.out.println("自定义的roles: " + body.get("roles"));//自定义的roles
}
- 签发的token不可能是永久生效的,所以需要给token设置一个过期时间。
setExpiration方法:用于设置过期时间,当未过期时可以正常读取,当过期时会引发io.jsonwebtoken.ExpiredJwtException异常。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTTest {
@Test
//生成设置有效时间的token
public void fun5() {
//将自定义放入map中
Map<String,Object> map = new HashMap<>();
map.put("roles","admin");
// 获得当前时间(毫秒数)
long currentTime = System.currentTimeMillis();
// 设置30秒后过期(毫秒数)
long expirationTime = currentTime + 1000 * 30;
String token = Jwts
.builder() //获得JwtToken的构建工具
.setClaims(map) //设置自定义载荷,自定义载荷需要放置到最前面
.setId("1") //设置载荷部分的键值对,一般用来设置用户id
.setSubject("tom") //设置载荷部分的键值对,一般用来设置用户名
.setIssuedAt(new Date()) // 设置载荷部分的键值对,token创建时间
.setExpiration(new Date(expirationTime))//设置载荷部分的键值对,设置失效时间
.signWith(SignatureAlgorithm.HS256, "abcd123456")//设置密钥签名方式 以及密钥的值
.compact();//返回Token
System.out.println(token);
}
@Test
//解析有设置有效时间的token
public void fun6() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJyb2xlcyI6ImFkbWluIiwiZXhwIjoxNjQ3NzAyNTU2LCJpYXQiOjE2NDc3MDI1MjYsImp0aSI6IjEifQ.PsmeiivM3nLtR8Oz7YRq1pXEPDK0_xEG_-LCGM-CTb0";
Claims body = Jwts.parser() //获得token解析器
.setSigningKey("abcd123456") //指定解析密钥
.parseClaimsJws(token) //解析token
.getBody();//获得载荷部分
//从载荷中提取信息
System.out.println("用户id: " + body.getId());//用户id
System.out.println("用户名: " + body.getSubject());//用户名
System.out.println("token创建时间: " + body.getIssuedAt());//token创建时间
System.out.println("自定义的roles: " + body.get("roles"));//自定义的roles
System.out.println("token失效时间: " + body.getExpiration());//token失效时间
}
}