前后端分離開發(fā)中的驗證碼問題

1.環(huán)境和工具

  • 后端:JDK 11、Tomcat 9、 maven 3.6.2、MySQL 5.7
  • 前端:node.js 12.13.0、npm 6.12.0、VueCLI構(gòu)建的SPA、axios網(wǎng)絡(luò)請求工具

2.方案描述

  • 后端通過工具方法生成隨機(jī)字符串
  • 封裝使用該字符串在緩沖區(qū)生成指定大小圖片的方法(可以加干擾線、噪點等),本文主要說明問題的解決流程,所以不對此展開過多探討
  • 同時需要將該字符串存入HttpSession對象中,其SessionId通過response的響應(yīng)頭Access-Token存入,和驗證碼一起返回給客戶端
  • Servlet處理請求,將該圖片通過response對象的輸出流返回給客戶端
  • 客戶端在頁面加載完畢,通過請求可以拿到該驗證碼圖片的字節(jié)流,通過blob的處理,生成一個URL,作為圖片的src屬性的值,顯示驗證碼圖片,同時可以在請求的回調(diào)函數(shù)中,通過response的headers取到access-token的值,就是和服務(wù)器端對應(yīng)的那個JSESSIONID的值(傳統(tǒng)開發(fā)中,一般記錄在cookie中),但是通過chrome工具可以看到,是個HTTPOnly,所以拿不到。
  • 加上時間戳,可以實現(xiàn)點擊圖片換一張的效果
  • 點擊“登錄”按鈕,將用戶輸入的賬號、密碼、驗證碼封裝成傳輸對象,一起傳到后臺,同時在請求頭里面加入之前取到的那個JSESSIONID的值。

3.注意點

  • 過濾器需要添加ACCESS-TOKEN的支持
  • 后端的response需要通過響應(yīng)頭將這個session的id返回給客戶端,客戶端在回調(diào)函數(shù)可以拿到響應(yīng)頭,取出這個值綁定給一個變量
  • 客戶端在第二次發(fā)起登錄請求的時候,記得在請求頭里面加入這個值,到了登錄的請求處理代碼里面,通過自定義的Session監(jiān)聽方法,可以通過session的id拿到前一次的那個session對象,從而得到之前生成的正確的驗證碼的值,把它和剛登錄的時候傳到后端的登錄用戶的DTO對象里的驗證碼比對,判斷要不要繼續(xù)下一步登陸驗證。

4.關(guān)鍵代碼

  • 生成隨機(jī)字符串
import java.util.Random;

/**
 * @author mq_xu
 * @ClassName StringUtil
 * @Description 字符串工具類
 * @Date 2019/11/14
 * @Version 1.0
 **/
public class StringUtil {

   private final static int MAX = 4;

    public static String getRandomString() {
        StringBuilder stringBuilder = new StringBuilder();
        Random random = new Random();
        int index;
        //生成四位隨機(jī)字符
        for (int i = 0; i < MAX; i++) {
            //隨機(jī)生成0、1、2三個整數(shù),代表數(shù)字字符、大寫字母、小寫字母,保證驗證碼的組成比較正態(tài)隨機(jī)
            index = random.nextInt(3);
            //調(diào)用本類封裝的私有方法,根據(jù)編號獲得對應(yīng)的字符
            char result = getChar(index);
            //追加到可變長字符串
            stringBuilder.append(result);
        }
        return stringBuilder.toString();
    }

    private static char getChar(int item) {
        //數(shù)字字符范圍
        int digitalBound = 10;
        //字符范圍
        int charBound = 26;
        Random random = new Random();
        int index;
        char c;
        //根據(jù)調(diào)用時候的三個選項,生成數(shù)字、大寫字母、小寫字母三種不同的字符
        if (item == 0) {
            index = random.nextInt(digitalBound);
            c = (char) ('0' + index);
        } else if (item == 1) {
            index = random.nextInt(charBound);
            c = (char) ('A' + index);
        } else {
            index = random.nextInt(charBound);
            c = (char) ('a' + index);
        }
        return c;
    }
}
  • 生成驗證碼圖片
import java.awt.*;
import java.awt.image.BufferedImage;

/**
 * @author mq_xu
 * @ClassName ImageUtil
 * @Description 驗證碼生成
 * @Date 2019/11/18
 * @Version 1.0
 **/
public class ImageUtil {
    public static BufferedImage getImage(int width, int height, String content) {
        //創(chuàng)建指定大小和圖片模式的緩沖圖片對象
        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        //繪圖對象
        Graphics2D graphics = (Graphics2D) img.getGraphics();
        //設(shè)置顏色
        graphics.setColor(new Color(68, 134, 49));
        //繪制填充矩形
        graphics.fillRect(0, 0, width, height);
        //設(shè)置畫筆顏色
        graphics.setPaint(new Color(60, 63, 65));
        //設(shè)置字體
        Font font = new Font("微軟雅黑", Font.BOLD, 40);
        graphics.setFont(font);
        //在指定位置繪制字符串
        graphics.drawString(content, width / 3, height / 2);
        return img;
    }
}
  • 處理驗證碼請求
import com.scs.web.blog.util.ImageUtil;
import com.scs.web.blog.util.StringUtil;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;

/**
 * @author mq_xu
 * @ClassName CodeController
 * @Description 驗證碼請求接口
 * @Date 2019/11/14
 * @Version 1.0
 **/
@WebServlet(urlPatterns = {"/api/code"})
public class CodeController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取隨機(jī)驗證碼
        String code = StringUtil.getRandomString();
        //存入session
        HttpSession session = req.getSession();
        session.setAttribute("code", code);
        //將sessionId通過響應(yīng)頭傳回客戶端
        resp.setHeader("Access-Token",session.getId());
         //調(diào)過生成驗證碼圖片的方法
        BufferedImage img = ImageUtil.getImage(200, 100, code);
        //設(shè)置resp的響應(yīng)內(nèi)容類型,前端將是blob
        resp.setContentType("image/jpg");
        //將圖片通過輸出流返回給客戶端
        OutputStream out = resp.getOutputStream();
        ImageIO.write(img, "jpg", out);
        out.close();
    }
}

  • 處理登錄請求的核心代碼(因為請求地址的不同,此處做了一些封裝,沒有直接寫在doPost()方法里)
        BufferedReader reader = req.getReader();
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
        }
        logger.info("登錄用戶信息:" + stringBuilder.toString());
        Gson gson = new GsonBuilder().create();
        UserDto userDto = gson.fromJson(stringBuilder.toString(), UserDto.class);
        //客戶端輸入的驗證碼
        String inputCode = userDto.getCode().trim();
        //取得客戶端請求頭里帶來的token
        String sessionId = req.getHeader("Access-Token");
        //從自定義的監(jiān)聽代碼中取得之前的session對象
        MySessionContext myc = MySessionContext.getInstance();
        HttpSession session = myc.getSession(sessionId);
        //取得當(dāng)時存入的驗證碼
        String correctCode = session.getAttribute("code").toString();
        PrintWriter out = resp.getWriter();
        //忽略大小寫比對
        if (inputCode.equalsIgnoreCase(correctCode)) {
            //驗證碼正確,進(jìn)入登錄業(yè)務(wù)邏輯調(diào)用
            Result result = userService.signIn(userDto);
        } else {
            //驗證碼錯誤,直接將錯誤信息返回給客戶端,不要繼續(xù)登錄流程了
            Result result = Result.failure(ResultCode.USER_VERIFY_CODE_ERROR);
        }
        out.print(gson.toJson(result));
        out.close();
  • 后端跨域處理
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author mq_xu
 * @ClassName CORSFilter
 * @Description 跨域過濾器類
 * @Date 2019/10/3
 * @Version 1.0
 **/
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
    private static Logger logger = LoggerFactory.getLogger(CorsFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        //允許客戶端請求頭攜帶
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, Content-Type,Access-Token");
        //允許給客戶端響應(yīng)頭攜帶
        response.setHeader("Access-Control-Expose-Headers", "Access-Token");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) {
        logger.info("跨域過濾器初始化");
    }

    @Override
    public void destroy() {
        logger.info("跨域過濾器銷毀");
    }

}
  • 前端登錄頁面(省略CSS樣式)
<template>
    <div id="bg">
        <router-link to="/">首頁</router-link>
        <div class="login-box">
            <form class="login-form">
                <input type="text" v-model="userDto.mobile" id="mobile" />
                <input type="password" v-model="userDto.password" />
                <div class="code-box">
                    <input type="text" v-model="userDto.code" class="code" />
                    <img class="verify" @click.prevent="refresh" ref="codeImg" />
                </div>
                <input type="button" class="btn btn-lg dark-fill" value="登錄" @click="signIn(userDto)" />
                <router-link to="/sign-up">沒有賬號?去注冊</router-link>
            </form>
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            userDto: {
                mobile: '',
                password: '',
                code: ''
            },
            token: ''
        };
    },
    created() {
        this.axios.get(this.GLOBAL.baseUrl + '/code', { responseType: 'blob' }).then(res => {
            // console.log(res);
            var img = this.$refs.codeImg;
            let url = window.URL.createObjectURL(res.data);
            img.src = url;
            console.log(res.headers);
            //取得后臺通過響應(yīng)頭返回的sessionId的值
            this.token = res.headers['access-token'];
            console.log(this.token);
        });
    },
    methods: {
        signIn(userDto) {
            this.axios({
                method: 'post',
                url: this.GLOBAL.baseUrl + '/user/sign-in',
                data: JSON.stringify(this.userDto),
                headers: {
                    'Access-Token': this.token  //將token放在請求頭帶到后端
                }
            }).then(res => {
                if (res.data.msg === '成功') {
                    alert('登錄成功');
                    localStorage.setItem('user', JSON.stringify(res.data.data));
                    this.$router.push('/');
                } else {
                    alert(res.data.msg);
                    this.userDto.code = '';
                }
            });
        },
        refresh() {
            this.axios.get(this.GLOBAL.baseUrl + '/code', { responseType: 'blob' }).then(res => {
                console.log(res);
                var img = this.$refs.codeImg;
                let url = window.URL.createObjectURL(res.data);
                img.src = url;
            });
        }
    }
};
</script>
<style scoped>
</style>
  • 驗證結(jié)果
    登陸頁面啟動,會先從后端請求一個驗證碼,并且拿到響應(yīng)頭里面的sessionid的值


    前端

    查看請求驗證碼的網(wǎng)絡(luò)請求,發(fā)現(xiàn)響應(yīng)頭里面加入了Access-Token的值


    驗證碼請求的響應(yīng)頭

可以看下后臺的信息,兩個id的值一致


后端

登錄請求的請求頭,可以看到帶著的Access-Token


登錄請求頭
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容