• Java 图片验证码需求分析


    在这里插入图片描述

    💗wei_shuo的个人主页

    💫wei_shuo的学习社区

    🌐Hello World !


    图片验证码

    需求分析

    在这里插入图片描述

    • 连续因输错密码而登录失败时,记录其连续输错密码的累加次数;若在次数小于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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 图片验证码接口编写
        /**
         * 生成验证码图片
         * @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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • Postman调用测试
    http://localhost:9036/api/identifyImage
    
    • 1

    在这里插入图片描述

    在这里插入图片描述

    验证码接口 | 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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    登录接口
    • 登录接口编写
    @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("登录成功");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • Postman测试

    在这里插入图片描述


    🌼 结语:创作不易,如果觉得博主的文章赏心悦目,还请——点赞👍收藏⭐️评论📝


    在这里插入图片描述

  • 相关阅读:
    Docker常见命令使用
    spark源码的scala解析
    互联网摸鱼日报(2022-12-02)
    AXI EPC IP 使用详细说明
    【Mac】Indesign 2023 Mac(ID2023) v18.5中文版安装教程
    MySQL 基础知识(九)之视图
    React新手必懂的知识点
    vite+vue3.0 使用tailwindcss
    CloudFlare系列--使用第三方来自定义CDN的IP(笨牛详细版)
    一些显示和画图的小命令函数用法
  • 原文地址:https://blog.csdn.net/weixin_62765017/article/details/131963978