ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

WGCLOUD二次开发:为监控平台添加短信验证码登录

WGCLOUD二次开发:为监控平台添加短信验证码登录 “WGCLOUD支持短信登录系统吗”——这个问题我在不少运维群里看到过。WGCLOUD作为一款开源的服务器监控平台很多团队拿它来做主机资源监控、告警通知用着用着就开始琢磨登录方式的事。毕竟现在大家手机不离手短信验证码登录几乎是企业内部系统的标配需求谁也不想记一堆复杂的密码。所以我把这个问题拆开聊透原生支持情况、能不能改、怎么改、改了之后会遇到哪些坑。WGCLOUD支持短信登录系统吗先说结论WGCLOUD原生版本默认不支持短信验证码登录它目前的登录认证走的是用户名密码的传统方式。但从架构和源码层面看它是可以支持短信登录的只是需要你自己动手做二次开发。这篇文章我就基于WGCLOUD的实际源码结构和认证流程把短信登录这件事完整拆解一遍包括可行性分析、改造思路、核心代码实现和排坑经验。1. 先搞清楚WGCLOUD的登录认证机制1.1 原生登录流程是什么WGCLOUD的服务端基于Spring Boot框架前端页面登录请求会提交到后端的登录接口由Controller处理。核心逻辑是接收用户提交的用户名和密码然后调用UserService里的方法去数据库表user里比对账号密码密码存储的是MD5加密值。校验通过后将用户信息写入Session后续请求都依赖这个Session来识别身份。整个链路可以简化成前端表单提交 - 登录Controller - 查询用户表 - 校验密码 - 写入Session。换句话说WGCLOUD的认证模型就是一个标准的“用户名密码Session”的老牌Web应用。它没有集成任何第三方认证体系也没有预留类似验证码登录的开关配置。所以你翻遍配置文件也只能设置密码强度、登录失败锁定这类功能找不到短信登录相关选项。1.2 为什么原生不支持短信登录想明白这个问题得从WGCLOUD的定位说起。它是一套轻量级监控系统核心价值在“监控”而不是“账号体系”。开发团队优先做的是Agent采集、数据展示、告警通知这些核心能力认证只做到“够用就行”。短信登录需要额外对接短信服务商、维护验证码存储、处理发送频率和过期策略这些都是成本。对于开源版本来说功能优先级不会排到它前面。但这不意味着短信登录和WGCLOUD无缘。它的代码开源而且登录逻辑集中在少数几个文件里只要了解了认证原理完全可以在不破坏原有功能的前提下把短信登录作为第二种登录方式嵌进去。接下来我重点讲怎么实现。2. 短信登录方案的可行性与设计思路2.1 三种实现路径对比想给WGCLOUD加上短信登录无非三条路。第一条是在前端登录页加一个“短信登录”Tab通过验证码接口换取临时凭证再用这个凭证登录后端第二条是直接复用现有的用户名密码登录入口把短信验证码临时拼成一个一次性密码第三条是引入外部统一认证平台比如CAS、OAuth2把短信登录交给认证中心处理。对比一下方案改造量安全性维护成本适用场景前端Tab独立验证码接口较大需改前端和新增后端接口高验证码独立认证中面向正式生产环境一次性密码拼接小改登录校验逻辑低密码易冲突且难管理低临时演示或测试外部统一认证较大需集成SSO高依赖运维能力高已有认证中心的企业我推荐第一种。虽然改造量虽然不是最小的但它在安全性和可用性之间取得了最好的平衡。短信验证码本身就是一个完整的认证因子不应该和密码混在同一个字段里独立接口会让后续排查和维护都更舒服。2.2 技术选型与准备工作做这个改造前你需要准备以下东西WGCLOUD服务端源码包建议下载和线上版本一致的源码我这边用的是3.x版本不同版本细节会有差异一个短信服务商账号国内常见的阿里云短信服务、腾讯云短信或者你公司自建的短信网关Maven、JDK1.8、IDE以及能连上WGCLOUD数据库的客户端一份能够收发短信的手机号测试用短信服务商这块不用纠结选你公司已经在用的那个别为了一个功能引入第二个短信供应商不然接下去对账、申请签名模板都要多忙一轮。2.3 核心设计要点我把整个方案抽象成四个关键点第一验证码的生成和存储。验证码用6位纯数字服务端生成后存储到Rediskey建议格式是sms:login:{手机号}value是验证码有效期5分钟。没有Redis的话可以先用内存Cache代替但生产环境还是建议上Redis因为WGCLOUD的服务器可能会重启内存缓存丢失会导致用户手机上验证码还在、服务端却验证不了体验很差。第二发送频率限制。同一个手机号60秒内只能发送一次验证码一天同一个号码最多发送10次防止被恶意刷量。这些限制参数放到配置文件里方便调整。第三登录状态管理。短信登录成功之后仍然复用WGCLOUD原有的Session机制也就是把当前用户写入Session这样短信登录和密码登录在登录后体验完全一致。第四账号绑定关系。短信登录必须依赖手机号和WGCLOUD用户账号之间的绑定关系。原生的user表里是没有手机号字段的需要在用户表扩展一个字段或者新建一张绑定表。考虑到改动最小直接给数据库用户表加一列phone更直接。3. 实操给WGCLOUD二次开发实现短信验证码登录3.1 环境准备与源码编译先确认你的WGCLOUD版本。我这边以当前主流版本为例服务端源码是标准的Maven项目拿到源码后先导入IDE让Maven自动下载依赖。注意JDK版本WGCLOUD服务端要求JDK1.8太新的JDK可能编译不过。编译之前先检查一下pom.xml里有没有spring-boot-starter-data-redis依赖如果没有就加上。同时加一个HTTP客户端的依赖我用的是hutool-all工具包里面封装的HttpUtil发请求写起来比较省事。如果你不想用Hutool用Spring的RestTemplate也行。依赖部分在pom.xml里加下面的内容dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency加完后刷新一下Maven确认依赖都下载成功然后先直接编译一次原版源码确保环境没问题。这一步别省不然改了代码后分不清是环境问题还是代码问题。3.2 数据库增加手机号字段用数据库客户端连上WGCLOUD的数据库找到user表执行下面的SQLALTER TABLE user ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT 手机号 AFTER username;然后给你已有的管理员账号绑定手机号UPDATE user SET phone 13800000000 WHERE username admin;注意这里的表前缀和字段名以你实际版本为准。有些版本的表名是t_user或者有统一前缀先看下数据库里的真实表结构再动手。3.3 编写短信发送和验证码存取工具类先把验证码的生成、存储、校验封装成一个服务类我把它命名为SmsCodeService。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; Service public class SmsCodeService { Autowired private StringRedisTemplate redisTemplate; private static final String SMS_LOGIN_PREFIX sms:login:; private static final long EXPIRE_MINUTES 5; public String generateCode(String phone) { // 生成6位随机数字验证码 int code ThreadLocalRandom.current().nextInt(100000, 999999); String key SMS_LOGIN_PREFIX phone; // 存储到Redis有效期5分钟 redisTemplate.opsForValue().set(key, String.valueOf(code), EXPIRE_MINUTES, TimeUnit.MINUTES); return String.valueOf(code); } public boolean verifyCode(String phone, String code) { String key SMS_LOGIN_PREFIX phone; String savedCode redisTemplate.opsForValue().get(key); if (savedCode ! null savedCode.equals(code)) { // 验证通过之后立即删除验证码防止重复使用 redisTemplate.delete(key); return true; } return false; } }这里有个关键细节验证码校验成功后必须立即删除Redis里对应的记录。很多第一次写验证码逻辑的人容易漏掉这一点结果同一个验证码能被反复使用一旦泄露出去就是漏洞。短信发送这块以阿里云短信服务为例核心步骤就是调API。它的SDK会稍微繁琐一点但逻辑不复杂import cn.hutool.json.JSONObject; import cn.hutool.http.HttpUtil; public class SmsSender { private static final String SEND_URL https://dysmsapi.aliyuncs.com/; // 实际接入时这里的accessKeyId、accessKeySecret、signName、templateCode // 都应该从配置文件中读取 private static final String ACCESS_KEY_ID yourAccessKeyId; private static final String ACCESS_KEY_SECRET yourAccessKeySecret; public static boolean sendLoginCode(String phone, String code) { // 阿里云新版SDK需要构建请求推荐直接用官方SDK // 这里为了演示只说明大致流程 // 实际开发中请使用阿里云官方短信服务SDKcom.aliyun:dysmsapi20170525 return true; } }为了保证博文可以照着做这里建议直接用阿里云的官方SDK而不是用HTTP拼接请求。原因很简单官方SDK已经帮你处理了签名、时间戳这些容易出错的细节你用HttpUtil手写请求一个参数顺序不对就可能报签名错误调试成本很高。SDK引入方式dependency groupIdcom.aliyun/groupId artifactIddysmsapi20170525/artifactId version2.0.24/version /dependency3.4 新增发送短信验证码接口在WGCLOUD的Controller目录下新建一个SmsLoginController提供两个接口一个发送验证码一个执行短信登录。发送验证码接口需要做频率限制。用Redis记录上次发送时间同一个手机号在60秒内重复请求时直接拒绝RestController RequestMapping(/api/sms) public class SmsLoginController { Autowired private SmsCodeService smsCodeService; Autowired private StringRedisTemplate redisTemplate; /** * 发送登录验证码 */ PostMapping(/sendLoginCode) public Result sendLoginCode(RequestBody JSONObject params) { String phone params.getStr(phone); if (phone null || !phone.matches(^1\\d{10}$)) { return Result.error(手机号格式不正确); } String freqKey sms:login:freq: phone; String lastTime redisTemplate.opsForValue().get(freqKey); long now System.currentTimeMillis(); if (lastTime ! null (now - Long.parseLong(lastTime)) 60_000) { return Result.error(请求过于频繁请稍后再试); } String code smsCodeService.generateCode(phone); // 调用短信服务商接口发送验证码 boolean sendOk SmsSender.sendLoginCode(phone, code); if (!sendOk) { return Result.error(验证码发送失败); } redisTemplate.opsForValue().set(freqKey, String.valueOf(now), 60, TimeUnit.SECONDS); return Result.success(验证码已发送); } }这里有个坑如果短信服务商接口响应很慢发送耗时可能超过几秒而Redis的60秒频率限制记录在发送成功后才写入这就导致连续点击发送按钮时第一次发送未结束、第二次请求进来频率记录还没写入照样可以发送。解决办法是提前记录发送中状态或者依赖短信服务商的云端流控但更稳妥的办法是前端在收到发送请求后立即禁用按钮60秒同时后端也做校验双保险。3.5 实现短信登录接口短信登录的核心逻辑就是校验验证码根据手机号找到对应的WGCLOUD用户然后写入Session。写入Session的方式要跟原版登录完全一致才会不破坏后续的访问控制逻辑。先看一下原版的登录成功是怎么写Session的。一般代码会是这样request.getSession().setAttribute(user, userObj)或者通过Spring Security框架管理。不同版本的WGCLOUD处理方式不同这步必须直接翻源码确认。找到那行之后把用户对象设置成你查询到的用户即可。/** * 短信验证码登录 */ PostMapping(/loginBySms) public Result loginBySms(RequestBody JSONObject params, HttpServletRequest request) { String phone params.getStr(phone); String code params.getStr(code); if (phone null || code null) { return Result.error(参数错误); } // 1. 校验验证码 boolean verifyOk smsCodeService.verifyCode(phone, code); if (!verifyOk) { return Result.error(验证码错误或已过期); } // 2. 根据手机号查用户 User user userService.findByPhone(phone); if (user null) { return Result.error(该手机号未绑定WGCLOUD账号); } // 3. 写入Session具体写法和WGCLOUD源码保持一致 request.getSession().setAttribute(user, user); request.getSession().setAttribute(loginType, sms); return Result.success(登录成功); }UserService里要新增一个findByPhone方法对应的Mapper加一个查询SQLSELECT * FROM user WHERE phone #{phone}这里有一个很容易踩的坑WGCLOUD的Session存储格式不一定只是user这个属性登录成功可能还会写入对应权限ID、角色标识等。你最好把原版密码登录成功的代码原封不动复制过来只替换掉前面的用户名密码校验逻辑改成验证码校验这样Session内容就不会漏。我是这么做的先看原版登录成功代码把那段代码抽到一个公共方法比如handleLoginSuccess(request, user)然后短信登录里也调用这个公共方法两套登录走同一个登录后逻辑。3.6 前端页面改造WGCLOUD的前端技术栈是JSP jQuery登录页面通常位于/src/main/webapp/WEB-INF/views下。改造思路就是在登录表单上方加一个切换按钮“密码登录”/“短信登录”。切到短信登录时隐藏密码输入框显示手机号输入框和“获取验证码”按钮。获取验证码按钮点击后调用/api/sms/sendLoginCode接口60秒倒计时之后提交时调用/api/sms/loginBySms接口成功后跳转到原版登录成功跳转的那个URL。前端核心代码片段function sendSmsCode() { var phoneVal $(#loginPhone).val(); if (!/^1\d{10}$/.test(phoneVal)) { alert(请输入正确的手机号); return; } $.post(/api/sms/sendLoginCode, {phone: phoneVal}, function(res) { if (res.code 200) { // 倒计时按钮 var timeLeft 60; var tipBtn $(#sendCodeBtn); tipBtn.attr(disabled, true); var timer setInterval(function() { tipBtn.text(timeLeft 秒后重发); timeLeft--; if (timeLeft 0) { clearInterval(timer); tipBtn.text(获取验证码); tipBtn.attr(disabled, false); } }, 1000); } else { alert(res.msg); } }); } function smsLogin() { var phoneVal $(#loginPhone).val(); var codeVal $(#loginCode).val(); $.post(/api/sms/loginBySms, {phone: phoneVal, code: codeVal}, function(res) { if (res.code 200) { window.location.href /; // 原版登录成功后的跳转地址 } else { alert(res.msg); } }); }前端注意一个小细节短信验证码输入框的autocomplete属性建议设置成new-password防止浏览器自动填充历史验证码特别是在公用电脑上这个细节能避免不少尴尬。3.7 配置文件补充把短信服务商密钥等敏感信息加到application.properties或application.yml配置文件里不要硬编码在Java类中。例如sms.provideraliyun sms.accessKeyIdyourAccessKeyId sms.accessKeySecretyourAccessKeySecret sms.signName监控平台 sms.templateCodeSMS_123456789 sms.verify.expireMinutes5 sms.verify.sendIntervalSeconds60 sms.verify.dayMaxTimes10在Java代码里用Value注入这些配置。这样以后更换短信服务商或调整策略只改配置文件不动代码。4. 常见问题与排查技巧实录4.1 验证码发送成功但收不到这个问题90%出现在短信服务商的签名和模板审核上。先登录短信控制台看发送记录一般有“发送成功但被运营商拦截”、“签名未通过”、“模板变量异常”等状态。常见原因是模板内容里写了“验证码”三个字但签名跟模板不匹配。比如你的签名是“WGCLOUD监控”模板内容是“您的验证码为${code}5分钟内有效”那没问题。但如果模板里带了“登录”两个字有些短信平台会要求额外提供“登录场景”的报备说明。排查顺序建议是先看服务商后台发送记录 - 确认签名和模板审核状态 - 手动在后台发一条测试短信 - 再回到平台调接口。4.2 短信接口调用时报签名错误阿里云等平台的签名错误基本是时间戳参数和编码问题。用官方SDK的通常不会出这类问题除非你用的SDK版本太老。另外服务器系统时间一定要校准偏差超过几分钟就会触发签名校验失败。我在一台内网服务器上部署时系统时间慢了两分钟所有短信请求全报签名错误查了半天才发现是时间同步问题。所以记得配置NTP时间同步。4.3 登录成功后跳转返回登录页如果短信登录成功但跳转之后又回到登录页说明Session可能没有被正确识别。有一种典型情况你直接把用户对象放进了Session但WGCLOUD后续校验的是另一个上下文属性。比如它用Spring Security库那登录成功需要走SecurityContextHolder而不是简单Session.setAttribute。这种时候只改登录Controller还不够需要把原版密码登录成功之后的完整逻辑抄过来。我建议你直接全局搜索addFlashAttribute、SecurityContext、session.setAttribute这些关键字把登录成功那段代码定位准确后再动手。4.4 多账号绑定同一手机号怎么处理我们的实现里是手机号唯一对应一个用户如果表里出现了多条相同phone的记录findByPhone查询会报错或者返回不确定记录。所以在功能上线前要确保phone字段有唯一约束ALTER TABLE user ADD UNIQUE KEY uk_phone (phone);如果历史数据有重复需要先清理。手机号绑定原则上是一个手机号只允许绑定一个WGCLOUD账号否则短信登录不知道该登录哪个身份。4.5 安全加固建议短信登录虽然方便但安全防护一定别落下。我建议至少做这几件事限制IP发送频率。一个IP超过比如每小时20次短信请求就锁定24小时防止有人批量刷验证码骚扰别人。验证码输入错误超过5次这个手机号当天禁止再登录避免暴力穷举。另外登录成功之后最好加上操作日志记录登录方式密码/短信、手机号、来源IP、登录时间方便出安全事件时回溯。还有一个容易忽略的点短信验证码接口一定别在未登录状态下暴露发送成功与否的差异。如果手机号未绑定账号接口返回“该手机号未绑定”那么攻击者可以通过这个接口批量探测哪些手机号注册过你的平台。建议统一返回“验证码已发送请查收”然后再在登录校验时提示账号不存在。安全细节越细后面越省心。4.6 升级WGCLOUD版本后代码要跟着改WGCLOUD更新节奏不算慢版本升级后登录模块的代码可能已经变化。你本地做过二次开发的升级时需要重新比对SmsLoginController、用户Mapper、前端登录页这三处。我自己的习惯是保持一份patch文件每次升级后用diff工具把原版代码和新版代码对比再应用自己的改动。虽然麻烦但总比升级完发现短信登录功能静默失效要强。5. 一些合适的扩展思路如果你觉得只做短信登录还不够这套改造框架可以很方便地延伸出其他能力。比如把短信验证码登录扩展到“忘记密码”功能用户输入手机号验证后重置密码。原理完全一样只需要复用验证码生成和校验逻辑。再比如在WGCLOUD的告警通知里加上短信渠道实际上很多团队已经在用短信收告警了这时候你把短信平台接入好登录和告警可以共用同一个短信通道资源利用率更高。另外如果你对账号安全要求很高可以考虑把短信登录做成双因子认证的一部分。也就是说用户先用密码登录登录时如果开启安全校验再要求输入短信验证码。这个在WGCLOUD上也能做只要修改密码登录成功后的跳转逻辑插入一个验证码校验页面即可。不过说实话WGCLOUD这种内网监控工具多数场景部署在内网密码登录已经够用短信登录更多是满足老板或者客户“方便快捷”的体验需求。改动前先想清楚需求不要为炫技而改造。做一个功能前先做好范围评估。WGCLOUD的短信登录不是官方开箱即用的功能但它的开源架构给二次开发留了充足空间。你不需要理解全部源码只需要抓住登录这条链路就能以很低的成本把短信验证码登录嵌进去。我在实际改造过两三个版本后最深的体会是官方不支持的功能并不代表不合适关键是找到正确的切入点。认证链路通常就那么几行代码把Session成功写入的方式复制清楚剩下的都是调用第三方短信接口的事情。最后再分享一个小技巧改造完成后一定不要急着关机先做一次完整的回归测试。拿一个绑定手机号的测试账号走一遍“发送验证码-输入验证码-登录成功-刷新页面-退出登录”全流程再拿一个没绑定手机号的账号试一下异常分支。短信登录这种功能出问题一般不会出在主流程而是出在异常提示、按钮倒计时、Session过期这种边角上。把这些边角打磨好功能才能真正交付出去。
返回列表