登录系统相关知识(JWT、加密算法、cookie、session)

1. cookie和session相关

(1)原生的session系统
//例子:
@Controller
public class CookieController {

    @RequestMapping("/test/cookie")
    public String cookie(@RequestParam("browser") String browser, 
                HttpServletRequest request, HttpSession session) {
        //取出session中的browser
        Object sessionBrowser = session.getAttribute("browser");
        if (sessionBrowser == null) {
            System.out.println("不存在session,设置browser=" + browser);
            session.setAttribute("browser", browser);
        } else {
            System.out.println("存在session,browser=" + sessionBrowser.toString());
        }
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                System.out.println(cookie.getName() + " : " + cookie.getValue());
            }
        }
        return "index";
    }
}

原生的session这里,就是session你直接用,然后Jssionid自动给你放到cookie里面,然后自动给你发送你就直接用session就行。

(2) Spring Session

导个包,配个Redis,再开启RedisSession,然后就把session存到了Redis里面,这一系列都对用户透明,你还是直接用session就行了,cookie里面照样存sessionid,照样自动发送。
这两个只是在于session存的位置不一样,对于你来说都是在Controller的括号里面写一个HttpSession然后就可以正常使用了。


2. 为什么使用JWT

(1)前后端分离

以前的传统模式下,后台对应的客户端就是浏览器,就可以使用session+cookies的方式实现登录,但是在前后分离的情况下,后端只负责通过暴露的RestApi提供数据,而页面的渲染、路由都由前端完成。因为rest是无状态的,因此也就不会有session记录到服务器端。

(2)传统方式带来的安全性问题

在前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

(3)性能问题

如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态,一般还需借助nosql和缓存机制来实现session的存储,如果是分布式应用还需session共享。

(4)兼容问题

在移动端app里,或者是前后端分离的架构中,用户访问的是前端的web server(如 node.js),前端的渲染,ajax请求都是由web server完成的,这里就跟传统的不一样了,用户不是直接访问后台应用服务器的,这时候用cookie+session就比较麻烦,问题在于开发繁琐、安全性和客户体验差、有些前端技术不支持cookie(如微信小程序)

(5)带来的好处

简洁,可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快。

自包含,负载中包含了所有用户所需要的信息,避免了多次查询数据库,服务端也不需要存储 session 信息,做到了服务端无状态。

JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。

JWT能轻松的实现单点登录,因为用户的状态已经被传送到了客户端。

支持移动设备,支持跨程序调用,Cookie 是不允许垮域访问的,而 Token 则不存在这个问题。

因为有签名,所以JWT可以防止被篡改


3. JWT相关知识

(在这里写一下JWT的加密原理。
这个东西只能保证防篡改,就是他能用第三个来核实前两个没有被改过,因为第三个签名那里用到了只有服务器才能有的secret。不过如果你把别人的header考进自己的请求里面,应该识别不出来,可以作弊。

以及JWT的用法:你把要传递的东西放在第一段和第二段里,基本上是明文,用Base64纯属为了好传递,第三段的签名没什么卵用,没添加新信息,所以只要截到了JWT一定能获取里面的信息。)

(1) JWT的数据结构:

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下:
Header(头部)
Payload(负载)
Signature(签名)

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

(2)Base64编码

Base64编码以后的,就变成0-9,A-Z,a-z,+/这些字符串组成的一个超大的字符串。

编码过程:
把原来的要编码的字符串按照二进制编码铺成一列01010101,然后每六位分割成一组,每组在前面加上00,这样一组就凑成了八位,然后这八位按照规则变成上面64个字符之一。

解码过程:
既然规则是不变的,那有了编码后的字符串,自然也可以每个字符串变成8位二进制,然后去掉两个0,再拼到一起就解出了原来的二进制。

关于加密算法:
所谓加密算法一定是要用到某种私钥,如果没有私钥就不能解密的那种才算是加密算法。所以Base64不算加密,这只是一种编码。

4. RSA非对称加密

对称加密是一段密文,经过一个秘钥加密之后,传递过去,接收人用同一个秘钥解密。
非对称加密是有公钥和私钥,用私钥加密之后传递,接收人用公钥能解密出来加密内容(这一对公钥和私钥一定是有某种联系的,必须是一对)。
RSA是用的最多的非对称加密。一开始生成一对公钥和私钥,然后一个加密一个解密。

用法:
如果RSA和JWT结合在一起,要加密的就是JWT上面两段拼在一起,组成的一长串字符串,然后这个字符串用getBytes()方法得出字节表示(你就理解成String变成了数字),然后要加密的就是数字了(公钥私钥本来就是数字别搞那么复杂),结合公钥私钥加密成签名,然后可以解密成原来的字符串。

5. 密码加密算法

(1)MD5(循环加密):

MD5使用的是部分信息进行计算的,不管多长的字符串进来算一下,加密过后都是128位的,所以里面肯定有大量的信息丢失,根本不可能反推。
根据这个过程,MD5加密之后的格式数量肯定是有限的,理论上就存在多种输入字符串会对应同一种输出的可能性。

算法过程太复杂了,只能简单写写:
首先做数据填充,把数据填成512的整数倍,然后一组一组加密,前一组的结果当成后一组的输入,循环加密,反推难度极大。

(2)加盐:

现在应该很少系统会直接保存用户的密码了吧,至少也是会计算密码的 md5 后保存。md5 这种不可逆的加密方法理论上已经很安全了,但是随着彩虹表的出现,使得大量长度不够的密码可以直接从彩虹表里反推出来。
所以,只对密码进行 md5 加密是肯定不够的。聪明的程序员想出了个办法,即使用户的密码很短,只要我在他的短密码后面加上一段很长的字符,再计算 md5 ,那反推出原始密码就变得非常困难了。加上的这段长字符,我们称为盐(Salt),通过这种方式加密的结果,我们称为 加盐 Hash 。

加盐这里的扩充:
如果加的盐值是固定的,自己在yml里面写了一个固定的字符串,以随机顺序进行填充,那么如果密码相同的话,那加的盐值也相同,破解一个之后就破解了一堆。
所以要保证每一个密码加的盐值都是不同的,那这个盐值就要单独生成,然后再用Md5加密。
个人感觉现在的使用tmcreate生成盐值和调用Security里面的BCryptPasswordEncoder应该差不多。

(3)彩虹表:

MD5是单向加密不可逆推,主流彩虹表都在100G左右,事先准备了很多的加密映射。
虽然不是完全意义上的根据结果查表直接查出来,不过也差不多了,根据查到的类似的结果,修修改改再导出正确的结果。

6. 微信登录相关(oauth2)

简单点讲的话,oauth2框架也就是两次请求,两次请求分别请求不同的服务器。
一次是用微信的扫一扫访问微信端的认证服务器,这个过程中没涉及到你的第三方服务。然后认证完了之后调用回调函数回到第三方服务,带回来了一个认证的code。
第二步用第三方的key、secret、还有刚才的code,一起去请求资源服务器,调用微信的资料。

你可能感兴趣的:(登录系统相关知识(JWT、加密算法、cookie、session))