RESTful API认证方式
一般来讲,对于RESTful API都会有认证(Authentication)和授权(Authorization)过程,保证API的安全性。
Authentication vs. Authorization
Authentication指的是确定这个用户的身份,Authorization是确定该用户拥有什么操作权限。
认证方式一般有三种
Basic Authentication
这种方式是直接将用户名和密码放到Header中,使用Authorization: Basic Zm9vOmJhcg==
,使用最简单但是最不安全。
TOKEN认证
这种方式也是再HTTP头中,使用Authorization: Bearer <token>
,使用最广泛的TOKEN是JWT,通过签名过的TOKEN。
OAuth2.0
这种方式安全等级最高,但是也是最复杂的。如果不是大型API平台或者需要给第三方APP使用的,没必要整这么复杂。
一般项目中的RESTful API使用JWT来做认证就足够了。
什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的, 特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息, 以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT官网:https://jwt.io/
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。
header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
这里的加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。这里使用基于密钥的Hash算法HMAC生成散列值。
- MD5 message-digest algorithm 5 (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值
- SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
- HMAC (Hash Message Authentication Code,散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证
完整的头部就像下面这样的JSON:
1
2
3
4{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分
1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
载荷就是存放有效信息的地方,这些有效信息包含三个部分:
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密
私有的声明:
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
1
2
3
4
5{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分:
1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
1 | header (base64后的) |
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串, 然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
1 | // javascript |
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证, 所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
如何应用
一般是在请求头里加入Authorization,并加上Bearer标注:
1
2
3
4
5fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
服务器负责解析这个HTTP头来做用户认证和授权处理。大致流程如下:
安全相关
JWT协议本身不具备安全传输功能,所以必须借助于SSL/TLS的安全通道,所以建议如下:
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议
和SpringBoot集成
简要的说明下我们为什么要用JWT,因为我们要实现完全的前后端分离,所以不可能使用session,cookie的方式进行鉴权, 所以JWT就被派上了用场,可以通过一个加密密钥来进行前后端的鉴权。
程序逻辑:
- 我们POST用户名与密码到/login进行登入,如果成功返回一个加密token,失败的话直接返回401错误。
- 之后用户访问每一个需要权限的网址请求必须在header中添加Authorization字段,例如Authorization: token,token为密钥。
- 后台会进行token的校验,如果不通过直接返回401。
这里我讲一下如何在SpringBoot中使用JWT来做接口权限认证,安全框架依旧使用Shiro,JWT的实现使用 jjwt
添加Maven依赖
1 | <dependency> |
创建用户Service
这个在shiro一节讲过如果创建角色权限表,添加用户Service来执行查找用户操作,这里就不多讲具体实现了,只列出关键代码:
1 | /** |
用户信息类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class ManagerInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Integer id;
/**
* 账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* md5密码盐
*/
private String salt;
/**
* 一个管理员具有多个角色
*/
private List<SysRole> roles;
JWT工具类
我们写一个简单的JWT加密,校验工具,并且使用用户自己的密码充当加密密钥, 这样保证了token 即使被他人截获也无法破解。并且我们在token中附带了username信息,并且设置密钥5分钟就会过期。
1 | public class JWTUtil { |
编写登录接口
为了让用户登录的时候获取到正确的JWT Token,需要实现登录接口,这里我编写一个LoginController.java
:
1 | /** |
注意上面登录的时候,我会从数据库中把这个用户取出来,密码加盐算MD5值比较,通过之后再用密码作为密钥来签名生成JWT。
编写RESTful接口
先编写一个通用的接口返回类:
1 | /** |
通过SpringMVC实现RESTful接口,这里我只写一个示例方法:
1 | /** |
自定义异常
为了实现我自己能够手动抛出异常,我自己写了一个UnauthorizedException.java
1 | public class UnauthorizedException extends RuntimeException { |
处理框架异常
之前说过restful要统一返回的格式,所以我们也要全局处理Spring Boot的抛出异常。利用@RestControllerAdvice能很好的实现。 注意这个统一异常处理器只对认证过的用户调用接口中的异常有作用,对AuthenticationException没有用
1 |
|
配置Shiro
大家可以先看下官方的 Spring-Shiro 整合教程,有个初步的了解。 不过既然我们用了SpringBoot,那我们肯定要争取零配置文件。
实现JWTToken
JWTToken差不多就是Shiro用户名密码的载体。因为我们是前后端分离,服务器无需保存用户状态,所以不需要RememberMe这类功能, 我们简单的实现下AuthenticationToken接口即可。因为token自己已经包含了用户名等信息,所以这里我就弄了一个字段。 如果你喜欢钻研,可以看看官方的UsernamePasswordToken是如何实现的。
1 | public class JWTToken implements AuthenticationToken { |
实现Realm
realm的用于处理用户是否合法的这一块,需要我们自己实现。
1 | /** |
在doGetAuthenticationInfo
中用户可以自定义抛出很多异常,详情见文档。
重写Filter
所有的请求都会先经过Filter,所以我们继承官方的BasicHttpAuthenticationFilter
,并且重写鉴权的方法,
另外通过重写preHandle,实现跨越访问。
代码的执行流程preHandle->isAccessAllowed->isLoginAttempt->executeLogin
1 | public class JWTFilter extends BasicHttpAuthenticationFilter { |
编写ShiroConfig配置类
这里我还增加了EhCache缓存管理支持,不需要每次都调用数据库做授权。
1 |
|
里面URL规则自己参考文档 http://shiro.apache.org/web.html ,这个在shiro那篇说的很清楚了。
运行验证
最后是将代码跑起来验证这一切是否正常。
启动SpringBoot后,先通过POST请求登录拿到token
然后在调用入网接口的时候在header中带上这个token认证:
如果token认证不正确会报异常:
如果使用普通用户登录,认证正确但是授权访问接口失败,会返回如下的未授权结果: