【全网最细致】SpringBoot整合Spring Security + JWT实现用户认证

【全网最细致】SpringBoot整合Spring Security + JWT实现用户认证

登录和用户认证是一个网站最基本的功能,在这篇博客里,将介绍如何用SpringBoot整合Spring Security + JWT实现登录及用户认证

文章目录

前置知识:Session、Cookie与Token

session与cookie

在一些传统项目中,我们或许会用session来保存用户信息,进行用户认证。而现在基本上都用token来代替session,为什么会出现这样的变化,我们来聊聊session、cookie以及token
在谈session和cookie前,首先我们来谈谈会话。http本身是无状态协议,服务器无法识别每一次HTTP请求的出处(不知道来自于哪个终端),它只会接受到一个请求信号,所以就存在一个问题:将用户的响应发送给相应的用户,必须有一种技术来让服务器知道请求来自哪,这就是会话技术。
会话就是客户端和服务器之间发生的一系列连续的请求和响应的过程。会话状态指服务器和浏览器在会话过程中产生的状态信息,借助于会话状态,服务器能够把属于同一次会话的一系列请求和响应关联起来。
实现会话有两种方式:session和cookie。Session通过在服务器端记录信息确定用户身份,相应的也增加了服务器的存储压力。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是Session。属于同一次会话的请求都有一个相同的标识符,sessionID,客户端浏览器再次访问时只需要通过sessionID从Session中查找该客户的状态就可以了。那么后端是怎么把sessionID返回给客户端的?可以通过设置cookie的方式返回给客户端,若浏览器禁止cookie,则可以通过URL重写的方式发送。
刚刚提到了cookie,Cookie是服务端在HTTP响应中附带传给浏览器的一个小的文本文件,一旦浏览器保存了某个Cookie,在之后的请求和响应过程中,会将此Cookie来回传递,这样就可以通过Cookie这个载体完成客户端和服务端的数据交互。
使用Session进行用户认证时,当用户第一次通过浏览器使用用户名和密码访问服务器时,服务器会验证用户数据,验证成功后在服务器端写入session数据,向客户端浏览器返回sessionid,浏览器将sessionid保存在cookie中,当用户再次访问服务器时,会携带sessionid,服务器会拿着sessionid从服务器获取session数据,然后进行用户信息查询,查询到,就会将查询到的用户信息返回,从而实现状态保持,流程如下图:
在这里插入图片描述

session的弊端
  • 服务器压力增大
    通常session是存储在内存中的,每个用户通过认证之后都会将session数据保存在服务器的内存中,而当用户量增大时,服务器的压力增大。
  • CSRF跨站伪造请求攻击
    一般session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。即使不用cookie,用重写url方式发送sessionId,那就更容易被截获信息了
  • 扩展性不强
    想象这么一个场景,若项目在多个服务器上部署,那我再其中一台登录了,称为A,session也保存到A中,万一下次我访问到另外一台服务器B怎么办?B上没有A的session呢?为了解决这个问题,我们需要将session保存到数据库中,所以每次保存这些session信息就是一个负担了,增加了服务器的存储压力。
token

token的意思是“令牌”,是服务端生成的一串加密字符串(服务器端并不进行保存),作为客户端进行请求的一个标识。当用户第一次登录后,服务器生成一个token并将此token返回给客户端浏览器,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
浏览器会将接收到的token值存储在Local Storage中,浏览器再次访问时服务器端时,服务器对浏览器传来的token值进行解密,解密完成后进行用户数据的查询,如果查询成功,则通过认证,实现状态保持,所以,即使有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,同时服务器也不需要保存token,token的出现就解决了session的弊端,成为了session的替代品。
使用token进行用户认证的流程如下图:
在这里插入图片描述

session与token的总结

token 是无状态的,后端不需要记录信息,每次请求过来进行解密就能得到对应信息。
session 是有状态的,需要后端每次去检索id的有效性。不同的session都需要进行保存,但也可以设置单点登录,减少保存的数据。
session与token的选择是空间与时间博弈,为什么这么说呢,是因为token不需要保存,不占存储空间,但每次访问都需要进行解密,消耗了一定的时间。
在一般的前后端分离项目中,token展现出了它的优势,成为了比session更好的选择

JWT

JWT其实就是一种被广泛使用的token,它的全称是JSON Web Token,它通过数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全地传输信息。
JWT最常见的使用场景就是授权认证,一旦用户登录,后续每个请求都将包含JWT,系统在每次处理用户请求之前,都要先进行JWT安全校验,通过之后再进行处理。
JWT由3部分组成,用.拼接,如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
这三部分分别是:

  • Header
    Header中保存了令牌类型type和所使用的的加密算法,例如:

    {
    ‘typ’: ‘JWT’,
    ‘alg’: ‘HS256’
    }

  • Payload
    Payload中包含的是请求体和其它一些数据,例如包含了和用户相关的一些信息

    {
    “sub”: “1234567890”,
    “name”: “John Doe”,
    “admin”: true
    }

  • Signature
    Signature签名属于jwt的第三部分。主要是把头部的base64UrlEncode与负载的base64UrlEncode拼接起来,再进行HMACSHA256加密等最终得到的结果作为签名部分。例如:

    // javascript
    var encodedString = base64UrlEncode(header) + ‘.’ + base64UrlEncode(payload);

    var signature = HMACSHA256(encodedString, ‘secret’); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

登录及用户认证流程设计

假设我们要设计这样一个登录功能:用户输入用户名、密码以及图片验证码进行登录,用户认证功能则是对于用户的每次请求,都需要校验用户信息,若不正确,则拒绝请求
这里不同于最简单的用户名密码登录,加入了图片验证码,验证码是为了防止非正常用户伪造请求进行登录,是一个较为重要的功能。
根据我们之前对于token和JWT的介绍,我们知道,对于首次登录,浏览器是没有JWT信息的,用户需输入用户名、密码和验证码完成登录,后端对验证码、用户名、密码进行校验后,若校验成功,则返回JWT给前端,完成登录。登录后的每次请求,请求头都将携带Jwt进行身份认证,若认证成功则能访问后端接口
我们先来看首次登录,后端需要对验证码和用户信息进行校验,我们不妨先校验验证码,再校验用户名密码。
对于验证码的校验,我们知道一般的前后端分离使用token,不使用session,那么后台必须保证当前用户输入的验证码是用户开始请求页面时候的验证码,必须保证验证码的唯一性。举个例子:
A用户看到的验证码是:ABC;B用户看到的验证码是:DEF。后台存储了ABC和DEF这2个验证码,如果不限定A用户输入的验证码是ABC,那么当A用户碰巧输入DEF,然后用户名和密码也是正确的话,A用户也是可以登录系统的。
也就是说,每个用户的请求都需要对应一个唯一的验证码,这一切的麻烦都源于http本身是无状态协议,我们需要保存用户与验证码之间的对应关系,而现在我们又不能使用session,不能用SessionID的字段和验证码对应,那么我们该如何做呢?
有一种方式是前端生成一个随机数(UUID形式),保存在localstorage里,对应着某个用户,前端带上随机数参数访问后端接口,后端用加密算法加密该随机数rand,生成验证码,即verify_code = f(rand)。当用户提交验证码的时候,之前的随机数一起带过来,后端再通过之前的加密规则验证输入的验证码是否正确。也即构造了随机数和验证码的对应关系
其实上述做法有点类似于token的做法,不过这种做法有几个问题,一是验证码强行和一个前端给的一串随机数通过一个算法f产生了联系,前端的请求可以随意伪造,随机数参数也可以五花八门,可能会导致一些意想不到的bug发生,验证码应保持随机性和独立性,不应该和一个随机数强行通过函数f关联。二是采用这种方式,后端需要进行两次加密过程生成验证码,会造成不必要的时间开销。
我们采用另一种方式类似于session的做法来完成验证码校验过程,首先,我们还是得构造随机数(UUID形式,代表着某个用户),它和验证码一一对应。不过这次我们将随机数的生成把握在后端手里,毕竟老话说得好:作为一个后端,不要相信前端传过来的任何参数(手动狗头),把随机数的生成把握在后端手中,这种方式更加安全。我们仿照session的原理,牺牲一部分存储空间,将随机数和对应的验证码作为key-value键值对形式进行存储,然后将生成的随机数返回给前端,前端在登录请求时将该随机数以及用户输入的验证码传给后端,后端就能通过该随机数进行查询,校验输入的验证码和正确验证码是否一致。我们可以引入redis中间件来完成随机数和验证码的存储,因为一个验证码对应一个用户的一次登录过程,所以当验证成功时,我们将redis中存储的验证码和随机码删除,采用这种方式也不会消耗多少存储空间。

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值