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

随着近几年技术的发展,人们对于系统安全性和用户体验的要求越来越高,大多数网站系统都逐渐采用行为验证码来代替图片验证码。GitEgg-Cloud集成了开源行为验证码组件和图片验证码,并在系统中添加可配置项来选择具体使用哪种验证码。

  • AJ-Captcha:行为验证码
  • EasyCaptcha: 图片验证码
1、在我们的gitegg-platform-bom工程中增加验证码的包依赖
1.2.71.6.2 com.github.anji-plus captcha-spring-boot-starter ${captcha.version} com.github.whvcse easy-captcha ${easy.captcha.version}

2、新建gitegg-platform-captcha工程,用于配置及自定义方法,行为验证码用到缓存是需要自定义实现CaptchaCacheService,自定义类CaptchaCacheServiceRedisImpl:
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-captcha的resources目录新建META-INF.services文件夹,参考resource/META-INF/services中的写法。
com.gitegg.platform.captcha.service.impl.CaptchaCacheServiceRedisImpl

4、在GitEgg-Cloud下的gitegg-oauth中增加CaptchaTokenGranter自定义验证码令牌授权处理类
/** * 验证码模式 */ 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 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 grantif (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-oauth中GitEggOAuthController新增获取验证码的方法
@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到我们前端工程的compoonents目录,修改Login.vue,增加验证码
SpringCloud微服务实战——搭建企业级开发框架(二十四)(集成行为验证码和图片验证码实现登录功能)
文章图片


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-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/ if (regex.test(value)) { state.loginType = 0 } else { state.loginType = 1 } callback() }, // 滑动验证码二次校验并提交登录 verifySuccess (params) { // params 返回的二次验证参数, 和登录参数一起回传给登录接口,方便后台进行二次验证 const { form: { validateFields }, state, customActiveKey, Login } = thisstate.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.usernameloginParams[!state.loginType ? 'email' : 'username'] = values.username loginParams.client_id = process.env.VUE_APP_CLIENT_ID loginParams.client_secret = process.env.VUE_APP_CLIENT_SECRETif (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 } = thisstate.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 = https://www.it610.com/article/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 } = thisvalidateFields(['phoneNumber'], { force: true }, (err, values) => { if (!err) { state.smsSendBtn = trueconst 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中增加配置项,默认使用行为验证码
#验证码配置 captcha: #验证码的类型 sliding: 滑动验证码 image: 图片验证码 type: sliding

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

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

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

源码地址: 【SpringCloud微服务实战——搭建企业级开发框架(二十四)(集成行为验证码和图片验证码实现登录功能)】Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg

    推荐阅读