1. 认证与授权
认证(Authentication)和授权(Authorization)是比较容易混淆的概念。
- 认证:验证你身份的凭据(用户名/ID、密码等),通过这个凭据,系统就能判断出你是平台的用户。
- 授权:发生在认证之后,掌管我们访问系统的权限,也就是你有权访问什么,无权访问什么。
简而言之的话就是:
- 认证:你是谁。
- 授权:你有权限干什么。
HTTP是无状态的,客户端与服务端会话完成后,服务端不会保存会话信息。也就是说,服务端并不知道是谁连接了他。那服务器怎么知道是谁在登录?通过认证,证明你是你自己。
互联网中的认证:
- 用户名密码登录
- 邮箱发送登录链接登录
- 手机号接收验证码登录
互联网中的授权:
- APP询问授予权限
- 小程序授予权限
- 会员权限
- 封禁限制权限
实现认证和授权的前提就是需要一种证书标记访问者的身份,这就是凭证。
例如我们的居民身份证,能证明持有人的身份。
例如网站有游客模式和登录模式,游客模式可以游览,但是想发言的话只能登录,获得令牌,令牌就是一个凭证。
2. 什么是cookie?
cookie是name=value键值对。
cookie存储在客户端,请求后,服务器发送回浏览器,浏览器在下次向同一服务器发送请求时,作为参数携带,并发送到服务器上。
cookie是一个具体的东西,指的是浏览器实现的一种数据存储功能。
cookie是不可跨域的,每个cookie绑定单一的域名。
2.1 cookie和localStorage区别
cookie | localStorage | |
---|---|---|
存储量 | 4K | 5M |
请求携带 | 会被带到HTTP请求中 | 不会带到 HTTP 请求中 |
生命周期 | 浏览器关闭,cookie消失,也可设置过期时间 | 持久化存储,除非清除浏览器缓存 |
爬虫 | 可以被爬虫抓取 | 不能被爬虫抓取 |
可操作性 | cookie可以存储数据,还可以设置一些操作属性 | 只是存储数据 |
使用场景 | 客户端与服务端的信息传递 | 客户端的数据存储 |
3. 什么是session?
session是基于cookie实现的,session存储在服务器端,每个session都会有一个对应的sessionID。
sessionID会被存储到客户端的cookie的value,对应的name为JSESSIONID。
浏览器第二次访问服务器时,服务端从cookie中获取sessionID,再查找对应的session信息。如果能找到,就找到了用户信息,证明可以登录。
session的缺陷是:如果web服务器做了负载均衡,那么下次发起请求到另一台服务器时,session会丢失。
解决方式:单独使用一台服务器存储所有session,每台服务器都去这台服务器上找。
sessionID是连接cookie和session的一道桥梁。
4. cookie和session概念区别?
cookie | session | |
---|---|---|
存储位置 | 客户端 | 服务端 |
存储数据类型 | 只能是字符串 | 可以是任意类型 |
存储数据量 | 4k | 无限制 |
生命周期 | 浏览器关闭,cookie消失,也可设置过期时间 | 一般较短,半小时左右 |
安全性 | 较不安全 | 较安全 |
5. 什么是token?
token:访问资源接口(API)时所需要的资源凭证。
token代替的就是sessionID的位置。
token身份验证过程:
- 客户端通过用户名和密码发送请求。
- 服务端验证用户信息是否存在。
- 服务端将登录凭证(用户名、密码等)做数字签名得到token给客户端。
- 客户端储存token,用于再次发送请求放入请求头。
- 服务端验证token,进行解密和签名认证,并返回数据。
存储token的是cookie或localStorage,token被放到HTTP的header里。
用解析token的时间换取session的存储空间,减轻服务器的压力,减少频繁查询数据库。
6. session和token概念区别?
session | token | |
---|---|---|
存储位置 | 存储在服务端 | 存储在客户端 |
状态化 | 有状态 | 无状态 |
跨域 | 可跨域 | 不可跨域 |
如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 token。
session对于非浏览器的客户端或手机移动端不适用,因为session依赖于cookie,而移动端没有cookie。
前后端分离系统中不适用session,后端部署复杂,前端发送的请求往往经过多个中间件到达后端,cookie中关于session的信息会转发多次。
7. 项目中实现token
7.1 使用JWT(Json Web Token)
JWT的优势是:JWT数据量小,传输速度很快。由于JWT是以JSON加密形式保存在客户端的,所以JWT可以跨语言。
JWT结构:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输时会将JWT的三部分分别进行Base64编码后连接形成最终的字符串,它的算法是这样的:
JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
了解一下其组成参数
Header
{
"alg": "HS256",
"typ": "JWT"
}
alg表示签名使用的算法,默认为HMAC SHA256,typ表示令牌的类型,JWT令牌统一写为JWT。
Payload
{
"userName": "zhangsan",
"dept": "safeAI",
"userId": 3
}
放入我们自定义的私有字段,比如用户信息数据。
需要注意一点:默认情况下JWT是未加密的,因为只是采用Base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息。
Signature
对上面两部分数据签名,需要使用Base64编码后的header和payload数据,通过指定算法生成哈希,保证数据不会被篡改。
首先需要指定一个密钥(secret),该密钥仅仅保存在服务器中,不能向用户公开。然后使用HMAC SHA256算法(Payload里指定)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
计算出签名哈希后,JWT头、有效载荷和签名哈希三个部分组成一个字符串,每个部分用.分隔,构成整个JWT对象。例如:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.eyJkZXB0IjoiaWt1buWQjuaPtOS8miIsInVzZXJOYW1lIjoi5byg5LiJIiwiZXhwIjoxNjY1NjMwMjc1LCJ1c2VySWQiOiIzIn0
.Oy82soyC8JGNFUzlZsZEC17Srxb6nokeBQHlonlxxkE
服务端收到JWT后,怎么处理呢?
Header和Payload可以直接用Base64解码出原文,可以从Header获取哈希签名算法,从Payload获取有效数据。
Signature使用的是SHA256这种不可逆的算法,无法解码成原码,它的作用是检验token有没有被篡改。
服务端获取header的加密算法后,利用该算法加上密钥secretKey对header、payload进行加密,比较加密后的数据和客户端发来的是否一致。secretKey对于MD5的摘要算法,代表的是盐值。
7.2 在Java中使用JWT
我们给JWT的加密与解密方法封装成工具类
先导入JWT的依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.0</version>
</dependency>
写一个JWT的工具类,比如JWTHelper
然后就可以写数字签名生成token的静态方法了。
我们想把什么参数写进token,就传参进去,这就是JWT加密。
public static String sign(String userName, String dept, Integer userId, String secret, long time) {
Date date = new Date(System.currentTimeMillis() + time);
Algorithm algorithm = Algorithm.HMAC256(secret);
String userIdString = null;
if (userId != null) {
userIdString = userId.toString();
}
return JWT.create().withClaim("userName", userName)
.withClaim("dept", dept)
.withClaim("userId", userIdString)
.withExpiresAt(date).sign(algorithm);
}
比如这里,我们就把用户名、部门、用户id的信息,外加时间戳和数字签名通过JWT生成token。
下面是JWT解密。
我们创建一个UserInfoDTO类接收拿到的个人信息。
@Data
public class UserInfoDTO {
private String userName;
private String deptName;
private Integer userId;
}
public static UserInfoDTO getInfo(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setUserName(jwt.getClaim("userName").asString());
userInfoDTO.setDeptName(jwt.getClaim("deptName").asString());
String userId = jwt.getClaim("userId").asString();
if (StringUtils.isNotBlank(userId)) {
userInfoDTO.setUserId(Integer.valueOf(userId));
}
return userInfoDTO;
} catch (JWTDecodeException e) {
return null;
}
}
当然,我们可以检验一下数字签名,看看token是否被人篡改了。
public static DecodedJWT decode(String token){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(CommonContext.TOKEN_SECRET)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT;
}
我们写一个测试方法
@Test
public void test02() {
// 加密
String token = JWTHelper.sign("张三", "ikun后援会", 3, CommonContext.TOKEN_SECRET, 30000);
System.out.println(token);
// 解密
UserInfoDTO userInfoDTO = JWTHelper.getInfo(token);
System.out.println(userInfoDTO.toString());
// 校验
DecodedJWT decode = JWTHelper.decode(token);
System.out.println(token.equals(decode.getToken()));
}
得到的结果是
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXB0TmFtZSI6ImlrdW7lkI7mj7TkvJoiLCJ1c2VyTmFtZSI6IuW8oOS4iSIsImV4cCI6MTY2NTY1MDY2NCwidXNlcklkIjoiMyJ9.ORNDmXHKhF7qQZaLuwlfVQ_kwgE5sivWd3d-5tibmYo
UserInfoDTO(userName=张三, deptName=ikun后援会, userId=3)
true
所以我们在之后,可以先判断一下token有无被篡改,如果无,再返回具体信息。
7.3 实际开发中使用JWT流程
在实际开发中,可以使用如下流程做登录:
-
在登录验证通过后,使用UUID算法生成一个随机token,并将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间。
-
将第1步中生成的随机token作为JWT的payload生成JWT字符串,并使用一个密钥进行签名,确保JWT的安全性。最后,将JWT字符串返回给前端。
-
前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串。
-
后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录。如果解析出的随机token不存在于Redis中,则返回401 Unauthorized状态码并提示用户未登录。
如何保证JWT的安全性?
因为JWT是在请求头中传递的,所以为了避免网络劫持,推荐使用HTTPS来传输,更加安全。
JWT的哈希签名的密钥是存放在服务端的,所以只要服务器不被攻破,理论上JWT是安全的。因此要保证服务器的安全。
为了防止暴力穷举攻击,建议定期更换服务端的哈希签名密钥(相当于盐值),周期建议设置为3-6个月,并在实施前通知所有参与方。