随着近年来技术的发展,人们对系统安全和用户体验的要求越来越高,大多数网站系统逐渐使用行为验证码来取代图片验证码。行为验证码是指通过用户行为验证用户身份的验证码,如滑动拼图、识别图片中的特定物品等。
行为验证码的重要性在于,它可以有效地防止机器人和恶意程序恶意攻击网站或应用程序,刷流量,撞击图书馆,从而确保用户和网站的安全。与传统的图形验证码相比,行为验证码更难破解,用户操作更方便。
此外,行为验证码还可以提高用户体验,减轻用户操作负担,提高网站或应用程序的可用性和用户保留率。因此,使用行为验证码是保护用户和网站安全的重要手段,也是提高用户体验的有效途径。
以下是几种常用的 Java 开源验证码库:
JCaptcha:Java下的开源验证码库采用Java Servlet 基于Apache标准的Jakarta项目Struts和Ant应用程序框架。
Kaptcha:Java开发的验证码库可生成随机数字、字母、汉字等验证码。
SimpleCaptcha:开源验证码库采用Java语言编写,支持生成数字、字母和简单的数学操作验证码。
Google Authenticator:Google发布的开源身份验证器可生成一次性密码,常用于二步验证。
AJ-Captcha:在Web应用程序中,用于保护用户免受恶意攻击和垃圾邮件的人机验证码。它是基于JavaScript的验证码,可以在不需要重新加载页面或使用外部API的情况下生成和验证验证码。它采用了多种技术,包括画布渲染和随机化,使得攻击者难以识别和破解验证码。 AJ-Captcha易于集成,适应不同的屏幕大小,并能定制外观和行为。
EasyCaptcha:它是一种简单易用的验证码生成器,用于添加人类验证,防止恶意自动化。它由PHP编写,提供基于文本、数字、数学等方法的多种验证码类型。EasyCaptcha支持自定义风格和语言,具有良好的可扩展性。它可用于各种Web应用程序,如登录页面、注册页面、联系表等,以提高网站的安全性。
此外,还有一些商业验证码库,如Turing Image、Secure Image等。
GitEgg-Cloud集成了开源行为验证码组件和图片验证码,并将可配置项添加到系统中,以选择具体使用的验证码。
- AJ-Captcha:行为验证码
- EasyCaptcha: 图片验证码
<!-- 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、登录效果使用行为验证码的主要优点是:防止机器人或自动化软件攻击:使用行为验证码可以有效地防止机器人或自动化软件攻击,因为这些程序无法模拟用户的真实行为。
无需人工干预:行为验证码可自动检测用户的行为,无需输入任何额外信息或完成任务。
更好的用户体验:行为验证码比传统的验证码更容易完成,减少了用户的不便和不愉快的体验。
更安全:由于其生成的令牌与用户的特定设备相关,行为验证码可以有效地防止“中间人”攻击。
更难破解:行为验证码采用复杂的算法和人工智能技术,使得即使攻击者获得了验证码令牌,也很难重现用户的行为模式,使攻击更加困难。