news 2026/5/8 0:00:38

登录架构设计

作者头像

张小明

前端开发工程师

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/5/5 9:57:01

9个开题报告神器,AI工具推荐让论文更高效!

9个开题报告神器&#xff0c;AI工具推荐让论文更高效&#xff01; 论文路上的“三座大山”&#xff1a;时间、重复率与效率 对于大多数本科生而言&#xff0c;撰写开题报告和毕业论文是一段既紧张又充满挑战的旅程。从选题到资料搜集&#xff0c;从文献综述到框架搭建&#xff…

作者头像 李华
网站建设 2026/5/1 0:36:47

【ros2】告别重复代码:Xacro让URDF编写效率翻倍

文章目录 告别重复代码:Xacro让URDF编写效率翻倍 一、Xacro到底解决了什么问题? 二、Xacro基础语法:先掌握这5个核心 1. 第一步:声明Xacro命名空间(必写!) 2. 变量定义与引用:`<xacro:property>` (1)定义变量 (2)引用变量 3. 数学计算:直接在`${}`中写公式 …

作者头像 李华
网站建设 2026/5/1 0:13:19

8 个论文写作工具,本科生期末论文轻松搞定!

8 个论文写作工具&#xff0c;本科生期末论文轻松搞定&#xff01; 论文路上的“拦路虎”&#xff0c;你是否也经历过&#xff1f; 对于大多数本科生来说&#xff0c;期末论文写作从来不是一件轻松的事。从选题到查资料&#xff0c;从写大纲到撰写正文&#xff0c;每一个环节都…

作者头像 李华
网站建设 2026/4/30 22:46:31

网络安全怎么快速入门,新手也能少走半年弯路!

后台总收到私信&#xff1a;“学网安该先看 Linux 还是先学 Burp&#xff1f;”“找了一堆教程&#xff0c;越学越乱怎么办&#xff1f;”—— 其实不是你学得慢&#xff0c;是没找对循序渐进的路径。很多人一上来就跟风学工具、刷漏洞&#xff0c;结果基础不牢&#xff0c;后期…

作者头像 李华
网站建设 2026/5/3 1:14:56

LeetCode hot 100 —— 哈希(面试纯背版)(一)

一、哈希 1、俩数之和 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。 示例 1: 输…

作者头像 李华