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

登錄請求頭

