news 2026/4/16 15:35:49

登录架构设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
登录架构设计

ps:
内含 分库分表 窗口限流 验证码校验 密码加密 jwt加密
等,算是一个合格的架构,我基本都是按照这个方法,生成的。哪怕是单体也是。主要是方便

登录设计

管理员登录

1.怎么实现登录安全的

2.获取短信验证码时间窗口使用了什么限流算法

登录安全

登录前:登录-去查询数据库 如果有反回jwt令牌

登录后

利用getway网关->进行控制请求->JWT验证通过后 可访问其他服务

CREATETABLEuser(id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT'自增主键',uuidCHAR(36)NOT NULL COMMENT'全局唯一标识,适用于分库分表',usernameVARCHAR(50)NOT NULL UNIQUE COMMENT'用户名,唯一',passwordCHAR(32)NOT NULL COMMENT'MD5加密后的密码',saltCHAR(8)NOT NULL COMMENT'随机盐值',emailVARCHAR(100)DEFAULT NULL COMMENT'用户邮箱',phoneVARCHAR(20)DEFAULT NULL COMMENT'手机号',status TINYINT DEFAULT1COMMENT'用户状态:1-正常,0-禁用',create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT'创建时间',update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'更新时间',PRIMARYKEY(id),UNIQUEKEYuq_uuid(uuid))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='微服务用户表';

开始吧,先看具体服务的逻辑。然后再从大的方向看

知识点

StringUtils.isNotBlank(dto.getPhone())
表达式JVM 层面执行空引用处理返回值示例用途
dto == null直接比较引用 (ifnull/ifnonnull)安全,dto 为 null 不报错dto 为 null → true;dto 不为 null → false判断对象是否存在
dto.equals(null)调用对象的equals方法 (invokevirtual)dto 为 null → 抛NullPointerException;非空对象返回 falsedto 不为 null → false;dto 为 null → NPE比较对象内容相等性
StringUtils.isNotBlank(dto.getPhone())null 检查 → length → 遍历字符判断空白安全,null 返回 falsenull → false;“” → false;" " → false;“abc” → true判断字符串是否有效(非空、非全空白)

不要使用dto.equals

加密

Stringpswd=DigestUtils.md5DigestAsHex((password+salt).getBytes());

==判断引用是否相同,即是否指向同一个对象。

equals判断内容是否相同

对字符串来说,==可能因为不同对象而返回false,即使内容相同。

if(!pswd.equals(dbUser.getPassword())

jwt加密

AppJwtUtil.getToken(dbUser.getId().longValue());

AppJwtUtil 工具类核心分为 5 大功能模块 抽离

老实讲,一直用sqtoken基本忘记了怎么写

  1. Token 生成(核心)
  2. 加密密钥生成
  3. Token 解析(获取 Claims/Header)
  4. Token 有效性校验
  5. 异常处理(过期 / 解析失败)

三部分

package io.jsonwebtoken;

1.生成

publicstaticStringgetToken(Longid){Map<String,Object>claimMaps=newHashMap<>();claimMaps.put("id",id);longcurrentTime=System.currentTimeMillis();returnJwts.builder().setId(UUID.randomUUID().toString()).setIssuedAt(newDate(currentTime))//签发时间.setSubject("system")//说明.setIssuer("heima")//签发者信息.setAudience("app")//接收用户.compressWith(CompressionCodecs.GZIP)//数据压缩方式.signWith(SignatureAlgorithm.HS512,generalKey())//加密方式.setExpiration(newDate(currentTime+TOKEN_TIME_OUT*1000))//过期时间戳.addClaims(claimMaps)//cla信息.compact();}
// 加密KEY private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
package javax.crypto.spec;
public static SecretKey generalKey() { byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes()); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; }

将字节数组封装成SecretKey对象(实现javax.crypto.SecretKey)。

这里"AES"并不是用于 AES 加密,而是指定密钥类型

实际用于 JWT 的signWith时,内部使用 HMAC-SHA512 算法对该字节数组做签名。

对我们的密钥再次加密,后进行哈希签名

所以 JWT 中的signWith(SignatureAlgorithm.HS512, key)就是在用密钥对 header + payload 做 HMAC-SHA512 签名,而不是单纯的 SHA512 哈希。

HS512 签名

用密钥对 header + payload 做哈希签名,保证信息未被篡改

Token 解析流程

privatestaticJws<Claims>getJws(Stringtoken){returnJwts.parser().setSigningKey(generalKey()).parseClaimsJws(token);}

Jws 返回值来调取东西

/** * 获取payload body信息 * * @param token * @return */publicstaticClaimsgetClaimsBody(Stringtoken){try{returngetJws(token).getBody();}catch(ExpiredJwtExceptione){returnnull;}

过期解析

/** * 是否过期 * * @param claims * @return -1:有效,0:有效,1:过期,2:过期 */publicstaticintverifyToken(Claimsclaims){if(claims==null){return1;}try{// 获取过期时间与当前时间比较claims.getExpiration().before(newDate());// 需要自动刷新TOKEN 如果 Token 距离过期时间大于 REFRESH_TIME 秒,则无需刷新if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){// Token 快过期,需要自动刷新return-1;}else{return0;}}catch(ExpiredJwtExceptionex){// 捕获 JWT 库抛出的过期异常return1;}catch(Exceptione){// 捕获其他异常(例如解码错误等)return2;}}

先这样

APP登录

接下来研究APP端的登录涉及

层级技术作用
应用层Java业务编排
缓存层Redis限流 + 验证码存储
算法滑动窗口 / 固定窗口限流策略
数据结构Hash / String / ZSet计数与时间

肯定可以接入对应拉框校验,这种,完成之后给一个校验,持久化,下次发送一起发来校验是否可以发送。

层级技术作用
应用层Java业务编排
缓存层Redis限流 + 验证码存储
算法滑动窗口 / 固定窗口限流策略
数据结构Hash / String / ZSet计数与时间
APP登录 ├─ sms:code:{phone}->风控对接放爬虫等一系列机制 ├─ sms:code:{phone}->验证码对象 ├─ sms:send:sliding:{phone}->发送限流 ├─ sms:verify:error:{phone}->校验错误次数 ├─ login:ip:{ip}->接口防刷 ### 实名存储

ZSET滑动窗口

|ZSet特性|在限流中的含义||--------------|--------------||score 有序|用时间戳作为事件发生时间||支持按 score 范围删除|快速删除窗口外请求||支持 `ZCARD`|O(1)得到窗口内请求数量|
    • 60 秒内最多发送 1 次
    • 10 分钟内最多发送 5 次

下面用滑动窗口实现「60 秒 1 次」,10 分钟规则是同一个模型换参数。

sms:send:sliding:{phone}

维度:手机号 一个手机号 = 一个滑动窗口

ZSet 内容

score=1700000000123 value=550e8400-e29b 时间锉和唯一ID

限流窗口定义

windowSize = 60_000 ms
maxCount = 1

任意连续 60 秒内,只允许 1 次发送行为

1️⃣ 限流组件

@ComponentpublicclassSmsSlidingWindowLimiter{@ResourceprivateStringRedisTemplatestringRedisTemplate;/** * 短信发送限流 * * @param phone 手机号 * @param maxCount 窗口内最大次数 * @param windowSize 窗口大小(毫秒) */publicbooleancanSend(Stringphone,intmaxCount,longwindowSize){Stringkey="sms:send:sliding:"+phone;longnow=System.currentTimeMillis();longwindowStart=now-windowSize;ZSetOperations<String,String>zSetOps=stringRedisTemplate.opsForZSet();// 1. 删除窗口外的数据zSetOps.removeRangeByScore(key,0,windowStart);// 2. 统计窗口内请求数Longcount=zSetOps.zCard(key);if(count!=null&&count>=maxCount){returnfalse;}// 3. 记录本次发送行为zSetOps.add(key,UUID.randomUUID().toString(),now);// 4. 设置过期时间(窗口 + 冗余)stringRedisTemplate.expire(key,Duration.ofMillis(windowSize+1000));returntrue;}}

短信验证码发送 Service(业务层)

@ServicepublicclassSmsService{@ResourceprivateSmsSlidingWindowLimiterlimiter;publicvoidsendLoginCode(Stringphone){// 60 秒内最多 1 次booleanallow=limiter.canSend(phone,1,60_000);// 10 分钟最多 5 次booleanallow10Min=limiter.canSend(phone,5,600_000);if(!allow10Min){thrownewRuntimeException("发送次数过多,请稍后再试");}if(!allow){thrownewRuntimeException("短信发送过于频繁,请稍后再试");}// 生成验证码Stringcode=String.valueOf((int)((Math.random()*9+1)*100000));// TODO 调用第三方短信平台发送System.out.println("向手机号 "+phone+" 发送验证码:"+code);// TODO 存储验证码(如 Redis,设置 5 分钟过期)}}

英文验证码(图形/字母校验)

既然弹了,就说说。要么是对接其他家的,要么是调用库

我都是调用库,真要爬,我也没办法-详情见easypan

1.APP 请求获取英文校验码2.后端生成英文验证码(如4位字母)3.返回:-校验码图片(Base64-captchaKey(唯一标识)4.用户输入英文验证码5.APP 请求发送短信:-phone-captchaKey-captchaValue(用户输入)6.后端校验英文验证码7.校验通过 → 执行短信限流 → 发送短信

captcha:img:{captchaKey}

code -> Ab3F

验证码生成工具

publicclassCaptchaUtil{privatestaticfinalStringCHARS="ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz";publicstaticStringrandomCode(intlength){StringBuildersb=newStringBuilder(length);Randomrandom=newRandom();for(inti=0;i<length;i++){sb.append(CHARS.charAt(random.nextInt(CHARS.length())));}returnsb.toString();}}

图片验证码生成(Java2D)

省,这块蛮多的。不多说

获取英文验证码接口

@RestController@RequestMapping("/captcha")publicclassCaptchaController{@ResourceprivateStringRedisTemplateredisTemplate;@GetMapping("/image")publicMap<String,String>getCaptcha()throwsIOException{StringcaptchaKey=UUID.randomUUID().toString();Stringcode=CaptchaUtil.randomCode(4);// 存 Redis(60 秒)redisTemplate.opsForValue().set("captcha:img:"+captchaKey,code,Duration.ofSeconds(60));BufferedImageimage=CaptchaImageUtil.createImage(code);ByteArrayOutputStreamos=newByteArrayOutputStream();ImageIO.write(image,"png",os);Stringbase64=Base64.getEncoder().encodeToString(os.toByteArray());Map<String,String>result=newHashMap<>();result.put("captchaKey",captchaKey);result.put("imageBase64","data:image/png;base64,"+base64);returnresult;}}
// 3. 生成图片BufferedImageimage=CaptchaImageUtil.createImage(code);// 4. 设置响应头response.setContentType("image/png");response.setHeader("Captcha-Key",captchaKey);//设置key 或者持久化,记得删除就好response.setHeader("Cache-Control","no-store, no-cache");// 5. 写入输出流ServletOutputStreamos=response.getOutputStream();ImageIO.write(image,"png",os);os.flush();

短信发送处理

注意,可以根据返回值来看看删不删验证码。容易被刷库。

publicvoidsendLoginCode(Stringphone,StringcaptchaKey,StringcaptchaValue){StringredisKey="captcha:img:"+captchaKey;StringrealCode=redisTemplate.opsForValue().get(redisKey);// 1. 校验英文验证码if(realCode==null||!realCode.equalsIgnoreCase(captchaValue)){//如果要删除记得处理thrownewRuntimeException("英文验证码错误或已过期");}// 2. 验证通过后立即删除(一次性)///记得删除别流空redisTemplate.delete(redisKey);// 3. 短信发送限流booleanallow=limiter.canSend(phone,1,60_000);if(!allow){thrownewRuntimeException("短信发送过于频繁");}// 4. 生成并发送短信验证码StringsmsCode=String.valueOf((int)((Math.random()*9+1)*100000));System.out.println("发送短信验证码:"+smsCode);// TODO 存储短信验证码}}

请求/captcha/image

展示 Base64 图片

提交:

{"phone":"138xxxx","captchaKey":"uuid","captchaValue":"Ab3F"}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 13:10:25

8 个自考文献综述工具,AI 写作降重推荐

8 个自考文献综述工具&#xff0c;AI 写作降重推荐 论文写作的“三重门”&#xff1a;自考人不得不面对的现实 对于正在备战自考的你来说&#xff0c;论文写作无疑是一道难以跨越的门槛。尤其是在撰写文献综述的过程中&#xff0c;不仅要查阅大量资料&#xff0c;还要对已有研究…

作者头像 李华
网站建设 2026/4/16 12:14:35

Cesium中实现动态扩散圆

概要 Cesium中实现动态扩散圆&#xff0c;关键函数仍然是czm_frameNumber。具体的方法参照上一篇流光线的实现方法&#xff0c;主要的思路就是控制片元的透明度即可。 DiffusionCircleMaterial.SOURCE czm_material czm_getMaterial(czm_materialInput materialInput) { cz…

作者头像 李华
网站建设 2026/4/16 13:31:17

PyTorch-CUDA基础镜像安装指南:Ubuntu下GPU环境一键部署教程

PyTorch-CUDA基础镜像安装指南&#xff1a;Ubuntu下GPU环境一键部署教程 在深度学习项目开发中&#xff0c;最让人头疼的往往不是模型设计或调参&#xff0c;而是环境配置——“为什么代码在他机器上跑得好好的&#xff0c;在我这却报CUDA错误&#xff1f;”这种问题几乎每个A…

作者头像 李华
网站建设 2026/4/15 23:21:40

轻量化多模态模型Qwen3-VL-8B在内容审核中的应用探索

轻量化多模态模型Qwen3-VL-8B在内容审核中的应用探索 在社交媒体日均产生数十亿条图文内容的今天&#xff0c;传统基于关键词和单一模态的审核方式早已捉襟见肘。一张看似普通的风景照配上“内部渠道&#xff0c;速来领取”的文案&#xff0c;可能暗藏诈骗诱导&#xff1b;一段…

作者头像 李华
网站建设 2026/4/14 2:27:29

LeetCode hot 100 —— 双指针(面试纯背版)(二)

双指针 1、移动零 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 请注意 ,必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0] 示例 2: 输入: nums = [0] 输出: [0] 提…

作者头像 李华