2025/12/30 23:51:33
网站建设
项目流程
杭州网络公司做网站报价,营销加盟网站建设,自媒体制作视频教程,微网站 举例ps:
内含 分库分表 窗口限流 验证码校验 密码加密 jwt加密
等#xff0c;算是一个合格的架构#xff0c;我基本都是按照这个方法#xff0c;生成的。哪怕是单体也是。主要是方便
登录设计
管理员登录
1.怎么实现登录安全的
2.获取短信验证码时间窗口使用了什么限流算法
登录…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 COMMENTMD5加密后的密码,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))ENGINEInnoDBDEFAULTCHARSETutf8mb4 COMMENT微服务用户表;开始吧先看具体服务的逻辑。然后再从大的方向看知识点StringUtils.isNotBlank(dto.getPhone())表达式JVM 层面执行空引用处理返回值示例用途dto null直接比较引用 (ifnull/ifnonnull)安全dto 为 null 不报错dto 为 null → truedto 不为 null → false判断对象是否存在dto.equals(null)调用对象的equals方法 (invokevirtual)dto 为 null → 抛NullPointerException非空对象返回 falsedto 不为 null → falsedto 为 null → NPE比较对象内容相等性StringUtils.isNotBlank(dto.getPhone())null 检查 → length → 遍历字符判断空白安全null 返回 falsenull → false“” → false → false“abc” → true判断字符串是否有效非空、非全空白不要使用dto.equals加密StringpswdDigestUtils.md5DigestAsHex((passwordsalt).getBytes());判断引用是否相同即是否指向同一个对象。equals判断内容是否相同。对字符串来说可能因为不同对象而返回false即使内容相同。if(!pswd.equals(dbUser.getPassword())jwt加密AppJwtUtil.getToken(dbUser.getId().longValue());AppJwtUtil 工具类核心分为 5 大功能模块 抽离老实讲一直用sqtoken基本忘记了怎么写Token 生成核心加密密钥生成Token 解析获取 Claims/HeaderToken 有效性校验异常处理过期 / 解析失败三部分package io.jsonwebtoken;1.生成publicstaticStringgetToken(Longid){MapString,ObjectclaimMapsnewHashMap();claimMaps.put(id,id);longcurrentTimeSystem.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(currentTimeTOKEN_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 解析流程privatestaticJwsClaimsgetJws(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(claimsnull){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 内容score1700000000123 value550e8400-e29b 时间锉和唯一ID限流窗口定义windowSize 60_000 msmaxCount 1任意连续 60 秒内只允许 1 次发送行为1️⃣ 限流组件ComponentpublicclassSmsSlidingWindowLimiter{ResourceprivateStringRedisTemplatestringRedisTemplate;/** * 短信发送限流 * * param phone 手机号 * param maxCount 窗口内最大次数 * param windowSize 窗口大小毫秒 */publicbooleancanSend(Stringphone,intmaxCount,longwindowSize){Stringkeysms:send:sliding:phone;longnowSystem.currentTimeMillis();longwindowStartnow-windowSize;ZSetOperationsString,StringzSetOpsstringRedisTemplate.opsForZSet();// 1. 删除窗口外的数据zSetOps.removeRangeByScore(key,0,windowStart);// 2. 统计窗口内请求数LongcountzSetOps.zCard(key);if(count!nullcountmaxCount){returnfalse;}// 3. 记录本次发送行为zSetOps.add(key,UUID.randomUUID().toString(),now);// 4. 设置过期时间窗口 冗余stringRedisTemplate.expire(key,Duration.ofMillis(windowSize1000));returntrue;}}短信验证码发送 Service业务层ServicepublicclassSmsService{ResourceprivateSmsSlidingWindowLimiterlimiter;publicvoidsendLoginCode(Stringphone){// 60 秒内最多 1 次booleanallowlimiter.canSend(phone,1,60_000);// 10 分钟最多 5 次booleanallow10Minlimiter.canSend(phone,5,600_000);if(!allow10Min){thrownewRuntimeException(发送次数过多请稍后再试);}if(!allow){thrownewRuntimeException(短信发送过于频繁请稍后再试);}// 生成验证码StringcodeString.valueOf((int)((Math.random()*91)*100000));// TODO 调用第三方短信平台发送System.out.println(向手机号 phone 发送验证码code);// TODO 存储验证码如 Redis设置 5 分钟过期}}英文验证码图形/字母校验既然弹了就说说。要么是对接其他家的要么是调用库我都是调用库真要爬我也没办法-详情见easypan1.APP 请求获取英文校验码2.后端生成英文验证码如4位字母3.返回-校验码图片Base64-captchaKey唯一标识4.用户输入英文验证码5.APP 请求发送短信-phone-captchaKey-captchaValue用户输入6.后端校验英文验证码7.校验通过 → 执行短信限流 → 发送短信captcha:img:{captchaKey}code - Ab3F验证码生成工具publicclassCaptchaUtil{privatestaticfinalStringCHARSABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz;publicstaticStringrandomCode(intlength){StringBuildersbnewStringBuilder(length);RandomrandomnewRandom();for(inti0;ilength;i){sb.append(CHARS.charAt(random.nextInt(CHARS.length())));}returnsb.toString();}}图片验证码生成Java2D省这块蛮多的。不多说获取英文验证码接口RestControllerRequestMapping(/captcha)publicclassCaptchaController{ResourceprivateStringRedisTemplateredisTemplate;GetMapping(/image)publicMapString,StringgetCaptcha()throwsIOException{StringcaptchaKeyUUID.randomUUID().toString();StringcodeCaptchaUtil.randomCode(4);// 存 Redis60 秒redisTemplate.opsForValue().set(captcha:img:captchaKey,code,Duration.ofSeconds(60));BufferedImageimageCaptchaImageUtil.createImage(code);ByteArrayOutputStreamosnewByteArrayOutputStream();ImageIO.write(image,png,os);Stringbase64Base64.getEncoder().encodeToString(os.toByteArray());MapString,StringresultnewHashMap();result.put(captchaKey,captchaKey);result.put(imageBase64,data:image/png;base64,base64);returnresult;}}// 3. 生成图片BufferedImageimageCaptchaImageUtil.createImage(code);// 4. 设置响应头response.setContentType(image/png);response.setHeader(Captcha-Key,captchaKey);//设置key 或者持久化记得删除就好response.setHeader(Cache-Control,no-store, no-cache);// 5. 写入输出流ServletOutputStreamosresponse.getOutputStream();ImageIO.write(image,png,os);os.flush();短信发送处理注意可以根据返回值来看看删不删验证码。容易被刷库。publicvoidsendLoginCode(Stringphone,StringcaptchaKey,StringcaptchaValue){StringredisKeycaptcha:img:captchaKey;StringrealCoderedisTemplate.opsForValue().get(redisKey);// 1. 校验英文验证码if(realCodenull||!realCode.equalsIgnoreCase(captchaValue)){//如果要删除记得处理thrownewRuntimeException(英文验证码错误或已过期);}// 2. 验证通过后立即删除一次性///记得删除别流空redisTemplate.delete(redisKey);// 3. 短信发送限流booleanallowlimiter.canSend(phone,1,60_000);if(!allow){thrownewRuntimeException(短信发送过于频繁);}// 4. 生成并发送短信验证码StringsmsCodeString.valueOf((int)((Math.random()*91)*100000));System.out.println(发送短信验证码smsCode);// TODO 存储短信验证码}}请求/captcha/image展示 Base64 图片提交{phone:138xxxx,captchaKey:uuid,captchaValue:Ab3F}