news 2026/4/18 5:44:28

登录架构设计

作者头像

张小明

前端开发工程师

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/17 18:26:26

介观交通流仿真软件:Aimsun Next_(1).介观交通流仿真的基础理论

介观交通流仿真的基础理论 1. 介观交通流仿真的概念 介观交通流仿真是一种介于宏观交通流仿真和微观交通流仿真之间的交通仿真方法。宏观仿真关注交通流的整体行为和统计特性&#xff0c;而微观仿真则关注每个个体车辆的详细行为。介观仿真通过结合这两种方法的优点&#xff0c…

作者头像 李华
网站建设 2026/4/18 2:19:17

从「第 K 小」这道题,看懂二叉搜索树的灵魂

别急着写代码 ——从「第 K 小」这道题,看懂二叉搜索树的灵魂 先说一句很多人不爱听、但非常重要的话: 这道题考的不是技巧,而是你到底懂不懂二叉搜索树。 如果你真的懂 BST,这题会让你觉得—— “哦,就该这么解”。 如果你不懂,那你会: 写一堆 if else 用数组存一遍…

作者头像 李华
网站建设 2026/4/11 0:58:35

通过API调用Qwen3-14B实现外部工具集成的方法

通过API调用Qwen3-14B实现外部工具集成的方法 在企业AI落地的实践中&#xff0c;一个常见的困境是&#xff1a;模型能说会道&#xff0c;却“光说不做”。用户问“我的订单到哪儿了”&#xff0c;系统只能回答“请查看物流信息”——这显然不是智能化服务应有的样子。真正有价…

作者头像 李华
网站建设 2026/4/9 8:24:47

特价股票投资中的跨资产类别系统性数字创新溢出效应识别

特价股票投资中的跨资产类别系统性数字创新溢出效应识别关键词&#xff1a;特价股票投资、跨资产类别、数字创新溢出效应、识别方法、金融科技摘要&#xff1a;本文聚焦于特价股票投资领域&#xff0c;深入探讨跨资产类别系统性数字创新溢出效应的识别问题。首先介绍了研究的背…

作者头像 李华
网站建设 2026/4/17 15:50:54

Python UV新玩法:结合Miniconda实现超高速包管理

Python UV新玩法&#xff1a;结合Miniconda实现超高速包管理 在现代AI与数据科学项目中&#xff0c;一个令人头疼的日常场景是&#xff1a;你刚克隆了一个新的机器学习仓库&#xff0c;满怀期待地准备跑通demo&#xff0c;结果执行 pip install -r requirements.txt 后&#x…

作者头像 李华