当前位置: 首页 > 图灵资讯 > 技术篇> SpringCloud微服务实战——搭建企业级开发框架(二十四):集成行为验证码和图片验证码实现登录功能

SpringCloud微服务实战——搭建企业级开发框架(二十四):集成行为验证码和图片验证码实现登录功能

来源:图灵教育
时间:2023-10-20 17:54:50

  随着近年来技术的发展,人们对系统安全和用户体验的要求越来越高,大多数网站系统逐渐使用行为验证码来取代图片验证码。行为验证码是指通过用户行为验证用户身份的验证码,如滑动拼图、识别图片中的特定物品等。

  行为验证码的重要性在于,它可以有效地防止机器人和恶意程序恶意攻击网站或应用程序,刷流量,撞击图书馆,从而确保用户和网站的安全。与传统的图形验证码相比,行为验证码更难破解,用户操作更方便。

  此外,行为验证码还可以提高用户体验,减轻用户操作负担,提高网站或应用程序的可用性和用户保留率。因此,使用行为验证码是保护用户和网站安全的重要手段,也是提高用户体验的有效途径。

  以下是几种常用的 Java 开源验证码库:

  1. JCaptcha:Java下的开源验证码库采用Java Servlet 基于Apache标准的Jakarta项目Struts和Ant应用程序框架。

  2. Kaptcha:Java开发的验证码库可生成随机数字、字母、汉字等验证码。

  3. SimpleCaptcha:开源验证码库采用Java语言编写,支持生成数字、字母和简单的数学操作验证码。

  4. Google Authenticator:Google发布的开源身份验证器可生成一次性密码,常用于二步验证。

  5. AJ-Captcha:在Web应用程序中,用于保护用户免受恶意攻击和垃圾邮件的人机验证码。它是基于JavaScript的验证码,可以在不需要重新加载页面或使用外部API的情况下生成和验证验证码。它采用了多种技术,包括画布渲染和随机化,使得攻击者难以识别和破解验证码。 AJ-Captcha易于集成,适应不同的屏幕大小,并能定制外观和行为。

  6. EasyCaptcha:它是一种简单易用的验证码生成器,用于添加人类验证,防止恶意自动化。它由PHP编写,提供基于文本、数字、数学等方法的多种验证码类型。EasyCaptcha支持自定义风格和语言,具有良好的可扩展性。它可用于各种Web应用程序,如登录页面、注册页面、联系表等,以提高网站的安全性。

此外,还有一些商业验证码库,如Turing Image、Secure Image等。

GitEgg-Cloud集成了开源行为验证码组件和图片验证码,并将可配置项添加到系统中,以选择具体使用的验证码。

  • AJ-Captcha:行为验证码
  • EasyCaptcha: 图片验证码
1、在我们的giteggg中-platform-bom工程中增加验证码的包依赖性
        <!-- AJ-Captcha滑动验证码 -->        <captcha.version>1.2.7</captcha.version>        <!-- Easy-Captcha图形验证码 -->        <easy.captcha.version>1.6.2</easy.captcha.version>        <!-- captcha 滑动验证码-->        <dependency>           <groupId>com.github.anji-plus</groupId>           <artifactId>captcha-spring-boot-starter</artifactId>           <version>${captcha.version}</version>       </dependency>       <!-- easy-captcha 图形验证码-->       <dependency>             <groupId>com.github.whvcse</groupId>             <artifactId>easy-captcha</artifactId>          <version>${easy.captcha.version}</version>      </dependency>
2、新建gitegg-platform-captcha工程用于配置和自定义方法,行为验证码用于缓存需要自定义来实现captchacheservice,captchacheservicerdisimple:
public class CaptchaCacheServiceRedisImpl implements CaptchaCacheService {    @Override    public String type() {        return "redis";    }    @Autowired    private StringRedisTemplate stringRedisTemplate;    @Override    public void set(String key, String value, long expiresInSeconds) {        stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);    }    @Override    public boolean exists(String key) {        return stringRedisTemplate.hasKey(key);    }    @Override    public void delete(String key) {        stringRedisTemplate.delete(key);    }    @Override    public String get(String key) {        return stringRedisTemplate.opsForValue().get(key);    }}
3、gitegg-platform-captcharesources目录新建META-INF.services文件夹参考resource/META-INF/services中的写法。
com.gitegg.platform.captcha.service.impl.CaptchaCacheServiceRedisImpl
4、在giteg-cloud下的giteg-oauth中添加captchatchatokengranter自定义验证码授权处理类
/** * 验证码模式 */public class CaptchaTokenGranter extends AbstractTokenGranter {    private static final String GRANT_TYPE = "captcha";    private final AuthenticationManager authenticationManager;    private RedisTemplate redisTemplate;    private CaptchaService captchaService;    private String captchaType;    public CaptchaTokenGranter(AuthenticationManager authenticationManager,        AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,        OAuth2RequestFactory requestFactory, RedisTemplate redisTemplate, CaptchaService captchaService,        String captchaType) {        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);        this.redisTemplate = redisTemplate;        this.captchaService = captchaService;        this.captchaType = captchaType;    }    protected CaptchaTokenGranter(AuthenticationManager authenticationManager,        AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,        OAuth2RequestFactory requestFactory, String grantType) {        super(tokenServices, clientDetailsService, requestFactory, grantType);        this.authenticationManager = authenticationManager;    }    @Override    protected OAuth2Authentication getoAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());        // 获取验证码类型        String captchaType = parameters.get(CaptchaConstant.CAPTCHA_TYPE);        // 确定传入的验证码类型是否与系统配置一致?        if (!StringUtils.isEmpty(captchaType) && !captchaType.equals(this.captchaType)) {            throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_CAPTCHA_TYPE.getMsg());        }        if (CaptchaConstant.IMAGE_CAPTCHA.equalsIgnoreCase(captchaType)) {            // 图片验证码验证            String captchaKey = parameters.get(CaptchaConstant.CAPTCHA_KEY);            String captchaCode = parameters.get(CaptchaConstant.CAPTCHA_CODE);            // 获取验证码            String redisCode = (String)redisTemplate.opsForValue().get(CaptchaConstant.IMAGE_CAPTCHA_KEY + captchaKey);            // 判断验证码            if (captchaCode == null || !captchaCode.equalsIgnoreCase(redisCode)) {                throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_CAPTCHA.getMsg());            }        } else {            // 滑动验证码验证            String captchaVerification = parameters.get(CaptchaConstant.CAPTCHA_VERIFICATION);            String slidingCaptchaType = parameters.get(CaptchaConstant.SLIDING_CAPTCHA_TYPE);            CaptchaVO captchaVO = new CaptchaVO();            captchaVO.setCaptchaVerification(captchaVerification);            captchaVO.setCaptchaType(slidingCaptchaType);            ResponseModel responseModel = captchaService.verification(captchaVO);            if (null == responseModel || !RepCodeEnum.SUCCESS.getCode().equals(responseModel.getRepCode())) {                throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_CAPTCHA.getMsg());            }        }        String username = parameters.get(TokenConstant.USER_NAME);        String password = parameters.get(TokenConstant.PASSWORD);        // Protect from downstream leaks of password        parameters.remove(TokenConstant.PASSWORD);        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);        try {            userAuth = authenticationManager.authenticate(userAuth);        } catch (AccountStatusException | BadCredentialsException ase) {            // covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)            throw new InvalidGrantException(ase.getMessage());        }        // If the username/password are wrong the spec says we should send 400/invalid grant        if (userAuth == null || !userAuth.isAuthenticated()) {            throw new InvalidGrantException("Could not authenticate user: " + username);        }        OAuth2Request storedoauth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);        return new OAuth2Authentication(storedoauth2Request, userAuth);    }}
5、gitegg-Gitegoauthcontroller在oauth中增加获取验证码的方法
    @Value("${captcha.type}")    private String captchaType;    @ApiOperation("获取系统配置的验证码类型")    @GetMapping("/captcha/type")    public Result captchaType() {        return Result.data(captchaType);    }    @ApiOperation("生成滑动验证码")    @PostMapping("/captcha")    public Result captcha(@RequestBody CaptchaVO captchaVO) {        ResponseModel responseModel = captchaService.get(captchaVO);        return Result.data(responseModel);    }    @ApiOperation("滑动验证码验证")    @PostMapping("/captcha/check")    public Result captchaCheck(@RequestBody CaptchaVO captchaVO) {        ResponseModel responseModel = captchaService.check(captchaVO);        return Result.data(responseModel);    }    @ApiOperation("生成图片验证码")    @RequestMapping("/captcha/image")    public Result captchaImage() {        SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 5);        String captchaCode = specCaptcha.text().toLowerCase();        String captchaKey = UUID.randomUUID().toString();        // 存入redis并将过期时间设置为5分钟        redisTemplate.opsForValue().set(CaptchaConstant.IMAGE_CAPTCHA_KEY + captchaKey, captchaCode, GitEggConstant.Number.FIVE,            TimeUnit.MINUTES);        ImageCaptcha imageCaptcha = new ImageCaptcha();        imageCaptcha.setCaptchaKey(captchaKey);        imageCaptcha.setCaptchaImage(specCaptcha.tobase64();        // 将key和base64返回到前端        return Result.data(imageCaptcha);    }
6、将滑动验证码提供的前端页面verifition目录copy转到我们前端工程的components目录,修改login.vue,增加验证码
<a-row :gutter="0"                 v-if="loginCaptchaType === 'image' && grantType !== 'password'">            <a-col :span="14">              <a-form-item>                <a-input v-decorator="['captchaCode', validatorRules.captchaCode]"                         size="large"                         type="text"                         :placeholder="$t('user.verification-code.required')">                  <a-icon v-if="inputCodeContent == verifiedCode"                          slot="prefix"                          type="safety-certificate"                          :style="{ fontSize: '20px', color: '#1890ff' }" />                  <a-icon v-else                          slot="prefix"                          type="safety-certificate"                          :style="{ fontSize: '20px', color: '#1890ff' }" />                </a-input>              </a-form-item>            </a-col>            <a-col :span="10">              <img :src="captchaImage"                   class="v-code-img"                   @click="refreshImageCode">            </a-col>          </a-row>
    <Verify @success="verifySuccess"            :mode="'pop'"            :captchaType="slidingCaptchaType"            :imgSize="{ width: '330px', height: '155px' }"            ref="verify"></Verify>
      grantType: 'password',      loginCaptchaType: 'sliding',      slidingCaptchaType: 'blockPuzzle',      loginErrorMsg: 用户名或密码错误,      captchaKey: '',      captchaCode: '',      captchaImage: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAICRAEAOw==',      inputCodeContent: '',      inputCodeNull: true
methods: {    ...mapActions(['Login', 'Logout']),    // handler    handleUsernameOrEmail (rule, value, callback) {      const { state } = this      const regex = /^([a-zA-Z0-9_-])+@([a-zA-(\).[a-zA-{2,3},Z0-9_-2})$/      if (regex.test(value)) {        state.loginType = 0      } else {        state.loginType = 1      }      callback()    },    // 二次验证滑动验证码,并提交登录    verifySuccess (params) {      // params 返回的二次验证参数, 与登录参数一起返回登录接口,方便后台进行二次验证      const {        form: { validateFields },        state,        customActiveKey,        Login      } = this      state.loginBtn = true      const validateFieldsKey = customActiveKey === 'tab_account' ? ['username', 'password', 'captchaCode', 'captchaKey'] : ['phoneNumber', 'captcha', 'captchaCode', 'captchaKey']      validateFields(validateFieldsKey, { force: true }, (err, values) => {        if (!err) {          const loginParams = { ...values }          delete loginParams.username          loginParams[!err) {          const loginParams = { ...values }          delete loginParams.username          loginParams[!state.loginType ? 'email' : 'username'] = values.username          loginParams.client_id = process.env.VUE_APP_CLIENT_ID          loginParams.client_secret = process.env.VUE_APP_CLIENT_SECRET          if (this.grantType === 'password' && customActiveKey === 'tab_account') {              loginParams.grant_type = 'password'              loginParams.password = values.password          } else {            if (customActiveKey === 'tab_account') {              loginParams.grant_type = 'captcha'              loginParams.password = values.password            } else {              loginParams.grant_type = 'sms_captcha'              loginParams.phone_number = values.phoneNumber              loginParams.code = values.captcha              loginParams.smsCode = 'aliLoginCode'            }            // loginParams.password = md5(values.password)            // 判断是图片验证码还是滑动验证码            if (this.loginCaptchaType === 'sliding') {              loginParams.captcha_type = 'sliding'              loginParams.sliding_type = this.slidingCaptchaType              loginParams.captcha_verification = params.captchaVerification            } else if (this.loginCaptchaType === 'image') {              loginParams.captcha_type = 'image'              loginParams.captcha_key = this.captchaKey              loginParams.captcha_code = values.captchaCode            }          }          Login(loginParams)            .then((res) => this.loginSuccess(res))            .catch(err => this.requestFailed(err))            .finally(() => {              state.loginBtn = false            })        } else {          setTimeout(() => {            state.loginBtn = false          }, 600)        }      })    },    // 验证滑动验证码    captchaVerify (e) {      e.preventDefault()      const {        form: { validateFields },        state,        customActiveKey      } = this      state.loginBtn = true      const validateFieldsKey = customActiveKey === 'tab_account' ? ['username', 'password', 'vcode', 'verkey'] : ['phoneNumber', 'captcha', 'vcode', 'verkey']      validateFields(validateFieldsKey, { force: true }, (err, values) => {        if (!err) {          if (this.grantType === 'password') {            this.verifySuccess()          } else {            if (this.loginCaptchaType === 'sliding') {              this.$refs.verify.show()            } else {              this.verifySuccess()            }          }        } else {          setTimeout(() => {            state.loginBtn = false          }, 600)        }      })    },    queryCaptchaType () {      getCaptchaType().then(res => {        this.loginCaptchaType = res.data        if (this.loginCaptchaType === 'image') {          this.refreshImageCode()        }      })    },    refreshImageCode () {      getImageCaptcha().then(res => {        const data = res.data        this.captchaKey = data.captchaKey        this.captchaImage = data.captchaImage      })    },    handleTabClick (key) {      this.customActiveKey = key      // this.form.resetFields()    },    handleSubmit (e) {      e.preventDefault()    },    getCaptcha (e) {      e.preventDefault()      const { form: { validateFields }, state } = this      validateFields(['phoneNumber'], { force: true }, (err, values) => {        if (!err) {          state.smsSendBtn = true          const interval = window.setInterval(() => {            if (state.time-- <= 0) {              state.time = 60              state.smsSendBtn = false              window.clearInterval(interval)            }          }, 1000)          const hide = this.$message.loading(在发送验证码时..', 0)          getSmsCaptcha({ phoneNumber: values.phoneNumber, smsCode: 'aliLoginCode' }).then(res => {            setTimeout(hide, 2500)            this.$notification['success']({              message: '提示',              description: 获得验证码成功,您的验证码为:' + res.result.captcha,              duration: 8            })          }).catch(err => {            setTimeout(hide, 1)            clearInterval(interval)            state.time = 60            state.smsSendBtn = false            this.requestFailed(err)          })        }      })    },    stepCaptchaSuccess () {      this.loginSuccess()    },    stepCaptchaCancel () {      this.Logout().then(() => {        this.loginBtn = false        this.stepCaptchaVisible = false      })    },    loginSuccess (res) {     // 判断密码是否记住      const rememberMe = this.form.getFieldValue('rememberMe')      const username = this.form.getFieldValue('username')      const password = this.form.getFieldValue('password')      if (rememberMe && username !== '' && password !== '') {          storage.set(process.env.VUE_APP_TENANT_ID + '-' + process.env.VUE_APP_CLIENT_ID + '-username', username, 60 * 60 * 24 * 7 * 1000)          storage.set(process.env.VUE_APP_TENANT_ID + '-' + process.env.VUE_APP_CLIENT_ID + '-password', password, 60 * 60 * 24 * 7 * 1000)          storage.set(process.env.VUE_APP_TENANT_ID + '-' + process.env.VUE_APP_CLIENT_ID + '-rememberMe', true, 60 * 60 * 24 * 7 * 1000)      } else {         storage.remove(process.env.VUE_APP_TENANT_ID + '-' + process.env.VUE_APP_CLIENT_ID + '-username')         storage.remove(process.env.VUE_APP_TENANT_ID + '-' + process.env.VUE_APP_CLIENT_ID + '-password')         storage.remove(process.env.VUE_APP_TENANT_ID + '-' + process.env.VUE_APP_CLIENT_ID + '-rememberMe')      }      this.$router.push({ path: '/' })      // 延迟 1 第二秒显示欢迎信息      setTimeout(() => {        this.$notification.success({          message: '欢迎',          description: `${timeFix()},欢迎回来`        })      }, 1000)      this.isLoginError = false    },    requestFailed (err) {      this.isLoginError = true      if (err && err.code === 427) {        // 超过最大限值的密码错误次数,请选择验证码模式登录        if (this.customActiveKey === 'tab_account') {            this.grantType = 'captcha'        } else {            this.grantType = 'sms_captcha'        }        this.loginErrorMsg = err.msg        if (this.loginCaptchaType === 'sliding') {            this.$refs.verify.show()        }      } else if (err) {            this.loginErrorMsg = err.msg      }    }  }
7、在Nacos中添加配置项,默认使用行为验证码
#captchahaha配置验证码:  #验证码的类型 sliding: 滑动验证码 image: 图片验证码  type: sliding
8、登录效果

登录页安全验证尝试过多,账号被锁

使用行为验证码的主要优点是:
  1. 防止机器人或自动化软件攻击:使用行为验证码可以有效地防止机器人或自动化软件攻击,因为这些程序无法模拟用户的真实行为。

  2. 无需人工干预:行为验证码可自动检测用户的行为,无需输入任何额外信息或完成任务。

  3. 更好的用户体验:行为验证码比传统的验证码更容易完成,减少了用户的不便和不愉快的体验。

  4. 更安全:由于其生成的令牌与用户的特定设备相关,行为验证码可以有效地防止“中间人”攻击。

  5. 更难破解:行为验证码采用复杂的算法和人工智能技术,使得即使攻击者获得了验证码令牌,也很难重现用户的行为模式,使攻击更加困难。