图片验证码
需求分析
- 连续因输错密码而登录失败时,记录其连续输错密码的累加次数;若在次数小于5时,用户输入正确的密码并成功登录,则次数被清零
- 连续5次因输错密码而登录失败后,系统弹框提示【您已连续5次输入错误的密码,暂时不允许登录,请10分钟后再次尝试登录】;点击提示框中的【确定】按钮,提示框被关闭
- 10分钟内再次尝试登录,则系统弹框提示【您已连续5次输入错误的密码,暂时不允许登录,7分43秒后可再次尝试登录】;点击提示框中的【确定】按钮,提示框被关闭;注:提示框中的剩余时间动态倒数至0分0秒
- 10分钟后,用户可再次尝试登录;此时,若用户在输错密码次数小于5次时成功登录,则其连续输错密码的次数、曾被锁定1次的信息被清空归零;反之,若用户再次连续5次输错密码,则系统弹框提示【您已连续10次输入错误的密码,账号已被锁定、不允许登录,请联系管理员解锁】;点击提示框中的【确定】按钮,提示框被关闭。此后,用户每次用该账号尝试登录时,均弹出此提示框。此时,在运营端,该用户详情页面中的【登录状态】已被自动切换为【锁定】。用户须主动联系莫族密运营人员,运营人员确认用户没有被盗号、遭遇网络攻击等风险后,主动将其【登录状态】置为【解锁】;此时,用户连续输错密码的次数、曾被锁定2次的信息被清空归零
- 用户登录时,须输入正确的【验证码】
- 若用户看不清,则可点击【看不清?换一张】字样,也可直接点击验证码部件,点击后自动刷新验证码
- 点击【登录】按钮后,【用户名】、【密码】、【验证码】这3项但凡有1项校验不通过,则登录失败,【用户名】、【密码】、【验证码】框中已录入的内容被清空,验证码自动刷新
- 点击【登录】按钮后,若【用户名】、【密码】校验通过,唯独【验证码】校验不通过,则登录失败的系统提示内容为【验证码错误,请重新录入验证码】。同时验证码自动刷新。
- 【验证码】的有效时间为60秒,超过之后则失效,但不自动刷新。失效之后若录入正确的【用户名】【密码】同时录入页面上已失效的【验证码】,则登录失败,且登录失败的系统提示内容为【验证码错误,请重新录入验证码】,同时验证码自动刷新
实施
验证码接口 | 请求头方式传递
- 依赖导入
<!-- 添加图形验证码依赖 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-captcha</artifactId> <version>5.8.5</version> </dependency>
- 图片验证码接口编写
/** * 生成验证码图片 * @return */ @ApiOperation("获取图形验证码") @GetMapping("/identifyImage") public Result<String> identifyImage(HttpServletResponse response, @ApiParam(value = "图形验证码id,无值:生成验证码,有值:刷新验证码") @RequestParam(name = "codeId", required = false) String codeId) throws IOException { // 创建验证码,设置宽、高、长度、干扰线数量 LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 90, 4, 100); // 获取验证码字符串,赋值code String code = lineCaptcha.getCode(); if (codeId == null) { // IdWorker.getId():IdWorker工具类生成唯一ID,并转换成String类型 codeId = String.valueOf(IdWorker.getId()); // 将codeId、code.toUpperCase()、过期时间60秒:存储入Redis中 // code.toUpperCase():code装换成大写形式存储 redisOps.set(codeId,code.toUpperCase(),60); } else { redisOps.set(codeId,code.toUpperCase(),60); } // 将图片验证码codeId设置请求头中 response.setHeader("codeId", codeId); // 获取向客户端发送响应数据的输出流 try (ServletOutputStream outputStream = response.getOutputStream()) { // 验证码图片数据写入到输出流 lineCaptcha.write(outputStream); } catch (Exception e) { throw new AuthException("图形验证码输出错误"); } return Result.succ(codeId); }
- Postman调用测试
http://localhost:9036/api/identifyImage
验证码接口 | base64方式传递
/** * 生成验证码图片 * @return */ @ApiOperation("获取图形验证码") @GetMapping("/identifyImage") public Result<IdentifyImageResp> identifyImage(HttpServletResponse response, @ApiParam(value = "图形验证码id,无值:生成验证码,有值:刷新验证码") @RequestParam(name = "codeId", required = false) String codeId) throws IOException { LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 90, 4, 100); String code = lineCaptcha.getCode(); if (codeId == null) { codeId = String.valueOf(IdWorker.getId()); redisOps.set(codeId,code.toUpperCase(),60); } else { redisOps.set(codeId,code.toUpperCase(),60); } IdentifyImageResp identifyImageResp = new IdentifyImageResp(codeId, lineCaptcha.getImageBase64Data()); return Result.succ(identifyImageResp); }
登录接口
- 登录接口编写
@PostMapping("login") @ApiOperation("用户登录") public Result login(@Validated @RequestBody LoginRequest request) { // request.getCodeId():请求体中获取codeId // redisOps.get(request.getCodeId():codeId为键,获取redis中对应的值 String codeId = (String) redisOps.get(request.getCodeId()); if (codeId.isEmpty()){ throw new AuthException("验证码已过期请刷新重试"); } AuthContext login = authService.login(request); // 登录成功后,通过 login.getMerchant() 获取到登录的用户对象,跟新登录信息 Merchant merchant = login.getMerchant(); merchant.setLastLoginAt(merchant.getLoginAt()); merchant.setLoginAt(new Date()); merchant.setLastLoginIp(merchant.getLoginIp()); merchant.setLoginIp(CommonTools.getIp(httpServletRequest)); merchantRepo.updateById(merchant); login.setMerchant(merchant); // JsonMapper.objectToJson(login):将login对象转换成 JSON 格式的字符串 log.info("LOGIN - > {}", JsonMapper.objectToJson(login)); return Result.succ("登录成功"); }
- LoginRequest.java:请求体字段
@Data @NoArgsConstructor @AllArgsConstructor public class LoginRequest { @NotEmpty @ApiModelProperty("登录名") private String username; @NotEmpty @ApiModelProperty("密码,md5加密全小写") private String password; @ApiModelProperty("验证码") private String code; @ApiModelProperty("验证码Id") private String codeId; }
- AuthService.java
public AuthContext login(LoginRequest login) { // 登录验证和处理 if (StringUtils.isBlank(login.getUsername())) { throw new AuthException("用户名不能为空"); } if (StringUtils.isBlank(login.getPassword())) { throw new AuthException("密码不能为空"); } // 缓存清空,登出操作 logout(); Merchant merchant = findMerchantByLoginEmail(login.getUsername()); if (merchant == null) { authError("账户不存在,或状态不正确"); } else if (merchant.getIsLocked()) { authError("账户已停用"); } // 从redis获取login.getUsername()+"lock-time")的键对应的值 if (redisOps.get(login.getUsername()+"lock-time") != null){ // redisOps.getExpire:获取 Redis 中指定键的过期时间 long expire = redisOps.getExpire(login.getUsername() + "lock-time"); // 转换为分钟 int minutes = (int) (expire / 60); // 转换为秒钟 int seconds = (int) (expire % 60); authError("您已连续5次输入错误的密码,暂时不允许登录,"+minutes+"分"+seconds+"秒后可再次尝试登录"); } System.out.println(merchant.getLoginPassword()); System.out.println(SecretUtils.encrypt(login.getPassword())); Integer errorNum = (Integer) redisOps.get(login.getUsername()); if (!merchant.getLoginPassword().equals(SecretUtils.encrypt(login.getPassword()))) { //密码错误次数为null时创建键值对 if (errorNum == null){ redisOps.set(login.getUsername(),1); }else if ((errorNum > 0 && errorNum < 4) || (errorNum > 5 && errorNum < 10)){ //密码错误次数为0-4、5-10时incr redisOps.incr(login.getUsername(),1); }else if (errorNum+1==5){ //密码错误次数为5时锁定10分钟 redisOps.set(login.getUsername()+"lock-time","lock",600); authError("您已连续5次输入错误的密码,暂时不允许登录,请10分钟后再次尝试登录"); }else { //密码错误次数为10时锁定 merchant.setIsLocked(true); merchantRepo.updateById(merchant); authError("您已连续10次输入错误的密码,账号已被锁定、不允许登录,请联系管理员解锁"); } authError("密码不正确"); } String code= (String) redisOps.get(login.getCodeId()); if (code == null || login.getCode()==null || !code.equals(login.getCode().toUpperCase())){ authError("请输入正确的验证码"); } // merchant.setLoginPassword("*"); String token = Sha.sha256(UUID.randomUUID().toString()); AuthContext authContext = new AuthContext(token, merchant, null); redisOps.set(token, JsonMapper.objectToJson(authContext), authProp.getExpiresSeconds()); CookieUtils.setCookie(response, "/", authProp.getTokenHeader(), token, authProp.getExpiresSeconds()); //登陆完成删除账号错误次数 if (errorNum!=null) redisOps.delete(login.getUsername()); return authContext; } public String logout() { String cookie = CookieUtils.getCookie(request, authProp.getTokenHeader()); String token = StringUtils.isNotBlank(cookie) ? cookie : request.getHeader(authProp.getTokenHeader()); if (StringUtils.isNotBlank(token)) { redisOps.delete(token); } return "登出成功"; } private void authError(String errorMsg) { throw new AuthException(errorMsg); }
- Postman测试
🌼 结语:创作不易,如果觉得博主的文章赏心悦目,还请——
点赞
👍收藏
⭐️评论
📝