一般做外贸上什么网站好,中国建设银行官方网站沈阳,大连网站开发乛薇,抖音优化排名文章目录 主要功能生成图形验证码redis滑动窗口操作限流0.限流设计的必要性1.原理2.代码#xff08;邮箱发验证码为例#xff09;3. 问题与解决高并发环境下redis操作的原子性过时数据的积累 续约token实现长期登录0.设计的出发点1.前置知识:JWT什么是 JWT#xff1f;JWT 的… 文章目录 主要功能生成图形验证码redis滑动窗口操作限流0.限流设计的必要性1.原理2.代码邮箱发验证码为例3. 问题与解决高并发环境下redis操作的原子性过时数据的积累 续约token实现长期登录0.设计的出发点1.前置知识:JWT什么是 JWTJWT 的结构1. Header头部2. Payload负载3. Signature签名 JWT 的工作原理 2.思路正常登录的流程访问受限资源 code 主要功能
生成图形验证码redis滑动窗口操作限流续约token实现长期登录的效果
生成图形验证码 hutoool提供了工具类直接用就行 public Captcha createCaptcha(){// 定义图形验证码的长和宽LineCaptcha lineCaptcha CaptchaUtil.createLineCaptcha(200, 100);//将验证码储存到redis中加上TTLString uuid UUID.randomUUID().toString();stringRedisTemplate.opsForValue().set(LOGIN_CAPTCHA uuid, lineCaptcha.getCode(), LOGIN_CAPTCHA_TTL, TimeUnit.MINUTES);return new Captcha(uuid, lineCaptcha.getImageBase64());}redis滑动窗口操作限流
0.限流设计的必要性
用户可能有很多行为是无意义或者非法的。比如频繁发送短信、频繁修改个人信息、频繁的点赞、评论等等行为这些做法不仅是意义不大的操作而且还会对我们的服务器带来压力所以需要设计限流操作。
1.原理
Redis 中的有序集合ZSet或称为 Sorted Set是按照成员的分数score从小到大排序的。因此我们将当前的时间戳作为分数的话这样我们就得到了一个“时间轴”。
Key的格式设计为【场景:行为:用户唯一标识】score分数值是时间戳value值是什么都可以不重要一般会放时间戳、用户唯一标识和次数等等。
具体流程
当用户每次发生限流行为都会记录这个行为以Redis zset的方式进行记录在业务处理流程中使用java api进行查询判断其实本质就是调用redis的zcount命令这个命令可以传入起始分值和结束分值。我就把当前时间戳作为结束分值然后当前时间戳减去限流时间比如说5分钟的毫秒值求出来5分钟前的时间戳。于是根据这两个时间戳作为分值范围查询zset中出现的次数就得到用户在5分钟内这个行为一共触发了几次。后续的业务就是不同场景中根据不同的需求进行校验就行了。
2.代码邮箱发验证码为例 public void getCode(String email) {//0.合法性检验虽然前端会检验邮箱合法性但后端最好还是也做一些保底的检验//TODO:更多的校验步骤int in email.indexOf();if(in -1){throw new RuntimeException(邮箱地址不合法);}//1.获取当前的时间窗口long currentTimeMillis System.currentTimeMillis();long start currentTimeMillis - LOGIN_EMAIL_WINDOW;//2.执行限流操作前检查用户是否达到了限制条件Long count stringRedisTemplate.opsForZSet().count(LOGIN_EMAIL email, start, currentTimeMillis);//时间窗口里面的操作次数if(count ! null count 2){//3.达到限流条件进行限制deny userthrow new RuntimeException(操作过于频繁请稍后再试);}//4.未达到执行操作String code RandomGenerator.generateRandom(6);MailUtil.send(email, 注册验证码, code, false);stringRedisTemplate.opsForValue().set(LOGIN_CODE email, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);//5.记录此次操作stringRedisTemplate.opsForZSet().add(LOGIN_EMAIL email, email, currentTimeMillis);}3. 问题与解决
高并发环境下redis操作的原子性
使用lua脚本进行一步执行 public void getCode(String email) {// 检验邮箱合法性int in email.indexOf();if (in -1) {throw new RuntimeException(邮箱地址不合法);}// Lua 脚本String luaScript local count redis.call(zcount, KEYS[1], ARGV[1], ARGV[2])\n if count tonumber(ARGV[3]) then\n return false\n else\n redis.call(zadd, KEYS[1], ARGV[2], ARGV[4])\n redis.call(setex, KEYS[2], ARGV[5], ARGV[6])\n return true\n end;long currentTimeMillis System.currentTimeMillis();long start currentTimeMillis - LOGIN_EMAIL_WINDOW;String code RandomGenerator.generateRandom(6);// 参数设置ListString keys Arrays.asList(LOGIN_EMAIL email, LOGIN_CODE email);ListString args Arrays.asList(String.valueOf(start),String.valueOf(currentTimeMillis),2, // 请求次数上限email,String.valueOf(LOGIN_CODE_TTL * 60), // 过期时间秒code);// 执行Lua脚本Boolean result stringRedisTemplate.execute(new DefaultRedisScriptBoolean(luaScript, Boolean.class),keys,args.toArray(new String[0]));// 判断结果if (Boolean.FALSE.equals(result)) {throw new RuntimeException(操作过于频繁请稍后再试);}// 发送邮件MailUtil.send(email, 注册验证码, code, false);}过时数据的积累
定时任务清理过时数据
Component
public class RedisDataCleaner {private final StringRedisTemplate stringRedisTemplate;public RedisDataCleaner(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate stringRedisTemplate;}Scheduled(fixedRate 300000) // 每5分钟执行一次public void cleanOldEntries() {long currentTimeMillis System.currentTimeMillis();long threshold currentTimeMillis - (5 * 60 * 1000); // 5分钟前stringRedisTemplate.opsForZSet().removeRangeByScore(yourZSetKey, 0, threshold);}
}续约token实现长期登录
0.设计的出发点
减少用户登录次数提高用户体验
1.前置知识:JWT
什么是 JWT
JWTJSON Web Token是一种基于 JSON 的开放标准RFC 7519用于在各方之间安全地传输信息。它可以被用来进行身份验证和信息交换。由于 JWT 是经过数字签名的因此信息是可信任的。
JWT 的结构
一个 JWT 由三个部分组成每部分之间用点.分隔
Header头部Payload负载Signature签名
组合后的格式如下
xxxxx.yyyyy.zzzzz1. Header头部
Header 通常包含两部分信息
类型即令牌类型JWT。算法用于生成签名的哈希算法例如 HMAC SHA256 或 RSA。
示例
{alg: HS256,typ: JWT
}然后将 JSON 格式的 Header 使用 Base64URL 编码得到 JWT 的第一部分。
2. Payload负载
Payload 部分包含声明Claims即需要传递的数据。这些声明分为三类
注册声明Registered Claims一组预定义的声明推荐但不强制使用例如 iss发行人、exp过期时间、sub主题、aud受众等。公共声明Public Claims可自定义的声明但为了避免冲突最好在 IANA JSON Web Token Registry 中注册或使用 URI 形式。私有声明Private Claims自定义的声明用于双方协商使用不在公共注册中。
示例
{sub: 1234567890,name: John Doe,admin: true
}同样将 Payload 使用 Base64URL 编码得到 JWT 的第二部分。
3. Signature签名
签名部分是对前两部分的签名确保令牌的完整性和真实性。签名的生成方式如下
signature HMACSHA256(base64UrlEncode(header) . base64UrlEncode(payload),secret
)其中secret 是服务器端的密钥不能泄露。
JWT 的工作原理 认证阶段用户通过提供凭证如用户名和密码向服务器请求认证。 生成 JWT服务器验证用户身份后生成 JWT包含用户信息和其他声明并使用密钥进行签名。 返回 JWT服务器将生成的 JWT 返回给客户端。 存储 JWT客户端通常将 JWT 存储在本地存储LocalStorage或 Cookie 中。 请求带上 JWT客户端在后续请求中将 JWT 放在 HTTP 请求的 Authorization 头部中 Authorization: Bearer token服务器验证 JWT服务器接收到请求后验证 JWT 的签名和有效性确认后处理请求。
2.思路 首先明确无论用户用什么方式登录包括第三方认证的全是返回token作为认证 正常登录的流程
后端在返回Token的时候是生成两个Token 一个是AccessToken我管他叫访问令牌我处于安全考虑比如防止令牌被恶意使用设置他的有效期为3个小时每次请求资源时携带这个令牌 另一个是RefreshToken我管他叫刷新令牌这个令牌不能用来访问资源只能用来刷新访问令牌就是每当访问令牌过期前端携带这个RefreshToken获取新的AccessToken这个刷新Token的有效期我设置为7天当然这个可以改这是写在配置文件中的。
当Token返回给前端后浏览器端用的是localStorage保存的App端的话有他们自己的本地保存方式将这两个Token保存下来。
访问受限资源
我设置了一个拦截器对受限资源进行拦截这个拦截器会检验请求中是否携带accessToken携带了正常的accessToken放行就行在这里没有讨论的必要这里讲解一下其它几种情况
未携带直接拒绝携带了过期的accessToken返回accessToken过期的标识提醒客户端使用refresh刷新accessToken;
前端判断拒绝的状态码为AccessToken无效后会重新发起一次请求携带RefreshToken重新请求续约接口这个续约接口是不需要网关拦截的然后续约接口针对RefreshToken进行解密后校验签名没有问题没有被篡改于是重新颁发新的AccessToken返回给前端。 前端重新携带AccessToken发起请求就行了。
code 登录返回双重token public LoginVO login(LoginDTO loginDTO) {String email loginDTO.getEmail().toLowerCase();LambdaQueryWrapperUser queryWrapper Wrappers.lambdaQuery(User.class).eq(User::getEmail, email).eq(User::getPassword, loginDTO.getPassword()).eq(User::getDelFlag, 0);User user userService.getOne(queryWrapper);if(BeanUtil.isEmpty(user)){throw new RuntimeException(用户账号或密码错误);}// 生成令牌String accessToken jwtTokenUtil.generateAccessToken(user);String refreshToken jwtTokenUtil.generateRefreshToken(user);return new LoginVO(accessToken, refreshToken);
}刷新接口提供使用refreshToken刷新accessToken public LoginVO refresh(String refreshToken) {boolean f jwtTokenUtil.validateToken(refreshToken);if(!f){throw new RuntimeException(刷新令牌异常请重新登录);}LambdaQueryWrapperUser queryWrapper Wrappers.lambdaQuery(User.class).eq(User::getEmail, jwtTokenUtil.getEmailFromToken(refreshToken)).eq(User::getDelFlag, 0);User user userService.getOne(queryWrapper);if(BeanUtil.isEmpty(user)){throw new RuntimeException(刷新令牌异常请重新登录);}String accessToken jwtTokenUtil.generateAccessToken(user);String newRefreshToken jwtTokenUtil.generateRefreshToken(user);return new LoginVO(accessToken, newRefreshToken);
}拦截器实现对accessToken的解析 public class JwtAuthenticationIntercept implements HandlerInterceptor {private JwtTokenUtil jwtTokenUtil;public JwtAuthenticationIntercept(JwtTokenUtil jwtTokenUtil) {this.jwtTokenUtil jwtTokenUtil;}Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String header request.getHeader(Authorization);if (StrUtil.isNotBlank(header) header.startsWith(Bearer )) {String token header.substring(7);if (jwtTokenUtil.validateToken(token)) {String email jwtTokenUtil.getEmailFromToken(token);UserHolder.saveUser(new UserDTO(email));return true;}response.setHeader(accessToken, outdated);response.getWriter().write(访问token过期);}return false;}Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {UserHolder.removeUser();}
}