ruoyi-vue3 集成aj-captcha實現(xiàn)滑塊、文字點選驗證碼功能
0. 前言
其實若依的官方文檔中有集成aj-captcha實現(xiàn)滑塊驗證碼的部分,但是一直給的前端示例代碼中都是Vue2的版本,而且后端部分也一直未保持更新。再比如官方文檔在集成aj-captcha后并未實現(xiàn)驗證碼開關的功能。
然后我最近正好在用若依的Vue3版本做東西,正好記錄一下。
0.1 說明
以官方文檔為模板寫的這篇文章,所以中間會穿插官方文檔中的一些文字。
文章中所涉及的截圖、代碼,由于我已經使用 若依框架包名修改器 修改過了,所以包名、模塊名前綴會和原版有出入,但僅限于包名和模塊名。請注意甄別。
本文基于后端RuoYi-Vue 3.8.7
和 前端 RuoYi-Vue3 3.8.7
官方文檔在集成后并沒有實現(xiàn)驗證碼開關功能,本文會進行實現(xiàn)。
集成以AJ-Captcha文字點選驗證碼為例,不需要鍵盤手動輸入,極大優(yōu)化了傳統(tǒng)驗證碼用戶體驗不佳的問題。目前對外提供兩種類型的驗證碼,其中包含滑動拼圖、文字點選。
1. 后端部分
1.1 添加依賴
在 ruoyi-framework
模塊中的 pom.xml
添加以下依賴:
<!-- 滑塊驗證碼 --> <dependency> <groupId>com.github.anji-plus</groupId> <artifactId>captcha-spring-boot-starter</artifactId> <version>1.2.7</version> </dependency>
刪除原本的 kaptcha
驗證碼依賴:
<!-- 驗證碼 --> <dependency> <groupId>pro.fessional</groupId> <artifactId>kaptcha</artifactId> <exclusions> <exclusion> <artifactId>servlet-api</artifactId> <groupId>javax.servlet</groupId> </exclusion> </exclusions> </dependency>
最終 pom.xml
截圖:
1.2. 修改 application.yml
修改application.yml,加入aj-captcha相關配置:
(我的項目使用的是文字點選,如需要使用滑塊,type
設置為 blockPuzzle
即可)
# 滑塊驗證碼 aj: captcha: # 緩存類型 cache-type: redis # blockPuzzle 滑塊 clickWord 文字點選 default默認兩者都實例化 type: clickWord # 右下角顯示字 water-mark: B站、抖音同名搜索七維大腦 # 校驗滑動拼圖允許誤差偏移量(默認5像素) slip-offset: 5 # aes加密坐標開啟或者禁用(true|false) aes-status: true # 滑動干擾項(0/1/2) interference-options: 2
1.3. 新增 CaptchaRedisService 類
在 ruoyi-framework
模塊下,com.ruoyi.framework.web.service
包下創(chuàng)建CaptchaRedisService.java
類,內容如下:
(請復制粘貼后注意修改包路徑為自己項目真實路徑)
package xyz.ytxy.framework.web.service; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import com.anji.captcha.service.CaptchaCacheService; /** * 自定義redis驗證碼緩存實現(xiàn)類 * * @author ruoyi */ public class CaptchaRedisService implements CaptchaCacheService { @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 Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); } @Override public void delete(String key) { stringRedisTemplate.delete(key); } @Override public String get(String key) { return stringRedisTemplate.opsForValue().get(key); } @Override public Long increment(String key, long val) { return stringRedisTemplate.opsForValue().increment(key, val); } @Override public String type() { return "redis"; } }
1.4. 添加必須文件
在ruoyi-admin
模塊下,找到 resources
目錄
在 resources
目錄找到 META-INF
目錄在 META-INF
目錄中新建 services
文件夾
在 services
文件夾中新建 com.anji.captcha.service.CaptchaCacheService
文件(注意是文件)
在 com.anji.captcha.service.CaptchaCacheService
文件中輸入 xxx.xxx.framework.web.service.CaptchaRedisService
(也就是剛剛創(chuàng)建的CaptchaRedisService類的真實路徑)
1.5. 移除不需要的類
ruoyi-admin
模塊下com.ruoyi.web.controller.common.CaptchaController.java
ruoyi-framework
模塊下com.ruoyi.framework.config.CaptchaConfig.java
ruoyi-framework
模塊下com.ruoyi.framework.config.KaptchaTextCreator.java
1.6. 修改登錄方法
修改 ruoyi-admin
模塊下 com.ruoyi.web.controller.system.SysLoginController.java
類中的 login
方法:
/** * 登錄方法 * * @param loginBody 登錄信息 * @return 結果 */ @PostMapping("/login") public AjaxResult login(@RequestBody LoginBody loginBody) { AjaxResult ajax = AjaxResult.success(); // 生成令牌 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode()); ajax.put(Constants.TOKEN, token); return ajax; }
修改后生成令牌這一步比原版少了 loginBody.getUuid()
參數(shù)。
修改 ruoyi-framework
模塊下的com.ruoyi.framework.web.service.SysLoginService.java
類:
package xyz.ytxy.framework.web.service; import javax.annotation.Resource; import com.anji.captcha.model.common.ResponseModel; import com.anji.captcha.model.vo.CaptchaVO; import com.anji.captcha.service.CaptchaService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import xyz.ytxy.common.constant.CacheConstants; import xyz.ytxy.common.constant.Constants; import xyz.ytxy.common.constant.UserConstants; import xyz.ytxy.common.core.domain.entity.SysUser; import xyz.ytxy.common.core.domain.model.LoginUser; import xyz.ytxy.common.core.redis.RedisCache; import xyz.ytxy.common.exception.ServiceException; import xyz.ytxy.common.exception.user.BlackListException; import xyz.ytxy.common.exception.user.CaptchaException; import xyz.ytxy.common.exception.user.CaptchaExpireException; import xyz.ytxy.common.exception.user.UserNotExistsException; import xyz.ytxy.common.exception.user.UserPasswordNotMatchException; import xyz.ytxy.common.utils.DateUtils; import xyz.ytxy.common.utils.MessageUtils; import xyz.ytxy.common.utils.StringUtils; import xyz.ytxy.common.utils.ip.IpUtils; import xyz.ytxy.framework.manager.AsyncManager; import xyz.ytxy.framework.manager.factory.AsyncFactory; import xyz.ytxy.framework.security.context.AuthenticationContextHolder; import xyz.ytxy.system.service.ISysConfigService; import xyz.ytxy.system.service.ISysUserService; /** * 登錄校驗方法 * * @author ruoyi */ @Component public class SysLoginService { @Autowired private TokenService tokenService; @Resource private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Autowired private ISysUserService userService; @Autowired private ISysConfigService configService; @Autowired @Lazy private CaptchaService captchaService; /** * 登錄驗證 * * @param username 用戶名 * @param password 密碼 * @param code 驗證碼 * @return 結果 */ public String login(String username, String password, String code) { // 驗證碼校驗 validateCaptcha(username, code); // 登錄前置校驗 loginPreCheck(username, password); // 用戶驗證 Authentication authentication = null; try { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); AuthenticationContextHolder.setContext(authenticationToken); // 該方法會去調用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager.authenticate(authenticationToken); } catch (Exception e) { if (e instanceof BadCredentialsException) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } else { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); throw new ServiceException(e.getMessage()); } } finally { AuthenticationContextHolder.clearContext(); } AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); recordLoginInfo(loginUser.getUserId()); // 生成token return tokenService.createToken(loginUser); } /** * 校驗驗證碼 * * @param username 用戶名 * @param code 驗證碼 * @return 結果 */ public void validateCaptcha(String username, String code) { boolean captchaEnabled = configService.selectCaptchaEnabled(); if (captchaEnabled) { CaptchaVO captchaVO = new CaptchaVO(); captchaVO.setCaptchaVerification(code); ResponseModel response = captchaService.verification(captchaVO); if (!response.isSuccess()) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); throw new CaptchaException(); } } } /** * 登錄前置校驗 * @param username 用戶名 * @param password 用戶密碼 */ public void loginPreCheck(String username, String password) { // 用戶名或密碼為空 錯誤 if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); throw new UserNotExistsException(); } // 密碼如果不在指定范圍內 錯誤 if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } // 用戶名不在指定范圍內 錯誤 if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } // IP黑名單校驗 String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); throw new BlackListException(); } } /** * 記錄登錄信息 * * @param userId 用戶ID */ public void recordLoginInfo(Long userId) { SysUser sysUser = new SysUser(); sysUser.setUserId(userId); sysUser.setLoginIp(IpUtils.getIpAddr()); sysUser.setLoginDate(DateUtils.getNowDate()); userService.updateUserProfile(sysUser); } }
login
方法比原版少了uuid
的參數(shù)validateCaptcha
方法比原版少了uuid
的參數(shù),方法內容更改為aj-captcha的驗證方式- 其他內容未更改
這地方如果直接替換官方文檔中的代碼會造成部分新功能缺失。所以這里直接替換我提供的代碼即可。(注意替換后將包名改為你實際的包名)
1.7. 新增驗證碼開關獲取接口
在 ruoyi-admin
模塊下的 com.ruoyi.web.controller.common
包新增 CaptchaEnabledController.java
:
(注意將包名改為你實際的包名)
package xyz.ytxy.web.controller.common; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import xyz.ytxy.common.core.domain.AjaxResult; import xyz.ytxy.system.service.ISysConfigService; /** * 驗證碼操作處理 * * @author B站、抖音搜索:七維大腦 點個關注唄 */ @RestController public class CaptchaEnabledController { @Autowired private ISysConfigService configService; /** * 獲取驗證碼開關 */ @GetMapping("/captchaEnabled") public AjaxResult captchaEnabled() { AjaxResult ajax = AjaxResult.success(); boolean captchaEnabled = configService.selectCaptchaEnabled(); ajax.put("captchaEnabled", captchaEnabled); return ajax; } }
1.8. 允許匿名訪問
在ruoyi-framework
模塊下的 com.ruoyi.framework.config
包下找到 SecurityConfig.java
類,修改以下內容:
原版:
// 對于登錄login 注冊register 驗證碼captchaImage 允許匿名訪問 .antMatchers("/login", "/register", "/captchaImage").permitAll()
修改為:
// 對于登錄login 注冊register 滑塊驗證碼/captcha/get /captcha/check 獲取驗證碼開關 /captchaEnabled 允許匿名訪問 .antMatchers("/login", "/register", "/captcha/get", "/captcha/check", "/captchaEnabled").permitAll()
2. 前端部分(Vue3)
2.1. 新增依賴 crypto-js
在 package.json
的 "dependencies"
中新增 "crypto-js": "4.1.1"
:
新增后重新 install
,比如我用的pnpm
,直接執(zhí)行:pnpm install --registry=https://registry.npmmirror.com
2.2. 新增 Verifition 組件
此部分代碼我放到了阿里云盤:https://www.alipan.com/s/4hEbavUC4Np
下載后粘貼到 src/components
目錄下:
2.3. 修改login.js
import request from '@/utils/request' // 登錄方法 export function login(username, password, code) { const data = { username, password, code } return request({ url: '/login', headers: { isToken: false, repeatSubmit: false }, method: 'post', data: data }) } // 注冊方法 export function register(data) { return request({ url: '/register', headers: { isToken: false }, method: 'post', data: data }) } // 獲取用戶詳細信息 export function getInfo() { return request({ url: '/getInfo', method: 'get' }) } // 退出方法 export function logout() { return request({ url: '/logout', method: 'post' }) } // 獲取驗證碼開關 export function isCaptchaEnabled() { return request({ url: '/captchaEnabled', method: 'get' }) }
- 修改了
login
函數(shù),去掉了uuid
參數(shù) - 刪除了獲取驗證碼函數(shù)
getCodeImg
- 新增了獲取驗證碼開關函數(shù)
isCaptchaEnabled
2.4. 修改 user.js
刪除 uuid
參數(shù) :
// 登錄 login(userInfo) { const username = userInfo.username.trim() const password = userInfo.password const code = userInfo.code return new Promise((resolve, reject) => { login(username, password, code).then(res => { setToken(res.token) this.token = res.token resolve() }).catch(error => { reject(error) }) }) },
2.5. 修改login.vue
修改內容較多,建議直接替換再修改:
<template> <div class="login"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <h3 class="title">若依后臺管理系統(tǒng)</h3> <el-form-item prop="username"> <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="賬號" > <template #prefix> <svg-icon icon-class="user" class="el-input__icon input-icon"/> </template> </el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密碼" @keyup.enter="handleLogin" > <template #prefix> <svg-icon icon-class="password" class="el-input__icon input-icon"/> </template> </el-input> </el-form-item> <Verify @success="capctchaCheckSuccess" :mode="'pop'" :captchaType="'clickWord'" :imgSize="{ width: '330px', height: '155px' }" ref="verify" v-if="captchaEnabled" ></Verify> <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">記住密碼</el-checkbox> <el-form-item style="width:100%;"> <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin" > <span v-if="!loading">登 錄</span> <span v-else>登 錄 中...</span> </el-button> <div style="float: right;" v-if="register"> <router-link class="link-type" :to="'/register'">立即注冊</router-link> </div> </el-form-item> </el-form> <!-- 底部 --> <div class="el-login-footer"> <span>Copyright ? 2018-2023 ruoyi.vip All Rights Reserved.</span> </div> </div> </template> <script setup> import Cookies from "js-cookie"; import {encrypt, decrypt} from "@/utils/jsencrypt"; import useUserStore from '@/store/modules/user' import Verify from "@/components/Verifition/Verify"; import {isCaptchaEnabled} from "@/api/login"; const userStore = useUserStore() const route = useRoute(); const router = useRouter(); const {proxy} = getCurrentInstance(); const loginForm = ref({ username: "admin", password: "admin123", rememberMe: false, code: "" }); const loginRules = { username: [{required: true, trigger: "blur", message: "請輸入您的賬號"}], password: [{required: true, trigger: "blur", message: "請輸入您的密碼"}] }; const loading = ref(false); // 驗證碼開關 const captchaEnabled = ref(true); // 注冊開關 const register = ref(false); const redirect = ref(undefined); watch(route, (newRoute) => { redirect.value = newRoute.query && newRoute.query.redirect; }, {immediate: true}); function userRouteLogin() { // 調用action的登錄方法 userStore.login(loginForm.value).then(() => { const query = route.query; const otherQueryParams = Object.keys(query).reduce((acc, cur) => { if (cur !== "redirect") { acc[cur] = query[cur]; } return acc; }, {}); router.push({path: redirect.value || "/", query: otherQueryParams}); }).catch(() => { loading.value = false; }); } function handleLogin() { proxy.$refs.loginRef.validate(valid => { if (valid && captchaEnabled.value) { proxy.$refs.verify.show(); } else if (valid && !captchaEnabled.value) { userRouteLogin(); } }); } function getCookie() { const username = Cookies.get("username"); const password = Cookies.get("password"); const rememberMe = Cookies.get("rememberMe"); loginForm.value = { username: username === undefined ? loginForm.value.username : username, password: password === undefined ? loginForm.value.password : decrypt(password), rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) }; } function capctchaCheckSuccess(params) { loginForm.value.code = params.captchaVerification; loading.value = true; // 勾選了需要記住密碼設置在 cookie 中設置記住用戶名和密碼 if (loginForm.value.rememberMe) { Cookies.set("username", loginForm.value.username, {expires: 30}); Cookies.set("password", encrypt(loginForm.value.password), {expires: 30,}); Cookies.set("rememberMe", loginForm.value.rememberMe, {expires: 30}); } else { // 否則移除 Cookies.remove("username"); Cookies.remove("password"); Cookies.remove("rememberMe"); } userRouteLogin(); } // 獲取驗證碼開關 function getCaptchaEnabled() { isCaptchaEnabled().then(res => { captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled; }); } getCookie(); getCaptchaEnabled(); </script> <style lang='scss' scoped> .login { display: flex; justify-content: center; align-items: center; height: 100%; background-image: url("../assets/images/login-background.jpg"); background-size: cover; } .title { margin: 0px auto 30px auto; text-align: center; color: #707070; } .login-form { border-radius: 6px; background: #ffffff; width: 400px; padding: 25px 25px 5px 25px; .el-input { height: 40px; input { height: 40px; } } .input-icon { height: 39px; width: 14px; margin-left: 0px; } } .login-tip { font-size: 13px; text-align: center; color: #bfbfbf; } .el-login-footer { height: 40px; line-height: 40px; position: fixed; bottom: 0; width: 100%; text-align: center; color: #fff; font-family: Arial; font-size: 12px; letter-spacing: 1px; } </style>
2.6. 切換文字點選或滑塊驗證碼
有兩種類型,一種是文字點選,一種是滑塊驗證,那如何切換呢?
2.6.1 后端修改
修改fcat-admin
模塊下 application.yml
中的 aj — type
:
- 填寫
blockPuzzle
為滑塊 - 填寫
clickWord
為文字點選
2.6.2 前端修改
修改 login.vue
:
<Verify @success="capctchaCheckSuccess" :mode="'pop'" :captchaType="'clickWord'" :imgSize="{ width: '330px', height: '155px' }" ref="verify" v-if="captchaEnabled" ></Verify>
修改上述代碼中的 captchaType
填寫blockPuzzle
為滑塊填寫 clickWord
為文字點選
2.7. 成果展示:
默認底圖展示,用于接口異常等情況:
滑塊驗證碼正常顯示截圖:
文字點選驗證碼正常顯示截圖:
到此這篇關于 ruoyi-vue3 集成aj-captcha實現(xiàn)滑塊、文字點選驗證碼的文章就介紹到這了,更多相關ruoyi-vue3 滑塊驗證碼內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Vue開發(fā)項目中如何使用Font Awesome 5
Font Awesome是一套流行的圖標字體庫,我們在實際開發(fā)的過程中會經常遇到需要使用圖標的場景,對于一些常用的圖標,我們可以直接在Font Awesome中找到并且使用,這篇文章主要給大家介紹了關于Vue開發(fā)項目中如何使用Font Awesome5的相關資料,需要的朋友可以參考下2021-11-11