黑马点评
短信登录
基于Session实现
-
发送短信验证码功能
-
校验手机号
-
正则表达式工具类
其它诸如邮箱、验证码、密码等等都有工具类可以使用。
-
-
生成验证码
-
RandomUtil工具类
-
-
保存验证码到Session
-
发送验证码(log模拟)
-
返回ok
-
-
短信验证登录注册功能
- 登录
- 前端请求发送的json格式的参数,后端要用RequestBody注解接收
- 校验手机号
- 校验验证码:从Session中取出存过的验证码,判断是否一致。
- 根据手机号码查用户:使用Mybatis- Plus进行查询。
- 存在,则保存用户信息到Session
- 不存在,则重新创建并且保存到数据库(Mybatis-Plus)和Session
- 登录
-
登录校验功能(可改进的点:使用token进行保存用户信息,使用JWT)
- 登录校验拦截器(SpringMVC中拦截器的使用和配置)
- 保存用户到TreadLocal中
- 清理用户信息,防止泄露
问题分析
集群的Session共享问题:多台tomcat并不共享session存储空间,请求切换到tomcat服务时导致数据丢失。
解决方案:copy多台服务器之间的数据,但存在问题,浪费空间、有延迟。
因此,符合session的替代方案就有如下的特点:
- 数据能够共享
- 内存存储
- key、value结构
基于Redis实现
发送短信验证码功能
-
校验手机号
-
生成验证码
-
保存验证码到Redis(选择什么类型来保存Value,Key应该使用什么)
-
由于验证码就是个几位数的数字,因此就用简单的String类型来保存验证码
-
Key在基于Session实现中,就是用的code来作为Key,这是因为每个请求的Session都是独立的互不干扰的,但是Redis是共享的,所以每个Key要单独标识,而刚好手机号码是唯一标识的,就用唯一手机号作为Key,存储到Redis。
-
用手机号还有一个好处就是,浏览器端也需要提交手机号,刚好就携带着这个唯一标识去redis找对应的数据了,因为服务器不会像session一样为用户维护一个ID,因此不使用浏览器端携带的参数作为标识的话就会比较麻烦。
-
同时,这里使用了常量来对redis中的KEY加了前缀,区分每个Key对应的功能。
-
-
发送验证码(log模拟,实际上可以调用腾讯、阿里的短信服务)
-
返回ok
短信验证登录注册功能
-
登录
-
前端请求发送的json格式的参数,后端要用RequestBody注解接收
-
校验手机号
-
校验验证码:根据手机号从redis中取出存过的验证码,判断是否一致。
-
根据手机号码查用户:使用Mybatis- Plus提供的query()进行查询。
-
存在,则保存用户信息到Redis(那就又设计到了使用什么类型去保存Value?那用什么作为Key吗)
- 对象类型一般使用哈希保存,更节省内存。
- 会使用一个随机生成的UUID(token令牌,也就是随机字符串)去做为Key,为什么不推荐使用手机号码了呢,这是因为这里使用的Key,相当于是一个用户凭证,需要返回给前端,以便于前端去携带进行访问服务器,而直接用手机号这种敏感信息,可能会出现安全问题。
- 前端将服务器端返回的token保存在浏览器端的一个区域,并通过实现一个拦截器,实现了每次请求都会携带这个token的功能。
-
不存在,则重新创建并且保存到数据库(Mybatis-Plus)和Redis
-
将token返回给浏览器。
-
登录校验
- 根据token从Redis中获取用户,判断用户是否存在,之后就和基于Session是一个类型了。
另外,这个登录状态有一点不合理的地方,就是你登录了之后,只要你一直在访问,在使用,按道理来说,不管你访问的区域是否需要用户信息,都应该保持登录状态才对,而这里的拦截器只拦截了开发者自定义的需要保持状态的请求,才会刷新token有效期,就会导致,你在访问首页等一些不登录也可以使用的区域的时候,有效期一到登录状态就过期了,虽然不影响什么,逻辑上不合理,需要改进。
改进:在访问具体的Controller之前,设置两个拦截器,一个用来刷新token有效期,针对所有请求,另一个用于针对需要使用到用户信息的请求,有登陆才放行,没登录就拦截。
登录模块主流实现方式
想一想上述使用redis保存这个用户信息验证码等等会存在什么问题呢?
- 单点故障,redis宕机的话,所有的用户就需要重新登录。
- 可以通过redis集群解决上述问题,但是为了存储一个登录状态就用集群redis,精力太大,并且用户信息很占redis内存。
首先,可以考虑基于token的JWT登录认证,也可以考虑使用SSO单点登录,下面详细介绍一下这两种实现方式:
基于Token的JWT认证
很多对外开放的API需要识别请求者的身份,并据此判断所请求的资源是否可以返回给请求者。token就是一种用于身份验证的机制,基于这种机制,应用不需要在服务端保留用户的认证信息或者会话信息,可实现无状态、分布式的Web应用授权,为应用的扩展提供了便利。
可以看到,上述过程中就不需要将token(用户凭证)存储到redis等数据库中了,验证用户状态也不需要查询数据库,只需要进行公钥验证。
JWT(Json Web Token)
是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT一般可以用作独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,特别适用于分布式站点的登录场景。
构成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
如上面的例子所示,JWT就是一个字符串,由三部分构成:
- Header(头部)
- Payload(数据)
- Signature(签名)
Header
JWT的头部承载两个信息:
- 声明类型,这里是JWT
- 声明加密的算法
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行Base64编码(该编码是可以对称解码的),构成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload
就是存放有效信息的地方。定义细节如下:
iss:令牌颁发者。表示该令牌由谁创建,该声明是一个字符串
sub: Subject Identifier,iss提供的终端用户的标识,在iss范围内唯一,最长为255个ASCII个字符,区分大小写
aud:Audience(s),令牌的受众,分大小写的字符串数组
exp:Expiration time,令牌的过期时间戳。超过此时间的token会作废, 该声明是一个整数,是1970年1月1日以来的秒数
iat: 令牌的颁发时间,该声明是一个整数,是1970年1月1日以来的秒数
jti: 令牌的唯一标识,该声明的值在令牌颁发者创建的每一个令牌中都是唯一的,为了防止冲突,它通常是一个密码学随机值。这个值相当于向结构化令牌中加入了一个攻击者无法获得的随机熵组件,有利于防止令牌猜测攻击和重放攻击。
也可以新增用户系统需要使用的自定义字段,比如下面的例子添加了name
用户昵称:
{
"sub": "1234567890",
"name": "John Doe"
}
然后将其进行Base64编码,得到Jwt的第二部分:
JTdCJTBBJTIwJTIwJTIyc3ViJTIyJTNBJTIwJTIyMTIzNDU2Nzg5MCUyMiUyQyUwQSUyMCUyMCUyMm5hbWUlMjIlM0ElMjAlMjJKb2huJTIwRG9lJTIyJTBBJTdE
Signature
这个部分需要Base64编码后的Header和Base64编码后的Payload使用 .
连接组成的字符串,然后通过Header中声明的加密方式进行加密($secret
表示用户的私钥),然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');
API网关会认为用户颁发的token有权利访问整个分组下的所有绑定JWT插件的API。如果需要更细力度的权限管理,还需要后端服务自己解开token进行权限认证。API网关会验证token中的exp字段,一旦这个字段过期了,API网关会认为这个token无效而将请求直接打回。过期时间这个值必须设置,并且过期时间一定要小于7天。
特点
- JWT 默认是不加密,不能将秘密数据写入 JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用HTTPS 协议传输。
SSO单点登录
单点登录指的是,同一个用户只需要认证一次,就可以在相关但是不同的一些系统之间传递登录状态信息,不需要重复登录。
通俗点来说,比如阿里家的产品,你在客户端登陆了淘宝app,然后你跳转至天猫等阿里家其它app时,你就会发现,不用再登陆了,已经存在登录状态了。
详细参考:https://developer.aliyun.com/article/636281
另外可以学习一下Spring Security,作为一个认证和授权的安全框架,被广泛使用,也可以了解一下Sa-Token这个出现较晚的权限认证框架。
官网文档:https://docs.spring.io/spring-security/reference/index.html