在網(wǎng)站實際應(yīng)用過程中,為了防止網(wǎng)站登錄接口被機器人輕易地使用,產(chǎn)生一些沒有意義的用戶數(shù)據(jù),所以,采用驗證碼進行一定程度上的攔截,當(dāng)然,我們采用的還是一個數(shù)字與字母結(jié)合的圖片驗證碼形式,后續(xù)會講到更加復(fù)雜的數(shù)字計算類型的圖片驗證碼,請持續(xù)關(guān)注我的博客。
實現(xiàn)思路
博主環(huán)境:springboot3 、java17、thymeleaf
-
訪問登錄頁面
-
登錄
- 驗證驗證碼
- 驗證賬號、密碼
- 驗證成功時,生成登錄憑證,發(fā)放給客戶端
- 驗證失敗時,跳轉(zhuǎn)回登錄信息,并保留原有填入信息
-
退出
- 將登錄憑證修改為失效狀態(tài)
- 跳轉(zhuǎn)至首頁
訪問登錄頁面的方法已經(jīng)在前文說明過了,就不多加贅述了,展示一下代碼:
// 登錄頁面
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String getLoginPage() {
return "/site/login";
}
訪問完登錄頁面,我們就要進行信息輸入,然而,現(xiàn)在,還沒有把驗證碼信息正確展現(xiàn)出來,所以,接下來,我們先來實現(xiàn)驗證碼的部分。
所需兩個數(shù)據(jù)表 SQL 代碼如下:
注:注冊流程可看前文。一文教你學(xué)會實現(xiàn)以郵件激活的注冊賬戶代碼_yumuing的博客-CSDN博客
-- user表
DROP TABLE IF EXISTS `user`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
`salt` varchar(50) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`type` int(11) DEFAULT NULL COMMENT '0-普通用戶; 1-超級管理員; 2-版主;',
`status` int(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;',
`activation_code` varchar(100) DEFAULT NULL,
`header_url` varchar(200) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_username` (`username`(20)),
KEY `index_email` (`email`(20))
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;
-- 登錄憑證表
DROP TABLE IF EXISTS `login_ticket`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `login_ticket` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`ticket` varchar(45) NOT NULL,
`status` int(11) DEFAULT '0' COMMENT '1-有效; 0-無效;',
`expired` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `index_ticket` (`ticket`(20))
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Kaptcha 驗證碼設(shè)計和校驗
目前使用圖片驗證碼較為廣泛的是 Kaptcha ,它只有一個版本:2.3.2,值得注意的是,在 springboot 3的環(huán)境下,使用該插件包大部分會使用到的 http 包,不能導(dǎo)入 javax 包內(nèi)的,而是應(yīng)該導(dǎo)入jakarta 包內(nèi)的。
它能夠?qū)崿F(xiàn)以下效果:水紋有干擾、魚眼無干擾、水紋無干擾、陰影無干擾、陰影有干擾
其中,它們的文字內(nèi)容限制、背景圖片、文字顏色、大小、干擾樣式顏色、整體(圖片)高度、寬度、圖片渲染效果、干擾與否都是可以進行自定義的。我們只要按需配置好對應(yīng)的 configuration 即可。當(dāng)然,它并沒有默認(rèn)集成進 springboot 中,使用之前必須先導(dǎo)入對應(yīng)依賴,如下:
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
導(dǎo)包成功之后,我們就需要進行按需設(shè)置配置類了,它相關(guān)配置屬性如下:
配置類模板如下:
package top.yumuing.community.config;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptchaProduce(){
Properties properties=new Properties();
//圖片的寬度
properties.setProperty("kaptcha.image.width","100");
//圖片的高度
properties.setProperty("kaptcha.image.height","40");
//字體大小
properties.setProperty("kaptcha.textproducer.font.size","32");
//字體顏色(RGB)
properties.setProperty("kaptcha.textproducer.font.color","0,0,0");
//驗證碼字符的集合
properties.setProperty("kaptcha.textproducer.char.string","123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
//驗證碼長度(即在上面集合中隨機選取幾位作為驗證碼)
properties.setProperty("kaptcha.textproducer.char.length","4");
//圖片的干擾樣式:默認(rèn)存在無規(guī)則劃線干擾
//無干擾:com.google.code.kaptcha.impl.NoNoise
properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
//圖片干擾顏色:默認(rèn)為黑色
properties.setProperty("kaptcha.noise.color", "black");
//圖片渲染效果:默認(rèn)水紋
// 水紋com.google.code.kaptcha.impl.WaterRipple 魚眼com.google.code.kaptcha.impl.FishEyeGimpy 陰影com.google.code.kaptcha.impl.ShadowGimpy
//properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.ShadowGimpy");
DefaultKaptcha Kaptcha = new DefaultKaptcha();
Config config=new Config(properties);
Kaptcha.setConfig(config);
return Kaptcha;
}
}
配置好相關(guān)屬性之后,我們就可以進行驗證碼生成的接口開發(fā)了,首先,讓 Producer 進入 Bean 工廠進行管理,之后,再生成驗證碼文本并傳入 session 中,以便后續(xù)進行驗證碼校驗,之后,再生成對應(yīng)驗證碼圖片,以 BufferedImage 的形式存儲,并利用 HttpServletResponse 和 ImageIO 將圖片傳輸給瀏覽器,其中,注意設(shè)置好圖片返回類型,并且無需手動關(guān)閉 IO 流,springboot 會進行管理,實現(xiàn)自行關(guān)閉。此時以 Get 方法訪問 域名/imageCode ,就會返回對應(yīng)驗證碼圖片了。
//驗證碼
@RequestMapping(path = "/imageCode",method = RequestMethod.GET)
public void getImgCode(HttpServletResponse response, HttpSession session){
String codeText = imageCodeProducer.createText();
BufferedImage imageCode = imageCodeProducer.createImage(codeText);
// 將驗證碼文本存入 session
session.setAttribute("imageCode", codeText);
//設(shè)置返回類型
response.setContentType("image/jpg");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(imageCode, "jpg", os);
} catch (IOException e) {
logger.error("響應(yīng)驗證碼失?。?+e.getMessage());
}
}
當(dāng)然,有些瀏覽器為了節(jié)省用戶訪問流量,較為智能地將已獲取的靜態(tài)資源鏈接自動不再訪問,所以,需要添加額外參數(shù)完成瀏覽器適配,這里采用的是利用 JavaScript 把每次訪問驗證碼圖片的鏈接添加一個隨機數(shù)字的參數(shù),以保證智能節(jié)省流量的問題。當(dāng)然,我們不用去 controller 獲取該參數(shù),因為沒有意義,也不要求一定要所有參數(shù)都匹配到。代碼如下:
function refresh_imageCode() {
var path = "/imageCode?p=" + Math.random();
$("#imageCode").attr("src", path);
}
獲取到驗證碼,我們就必須對其進行校對,只有驗證碼通過之后,才能去校驗賬戶和密碼。而驗證碼校對最重要的一點就是,需要忽略大小寫,不能苛求用戶的耐心。校驗驗證碼不通過的情況不僅僅需要考慮發(fā)送方的驗證碼文本為空或者文本不一致導(dǎo)致的錯誤,還需要考慮接受方(服務(wù)端)的驗證碼文本究竟有沒有存儲下來,以防通過接口工具直接 post 訪問該接口產(chǎn)生的空數(shù)據(jù)。代碼如下:
//登錄
@RequestMapping(path = "/login",method = RequestMethod.POST)
public String login(String username, String password, String code,
boolean rememberMe, Model model, HttpSession session, HttpServletResponse response){
String imageCode = (String) session.getAttribute("imageCode");
// 驗證碼
if (StringUtils.isBlank(imageCode) || StringUtils.isBlank(code) || !imageCode.equalsIgnoreCase(code)){
model.addAttribute("codeMsg","驗證碼不正確!");
return "/site/login";
}
}
記住我功能的實現(xiàn)
用戶進行登錄時,常常需要勾選是否記住的按鈕,這是為了保證用戶長時間使用該應(yīng)用而不因為需要頻繁登錄,喪失用戶量。當(dāng)然,也有部分用戶不希望自己的用戶憑證長時間保存,希望通過經(jīng)常性更新,保證一定程度上的用戶數(shù)據(jù)安全。實現(xiàn)這個功能并不困難,只要發(fā)送數(shù)據(jù)時,多添加一個布爾參數(shù)而已。為了便于代碼閱讀,增加兩個常量:登錄默認(rèn)狀態(tài)超時時間常量、記住我登錄狀態(tài)超時時間常量,如下:
// 默認(rèn)登錄狀態(tài)超時常量
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
// 記住狀態(tài)的登錄憑證超時時間
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
之后在登錄接口進行判斷就行,記住我布爾值為 true ,故代碼如下:
// 是否記住我
int expiredSeconds = rememberMe ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
校驗賬號和密碼
按照標(biāo)準(zhǔn)流程,先從數(shù)據(jù)訪問層開始寫,我們校驗賬戶和密碼都是使用查詢語句就行了,當(dāng)然,一句查詢語句就行,不用為了兩個參數(shù)就建兩個查詢語句,因為我們已經(jīng)獲得了這個對象,直接使用映射方法里的 get 方法就行,再進行所需要的校驗工作。這里采用的是 username 為參數(shù)的查詢語句來獲取 user 對象。具體代碼如下:
userMapper.java
User selectOneByUsername(@Param("username") String username);
userMapper.xml
<sql id="Base_Column_List">
id,username,password,
salt,email,type,
status,activation_code,header_url,
create_time
</sql>
<select id="selectOneByUsername" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from user
where
username = #{username,jdbcType=VARCHAR}
</select>
使用該查詢語句之前,我們必須先保證傳過來的賬戶和密碼不能為空,查詢才有意義,獲取到 user 對象之后,我們先驗證賬戶存不存在,如果不存在,返回錯誤信息就行了,如果存在的話,檢查它的賬戶狀態(tài)是否是激活狀態(tài),不是的話,返回錯誤信息,是的話,我們就能進行校驗工作了,當(dāng)然,賬戶存在,用戶名就不用校驗了,只需要校驗密碼就行了。代碼如下:
//空值處理
if(StringUtils.isBlank(username)){
map.put("usernameMsg", "賬號不能為空!");
return map;
}
if (StringUtils.isBlank(password)){
map.put("passwordMsg", "密碼不能為空!");
return map;
}
//驗證賬號
User user = userMapper.selectOneByUsername(username);
if (user == null){
map.put("usernameMsg","該賬號不存在");
return map;
}
//驗證狀態(tài)
if (user.getStatus() == 0){
map.put("usernameMsg","該賬號未激活!");
return map;
}
//驗證密碼
password = CommunityUtil.md5(password+user.getSalt());
if(!user.getPassword().equals(password)){
map.put("passwordMsg","密碼不正確!");
return map;
}
當(dāng)賬戶密碼校驗成功時,將登錄憑證存入 cookie 即可,設(shè)置好全局可用,以及失效時間,只要設(shè)置好登錄憑證失效時間,后續(xù)客戶端會自動在時間到達,將登錄憑證注銷掉,以便我們把登錄狀態(tài)取消掉。如果校驗不成功的話,就直接返回校驗信息。在登錄接口進行調(diào)用即可
// 檢測賬號密碼
Map<String,Object> map = userServiceImpl.login(username,password,expiredSeconds);
if (map.containsKey("loginTicket")){
//設(shè)置cookie
Cookie cookie = new Cookie("loginTicket",map.get("loginTicket").toString());
cookie.setPath("/");
cookie.setMaxAge(expiredSeconds);
response.addCookie(cookie);
return "redirect:/index";
}else {
model.addAttribute("usernameMsg",map.get("usernameMsg"));
model.addAttribute("passwordMsg",map.get("passwordMsg"));
return "/site/login";
}
生成登錄憑證
還是先從數(shù)據(jù)訪問層說起,注意生成自增id即可。具體的 xml 語句如下:
<insert id="insertAll" parameterType="LoginTicket" keyProperty="id">
insert into login_ticket
(id, user_id, ticket,
status, expired)
values (#{id,jdbcType=NUMERIC}, #{userId,jdbcType=NUMERIC}, #{ticket,jdbcType=VARCHAR},
#{status,jdbcType=NUMERIC}, #{expired,jdbcType=TIMESTAMP})
</insert>
采用的是字母和數(shù)字混合的隨機字符串的形式,利用的是 java.util.UUID 來生成的。將需要的參數(shù)利用 set 方法存入對象里面,再利用對應(yīng)插入語句插入數(shù)據(jù)庫即可,注意默認(rèn)生效狀態(tài)為 1。具體生成登錄憑證的登錄接口代碼如下:
//生成登錄憑證
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(1);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
loginTicketMapper.insertAll(loginTicket);
map.put("loginTicket",loginTicket.getTicket());
return map;
不知道你們有沒有察覺一個問題:失效時間到了,狀態(tài)仍為生效狀態(tài)的。我們的登錄憑證生效狀態(tài)是后續(xù)登錄信息展示的關(guān)鍵,后續(xù)還會考慮,時間過期之后,生效狀態(tài)該怎么去自動修改?或者不作修改該怎么去解決失效時間到了,狀態(tài)仍為生效狀態(tài)的問題,請持續(xù)關(guān)注博主,后續(xù)為你們解答。
將登錄憑證發(fā)送給客戶端,就基本完成了登錄的實現(xiàn)。
相關(guān)代碼資源已上傳,可看:項目代碼
相關(guān) bug
No primary or single unique constructor found for interface javax.servlet.http.HttpServletResponse
springboot3 下導(dǎo)不了 javax.servlet.http 包,必須導(dǎo) jakarta.servlet.http
也就是 http 包 又更改了。文章來源:http://www.zghlxwxcb.cn/news/detail-402377.html
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
不能導(dǎo),不然會發(fā)生錯誤。文章來源地址http://www.zghlxwxcb.cn/news/detail-402377.html
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
到了這里,關(guān)于基于 Kaptcha 驗證碼檢驗的登錄就該這么實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!