不出意外的话,我猜在座的各位同学,刚刚学编程的时候,80%以上的人写的第一个项目不是学生管理系统,就是用户的登录、注册;需求都是源于生活而高于生活,因为学生对这些场景最为熟悉,功能也相对简单,拿来练手最适合不过了;但是,一个看似小小的登录、注册功能,要想把他真正的做好,并不是一件容易的事情;本文通过图示及代码的方式介绍用户登录流程及技术实现,内容包括用户登录,用户验证,如何获取操作用户的信息以及一些黑名单及匿名接口如何免验证相关的实现。
业务图解
对于用户登录来说、涉及到了用户注册、登录验证几个方面,通过流程图演示如何处理(新用户/老用户)登录。
图片
流程解读
客户端-登录界面(通常手机验证码登录)
- 填写手机号
- 发送验证码
- 填写验证码
- 勾选新用户自动注册
服务端-用户验证
- 验证账号验证码是否正确
- 验证用户是否存在(不存在出初始化用户信息)
- 完成验证生成 token
- 将 token 返回给客户端
用户信息设计:
图片
验证流程图解
图片
登录验证流程涉及到了两个接口,两个缓存:
- 获取验证码接口,给手机号发送验证码并设置验证码缓存,设置过期时间。
- 登录接口,提交手机号及验证码,读取缓存进行匹配验证,成功则生成 token 返回给客户端,客户端登录成功,登录后请求头携带 token 进行业务请求即可。
关于 token 过期时间
通常我们 token 的过期时间是根据客户端的类型来定义的,app 的过期时间会更长一些(通常一个星期)。
web 端过期时间以小时为单位,如果控制过期时间可以将 web 登录和 app 登录拆分为两个接口(能够分流,接口压力更小),或者是根据请求头信息进行判断即可,是移动端就设置 7 天,是 web 端就设置两小时。
关于业务请求 token 验证
登录成功后,客户端每次请求都会携带 token,通常我们会有一个网关来进行 token 验证,网关用于登录验证的核心就是登录成功后写入的 token 作为 key,值为用户基础信息的缓存。
图解如下:
图片
验证成功后,重写内部请求头,将用户的的 id,账号,昵称信息放入请求头中,这样可以方便业务系统获取当前操作用户信息以及权限控制等等。
关于登出操作
用户携带 token 请求登出接口,登出接口对 token 对应的缓存进行删除操作,返回 401 即可,客户端获取到 401 就会跳转到登录页面。
关于匿名请求(免登录)
通常匿名请求放行有两种方案:
- 授权 token,为 token 设置单位时间内请求次数
- 配置路径放行规则,对请求接口路径进行正则匹配,符合正则规则的进行放行
方案 1:授权 token,限制单位时间请求次数
优点就是虽然是免登录接口,但是接口的操作对象可以追溯,请求次数可控,避免被非法利用;缺点就是需要更多的编码及配置工作。
技术实现:
- 提供一个授权 token 管理页面,主要管理 token 使用者,token 的值,单位时间访问次数(如每分钟 60 次)
- 增删改查,将授权 token 存放到缓存中,使用 map 进行存储,key 为 token,值为每分钟访问次数
- 单位时间计数缓存,过期时间为 1 分钟
这时候我们需要在上面的验证流程图基础上进行升级:
图片
请求次数检查代码实现:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 授权token请求限制缓存
*/
@Component
public class AuthTokenRequestLimitCache {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
private static final String AUTH_TOKEN_LIMIT_KEY_PREFIX = "auth_token_limit";
/**
* 请求次数+1并检查是否超限
*
* @param token
* @return 是否放行
*/
public boolean incrementWithCheck(String token) {
// 1.获取token请求次数限制,获取为null代表授权配置已被修改,此token已经不具备权限
Integer limit = getLimit(token);
if (limit == null) {
return false;
}
// 2.组装缓存key,读取缓存
String key = String.join(":", AUTH_TOKEN_LIMIT_KEY_PREFIX, token);
Integer count = redisTemplate.opsForValue().get(key);
// 3.没有值代表一分钟内没有请求产生了
if (count == null) {
// 初始化值
redisTemplate.opsForValue().increment(key);
// 设置过期时间
redisTemplate.expire(key, 1L, TimeUnit.MINUTES);
return true;
}
// 自增并获取当前值 大于限制的话 返回false 网关过滤器返回提示信息(如请求过于频繁)
Long inc = redisTemplate.opsForValue().increment(key);
return inc <= limit;
}
/**
* 获取限值
*
* @param token
* @return
*/
public Integer getLimit(String token) {
Object limit = redisTemplate.opsForHash().get("auth_token_limit", token);
return limit == null ? null : (Integer) limit;
}
}
对于授权接口,通常是只允许 get 操作,对数据进行提交或者更新是不被允许的,当然这个是业务层面的,最终取决于系统设计。
方案 2:请求路径正则校验
我们在网关的配置文件中增加匿名接口规则,请求到网关时,检查请求的路径是否符合匿名接口规则,是则放行,不是则进行 token 校验,方案比较简单,只需要对网关进行处理即可。
关于黑名单
对于一个系统来说,黑名单是最后一道关卡,所以为了安全我们需要对问题用户进行黑名单操作。
具体实现也比较简单:
- 用户管理页面提供一个拉黑的按钮,拉黑后,这些用户的 id 会存储到一个 set 集合中去
- 登录时候检查用户是否在黑名单中,是则拒绝登录并提示
- 如果用户已经登录后进行拉黑操作,网关会在鉴权通过后检查用户是否在黑名单中,是则删除 token 对应缓存,返回 401,401 就会跳到登录页,步骤 2 就会进行拦截
总结
用户系统是非常基础的系统,但是很多程序员工作中可能并没有真正的参与到用户系统的开发,通过此文可以对用户登录流程及配套功能有一个全面的了解。