为什么使用Token验证:
在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,tokens 是多用户下处理认证的最佳方式。
以下几点特性会让你在程序中使用基于Token的身份验证:
- 1.无状态、可扩展
- 2.支持移动设备
- 3.跨程序调用
- 4.加密,安全
(A)Token的起源
在介绍基于Token的身份验证的原理与优势之前,不妨先看看之前的认证都是怎么做的。
基于服务器的验证
HTTP协议是无状态的,这种无状态意味着程序需要验证每一次请求,从而辨别客户端的身份,在这之前,程序都是通过在服务端存储的登录信息来辨别请求的。这种方式一般都是通过存储Session来完成。随着Web,应用程序,移动端的兴起,基于服务器验证这种验证的方式逐渐暴露出了问题。尤其是在可扩展性方面。 - 1.Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
- 2.可扩展性:在服务端的内存中使用Seesion存储登录信息,无法水平扩展
- 3.CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源进行跨域请求时,就可以会出现禁止请求的情况。
- 4.CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
在这些问题中,可扩展性是最突出的。因此我们有必要去寻求一种更有行之有效的方法。
基于Token的验证原理
基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。这种概念解决了在服务端存储信息时的许多问题NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录。 基于Token的身份验证的过程如下: - 1.用户通过用户名和密码发送请求。
- 2.程序验证。
- 3.程序返回一个签名的token 给客户端。
- 4.客户端储存token,并且每次用于每次发送请求。
- 5.服务端验证token并返回数据。
Tokens的优势
(1)无状态、可扩展
在客户端存储的Token是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载均衡器能够将用户信息从一个服务器传到其他服务器上,因为tokens里负载了用户的验证信息。相反,如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性),用户量大时,可能会造成拥堵,消耗内存。
Token能够创建与其它程序共享权限的程序。使用token时,可以提供可选的权限给第三方应用程序。当用户想让另一个应用程序访问它们的数据,我们可以通过建立自己的API,得出特殊权限的token
(2)安全性
请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。token是有时效的,可以设置有效期,一段时间之后用户需要重新验证。也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效
(3)多平台跨域
先来谈论一下CORS(跨域资源共享,详见上篇文章juejin.cn/post/691605…
(B)Session
session就是会话,这是服务端的一种操作。当你第一次访问一个web网站的时候,服务端会生成一个session,并有一个sessionid和他对应。这个session是存储到内存中的,你可以向这个session中写入信息,比如当前登录用户的信息。sessionid会被返回到客户端,客户端一般采用cookie来保存,当然这个cookie不用人为写入,通常缓存在浏览器中
当后端调用HttpServletRequest对象的getSession的方法的时候,tomcat内部会生成一个jsessonid(tomcat sessionid的叫法)。这个jsessonid会随本次请求返回给客户端。
响应头信息:
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=xxxxxxxxxxxxxxxxxxx
这个jessionid就会写到cookie中。之后jessionid就会通过cookie传递到服务端。
这里我们就会很清楚了,session的数据是存储到内存中,那问题就来了,如果我们的服务是分布式部署,有多台机器的话,可能我们第一次登陆的时候,我们把用户的信息存储到了session,但是后面的请求打到了B机器上,那B机器是获取不到用户的session的。另外就是session存储在内存中,那服务器重启,session就丢失了,这就是它的弊端。现在有一些技术,例如session共享、iphash、session持久等也可以解决上述问题
(C)Cookie
cookie是浏览器的一种策略。上述讲到了sessionid就是存储在cookie中的。我们知道http协议是无状态的,cookie就是用来解决这个问题的。cookie中可以用来保存服务端返回的一些用户信息的,例如前文提到的token、sessionid。每一次的请求,都会携带这些cookie。服务端从请求头中取到cookie中的信息,就可以识别本次请求的来源,这样,http是不是就变成有状态的了。
说几点cookie注意事项:
- 1.cookie存放在客户端,所以是不安全的,人为可以清除。
- 2.cookie有过期时间设定。如果不设置过期时间,说明这个cookie就是当前浏览器的会话时间,浏览器关了,cookie就不存在了。如果有过期时间,cookie就会存储到硬盘上,浏览器关闭不影响cookie。下次打开浏览器,cookie还存在.
- 3.cookie有大小的限制,4KB。
最后,对于一个分布式的web系统,通用的解决方案就是cookie+token。由服务端生成token,将用户信息与token进行关联,token返回给浏览器,存储到cookie(
cookie无法直接跨域,可以存在header中)中。后续请求都携带cooke或者将token从cookie取出以参数传递。
(D)总结
-
1.对于会话来说,JWT 真的算 Token 么?虽然名字中带 Token,但是对于会话来说,我觉得 JWT 其实更像是一种“客户端会话”。客户端会话虽然有一些吸引力,但是带来的问题好像比服务端会话更多,怎么还有好多人推荐使用,是我学艺不精,还是他们没搞清楚?
-
2.服务端会话已经很成熟易用了,在前端也只需要保存一个会话 ID 而已,在传统的开发模式中这个会话 ID 基本上都是保存在 Cookie 中的。如果在前后端分离这种开发模式下,真的用不了 Cookie (比如说部分客户端不支持 Cookie 或者跨域了),那直接基于 HTTP 头部实现一种替代的会话 ID 传输方式,然后再找一个地方存储这个会话 ID,不就可以了么?我看好像也有些人这么做并且管这个叫 Token 。
-
3.见到一些人说“先输入用户名和密码进行登陆,成功后返回一个 Token,然后后续请求都带上这个 Token,定期更换即可”。从功能上讲,这种 Token 感觉十分类似我们做“保持登陆 7 天”时的那种自动登陆 Token,是么?那这种 Token 和会话好像已经没关系了啊! 前后端分离中不使用 Cookie 的情况下,Session 和 Token 到底该怎么用?
-
(1)多个业务的情况下 Session 是单个业务维护登陆用户信息的 而 token 是多个系统共用的登陆鉴权的
-
(2)jwt 是去中心化,自包含的
-
(3)这个东西主要是分布式或者负载均衡里面用,当你同一个用户的两个请求打到不同服务实例上的时候,这两个实例都需要有能力验证它的合法性。session 的话你就得保证两个请求都进到同一个服务器里才行。或者你确定你的业务规模用不着做这些东西,那直接用 session 最方便。
-
(4)JWT 最大的问题是一旦发出去了,在它的失效时间到来之前你永远无法撤销它的认证,要是存 redis 里过滤那就太好笑了。它最大的好处就是"无状态",如果要考虑到复杂的认证 /鉴权系统,JWT 是很有优势的,因为 JWT 的灵活性远远高于 session 。如果只是简单的客户端认证,其实和前后端是否分离关系不大,就直接用自带的 session 认证系统基本就能 hold 住。否则还得设计 token 分发,刷新,过期处理等等麻烦的事情。不过,可以用 jwt 来存 session id,这样不就完美解决这个问题了!!!
-
(5)JWT 最大的特点是存在客户端,而不是服务器端,所以“无状态”,但也正因为没有存在服务端,所以服务端没办法主动让其失效,只能等其过期。无法被服务端主动撤销,以及潜在的安全问题,就注定无法替代 session ,感觉 jwt 存储 session id,服务端使用分布式 session 才是比较好的解决方案 (E)token主动失效解决方案
由于项目中涉及到token主动失效的需求,因此记录相关使用的解决方案。由于该项目部分模块是涉及到内部商城系统,只对集团内部员工开放福利,因此用户鉴权要求较高。具体需求为:在token泄露(如Cookies被劫持)以及密码重置的场景下,商城系统需要将未过期的但是已经不安全的token主动失效。
一般而言,token 存在 Header 或者 Cookies中 ,由于HTTP 是明文传输,HTPPS 是密文传输,因此可以将HTPPS代替HTTP传输,这样可以一定程度防止token被截获。由于工作中商城系统项目涉及到的用户不仅包括大众用户,还包括内部用户,而商城中部分折扣、满减等福利只对内部用户开放,为防止内部用户token被截取,项目中采用了常用的应对方式:用户登录时,Server端将生成的token与用户登录IP(反向代理时,Server端也能从相应的消息头获取到Client端的IP)进行绑定,并将生成的token返回给Client端,最后当用户再次登录时,即使token在中途被截取,Server端通过检验token中的IP,就能发现token已泄露。同时为了增加截获成本,可以在Server端生成token时,对其进行RSA非对称加密方式进行加密或者AES对称加密。另外,对于部分敏感操作,为了安全起见,会利用短信验证码进行二次校验。
如果Server发给Client的token还没有过期,但是变得不安全,如token泄露或者想踢掉恶意用户,Server端需要及时地主动将还在有效期但是不安全的token剔除,使其失效。Server端对token的控制比Session弱,Session可以用Server端掌控,但是token一旦签发,就脱离了Server的掌控,成了脱缰的野马。通常而言,Server端只能通过声明token的过期时间expire,使其在过期后被动失效。因此,就像上文所说,token最大的优点也是其最大的弱点。有人可能会说,为什么不把Server将签发的token都存一份到分布式缓存中,这样token就能被跟踪和标识。这种方式可以解决问题,但每份token都要保存起来,并且每次校验都要做查询。这种方式其实和分布式session并没有多大差别。显然,这种方式放弃了token占用空间小、有效性校验速度快的优点。在没有跨域资源访问的需求场景下,分布式Session的方案反而更有优势,至少可以在Session中保存敏感信息。Session是存在Server端,而不用担心在Client端被泄露。
项目中采用的方案(不一定最优): -
(1) 为每个token设置不太长的过期时间,token过期时间设置太久会不安全
-
(2) 在用户表user中,添加一个“token_black_white_id”字段用于保存一个随机String
user_name | password | ... | token_black_white_id -
(3) 在token(项目中使用JWT)的payload中负载"tokenBlackWhiteId"属性。签发token时,将用户的"token_black_white_id"字段对应的字符串写入。
{
"exp": 失效时间,
"tokenBlackWhiteId": "对应的字段值随机字符串"
}
-
(4) 将分布式缓存服务器(Redis)作为黑名单容器。若用户token泄露或者重置密码等需要剔除已经签发的token时,为该用户生成新的"token_black_white_id"存入用户登陆表并将"token_black_white_id"旧值加入黑名单即分布式缓存中。通过保存Key为"tokenBlack + userId(user表的主键user_id)",Value为上述tokenBlackWhiteId属性值的K-V结构对泄露的token进行拉黑。
-
(5) "token_black_white_id"在黑名单容器(Redis)中的有效时间是token设置的过期时间,过期后自动从黑名单中删除。
-
(6) 做token有效性校验的Server在启动时从分布式缓存下载黑名单到本地内存(通过redisTemplate.keys(prefix),其中prefix="tokenBlack*", * 号用于匹配所有黑名单中的userId)。并且订阅分布式缓存的消息推送功能以监听黑名单的增删动作,同步修改本地内存中的黑名单。
-
(7) Server端做token校验时,不仅仅校验过期时间,也要查询内存中的黑名单列表。若token的"tokenBlackWhiteId"在黑名单中,则该token失效。 【注】(a)其实,也可以直接使用分布式Session,但是分布式Session上面也分析过,每次都会查询用户token的整个集合,效率不及上述方案; (b)另外,采用专门的字段token_black_white_id是为了进一步提升查询速度,因为通常而言,token串比较长,占用存储的同时还可能会影响查询速度,不直接存储在Redis目的是防止恶意请求token导致黑名单膨胀,采用字段token_black_white_id,可以保证永远只有一个token。
(F)分布式session -
1)session复制 session复制是小型企业应用使用较多的一种服务器集群session管理机制,在真正的开发使用的并不是很多,通过对web服务器(例如Tomcat)进行搭建集群。
缺点:
session同步的原理是在同一个局域网里面通过发送广播来异步同步session的,一旦服务器多了,并发上来了,session需要同步的数据量就大了,需要将其他服务器上的session全部同步到本服务器上,会带来一定的网路开销,在用户量特别大的时候,会出现内存不足的情况
优点:
服务器之间的session信息都是同步的,任何一台服务器宕机的时候不会影响另外服务器中session的状态,配置相对简单
Tomcat内部已经支持分布式架构开发管理机制,可以对tomcat修改配置来支持session复制,在集群中的几台服务器之间同步session对象,使每台服务器上都保存了所有用户的session信息,这样任何一台本机宕机都不会导致session数据的丢失,而服务器使用session时,也只需要在本机获取即可
如何配置:
在Tomcat安装目录下的config目录中的server.xml文件中,将注释打开,tomcat必须在同一个网关内,要不然收不到广播,同步不了session 在web.xml中开启session复制:<distributable/> -
2)session黏性(绑定) 表示从同一窗口发来的请求都将有集群中的同一个tomcat进行处理 worker.lbcontroller.sticky_session=True 粘性session的好处在不会在不同的tomcat上来回跳动处理请求,但是坏处是如果处理该session的tomcat崩溃,那么之后的请求将由其他tomcat处理,原有session失效而重新新建一个新的session,这样如果继续从session取值,会抛出nullpointer的访问异常。
Nginx:
Nginx是一款自由的、开源的、高性能的http服务器和反向代理服务器;反向代理、负载均衡、http服务器(动静代理)、正向代理
如何使用nginx进行session绑定?
利用nginx的反向代理和负载均衡,之前是客户端会被分配到其中一台服务器进行处理,具体分配到哪台服务器进行处理还得看服务器的负载均衡算法(轮询、随机、ip-hash、权重等),但是我们可以基于nginx的ip-hash策略,可以对客户端和服务器进行绑定,同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理
在nginx安装目录下的conf目录中的nginx.conf文件
upstream www.sasuke.cn {//upstream的负载均衡
// Ip_hash;可以用hash
server 39.125.59.4:8080 weight=2;
/*也可以用权重,weight是权重,可以根据机器配置定义权重。weigth参数
表示权值,权值越高被分配到的几率越大。*/
Server 39.135.59.4:8081 weight=3;
}
server {
listen 80;
server_name www.sasuke.cn;
#root /usr/local/nginx/html;
#index index.html index.htm;
location / {
proxy_pass http:39.105.59.4;
index index.html index.htm;
}
}
缺点:
容易造成单点故障,如果有一台服务器宕机,那么该台服务器上的session信息将会丢失
前端不能有负载均衡,如果有,session绑定将会出问题
优点:
配置简单
- 3)session集中式管理
优点:redis为内存数据库,读写效率高,并可在集群环境下做高可用.是常用的方式
加入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- session redis 共享 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
yml文件:
server:
port: 8080
spring:
application:
name: manage-session
redis:
password: *****
database: 0
host: 127.0.0.1
port: 6379
@EnableRedisHttpSession注解启动类
@SpringBootApplication
@EnableRedisHttpSession
/* 可以通过maxInactiveIntervalInSeconds设置Session过期时间 */
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
通过redis集中式管理session这种方式在使用上面对客户端是透明的,无需自己操作redis,在使用HttpSession对象的时候直接使用即可
@RestController
public class IndexController {
@GetMapping("/item")
public ResponseEntity<String> index(HttpSession httpSession) {
httpSession.setAttribute("user", "Sasuke");
return ResponseEntity.ok("ok");
}
@GetMapping("/page")
public ResponseEntity<String> hello(HttpSession httpSession) {
return ResponseEntity.ok(httpSession.getAttribute("user"));
}
}
优点:
这是企业中使用的最多的一种方式
spring为我们封装好了spring-session,直接引入依赖即可
数据保存在redis中,无缝接入,不存在任何安全隐患
redis自身可做集群,搭建主从,同时方便管理
缺点:
多了一次网络调用,web容器需要向redis访问
总结:
一般会将web容器所在的服务器和redis所在的服务器放在同一个机房,减少网络开销,走内网进行连接
-
4)基于cookie管理 缺点:
数据存储在客户端,存在安全隐患; cookie存储大小、类型存在限制; 数据存储在cookie中,如果一次请求cookie过大,会给网络增加更大的开销 优点:
数据存储在cookie中,某种程度上会减轻Server端的压力
参考博文:
fairysoftware.com/token_sessi…
www.v2ex.com/amp/t/70331…