手把手教你验证码检验的登录

简介: 在网站实际应用过程中,为了防止网站登录接口被机器人轻易地使用,产生一些没有意义的用户数据,所以,采用验证码进行一定程度上的拦截,当然,我们采用的还是一个数字与字母结合的图片验证码形式,后续会讲到更加复杂的数字计算类型的图片验证码,请持续关注我的博客。

在网站实际应用过程中,为了防止网站登录接口被机器人轻易地使用,产生一些没有意义的用户数据,所以,采用验证码进行一定程度上的拦截,当然,我们采用的还是一个数字与字母结合的图片验证码形式,后续会讲到更加复杂的数字计算类型的图片验证码,请持续关注我的博客。
实现思路
博主环境:springboot3 、java17、thymeleaf

访问登录页面

登录

验证验证码
验证账号、密码
验证成功时,生成登录凭证,发放给客户端
验证失败时,跳转回登录信息,并保留原有填入信息

退出

将登录凭证修改为失效状态
跳转至首页

访问登录页面的方法已经在前文说明过了,就不多加赘述了,展示一下代码:
// 登录页面
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String getLoginPage() {

return "/site/login";

}
复制代码
访问完登录页面,我们就要进行信息输入,然而,现在,还没有把验证码信息正确展现出来,所以,接下来,我们先来实现验证码的部分。
所需两个数据表 SQL 代码如下:
注:注册流程可看前文.一文教你学会实现以邮件激活的注册账户代码 - 掘金 (juejin.cn)
-- 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 验证码设计和校验
目前使用图片验证码较为广泛的是 Kaptcha ,它只有一个版本:2.3.2,值得注意的是,在 springboot 3的环境下,使用该插件包大部分会使用到的 http 包,不能导入 javax 包内的,而是应该导入jakarta 包内的。
它能够实现以下效果:水纹有干扰、鱼眼无干扰、水纹无干扰、阴影无干扰、阴影有干扰

其中,它们的文字内容限制、背景图片、文字颜色、大小、干扰样式颜色、整体(图片)高度、宽度、图片渲染效果、干扰与否都是可以进行自定义的。我们只要按需配置好对应的 configuration 即可。当然,它并没有默认集成进 springboot 中,使用之前必须先导入对应依赖,如下:

<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>


复制代码
导包成功之后,我们就需要进行按需设置配置类了,它相关配置属性如下:

配置类模板如下:
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");
    //图片的干扰样式:默认存在无规则划线干扰
    //无干扰:com.google.code.kaptcha.impl.NoNoise
    properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
    //图片干扰颜色:默认为黑色
    properties.setProperty("kaptcha.noise.color", "black");
    //图片渲染效果:默认水纹
    // 水纹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;
}

}
复制代码
配置好相关属性之后,我们就可以进行验证码生成的接口开发了,首先,让 Producer 进入 Bean 工厂进行管理,之后,再生成验证码文本并传入 session 中,以便后续进行验证码校验,之后,再生成对应验证码图片,以 BufferedImage 的形式存储,并利用 HttpServletResponse 和 ImageIO 将图片传输给浏览器,其中,注意设置好图片返回类型,并且无需手动关闭 IO 流,springboot 会进行管理,实现自行关闭。此时以 Get 方法访问 域名/imageCode ,就会返回对应验证码图片了。
//验证码
@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);

//设置返回类型
response.setContentType("image/jpg");

try {
    OutputStream os = response.getOutputStream();
    ImageIO.write(imageCode, "jpg", os);
} catch (IOException e) {
    logger.error("响应验证码失败!"+e.getMessage());
}

}
复制代码
当然,有些浏览器为了节省用户访问流量,较为智能地将已获取的静态资源链接自动不再访问,所以,需要添加额外参数完成浏览器适配,这里采用的是利用 JavaScript 把每次访问验证码图片的链接添加一个随机数字的参数,以保证智能节省流量的问题。当然,我们不用去 controller 获取该参数,因为没有意义,也不要求一定要所有参数都匹配到。代码如下:
function refresh_imageCode() {

var path = "/imageCode?p=" + Math.random();
$("#imageCode").attr("src", path);

}
复制代码
获取到验证码,我们就必须对其进行校对,只有验证码通过之后,才能去校验账户和密码。而验证码校对最重要的一点就是,需要忽略大小写,不能苛求用户的耐心。校验验证码不通过的情况不仅仅需要考虑发送方的验证码文本为空或者文本不一致导致的错误,还需要考虑接受方(服务端)的验证码文本究竟有没有存储下来,以防通过接口工具直接 post 访问该接口产生的空数据。代码如下:
//登录
@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";
}

}
复制代码
记住我功能的实现
用户进行登录时,常常需要勾选是否记住的按钮,这是为了保证用户长时间使用该应用而不因为需要频繁登录,丧失用户量。当然,也有部分用户不希望自己的用户凭证长时间保存,希望通过经常性更新,保证一定程度上的用户数据安全。实现这个功能并不困难,只要发送数据时,多添加一个布尔参数而已。为了便于代码阅读,增加两个常量:登录默认状态超时时间常量、记住我登录状态超时时间常量,如下:
// 默认登录状态超时常量
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

// 记住状态的登录凭证超时时间
int REMEMBER_EXPIRED_SECONDS = 3600 24 100;
复制代码
之后在登录接口进行判断就行,记住我布尔值为 true ,故代码如下:
// 是否记住我
int expiredSeconds = rememberMe ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
复制代码
校验账号和密码
按照标准流程,先从数据访问层开始写,我们校验账户和密码都是使用查询语句就行了,当然,一句查询语句就行,不用为了两个参数就建两个查询语句,因为我们已经获得了这个对象,直接使用映射方法里的 get 方法就行,再进行所需要的校验工作。这里采用的是 username 为参数的查询语句来获取 user 对象。具体代码如下:
userMapper.java
User selectOneByUsername(@Param("username") String username);
复制代码
userMapper.xml

id,username,password,
salt,email,type,
status,activation_code,header_url,
create_time


select
<include refid="Base_Column_List"/>
from user
where
username = #{username,jdbcType=VARCHAR}


复制代码
使用该查询语句之前,我们必须先保证传过来的账户和密码不能为空,查询才有意义,获取到 user 对象之后,我们先验证账户存不存在,如果不存在,返回错误信息就行了,如果存在的话,检查它的账户状态是否是激活状态,不是的话,返回错误信息,是的话,我们就能进行校验工作了,当然,账户存在,用户名就不用校验了,只需要校验密码就行了。代码如下:
//空值处理
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;

}

//验证状态
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;

}
复制代码
当账户密码校验成功时,将登录凭证存入 cookie 即可,设置好全局可用,以及失效时间,只要设置好登录凭证失效时间,后续客户端会自动在时间到达,将登录凭证注销掉,以便我们把登录状态取消掉。如果校验不成功的话,就直接返回校验信息。在登录接口进行调用即可
// 检测账号密码
Map<String,Object> map = userServiceImpl.login(username,password,expiredSeconds);
if (map.containsKey("loginTicket")){

//设置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";

}
复制代码
生成登录凭证
还是先从数据访问层说起,注意生成自增id即可。具体的 xml 语句如下:

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})


复制代码
采用的是字母和数字混合的随机字符串的形式,利用的是 java.util.UUID 来生成的。将需要的参数利用 set 方法存入对象里面,再利用对应插入语句插入数据库即可,注意默认生效状态为 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;
复制代码
不知道你们有没有察觉一个问题:失效时间到了,状态仍为生效状态的。我们的登录凭证生效状态是后续登录信息展示的关键,后续还会考虑,时间过期之后,生效状态该怎么去自动修改?或者不作修改该怎么去解决失效时间到了,状态仍为生效状态的问题,请持续关注博主,后续为你们解答。
将登录凭证发送给客户端,就基本完成了登录的实现。

相关代码资源已上传,可看:项目代码
相关 bug
No primary or single unique constructor found for interface javax.servlet.http.HttpServletResponse
springboot3 下导不了 javax.servlet.http 包,必须导 jakarta.servlet.http
也就是 http 包 又更改了。
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
复制代码
不能导,不然会发生错误。
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

相关文章
|
6月前
|
存储 JSON JavaScript
前后端分离项目知识汇总(微信扫码登录,手机验证码登录,JWT)-1
前后端分离项目知识汇总(微信扫码登录,手机验证码登录,JWT)
179 0
|
6月前
|
前端开发 安全 Java
SpringBoot 实现登录验证码(附集成SpringSecurity)
SpringBoot 实现登录验证码(附集成SpringSecurity)
396 0
|
3月前
|
存储 NoSQL 数据库
认证服务---整合短信验证码,用户注册和登录 ,密码采用MD5加密存储 【二】
这篇文章讲述了在分布式微服务系统中添加用户注册和登录功能的过程,重点介绍了用户注册时通过远程服务调用第三方服务获取短信验证码、使用Redis进行验证码校验、对密码进行MD5加密后存储到数据库,以及用户登录时的远程服务调用和密码匹配校验的实现细节。
认证服务---整合短信验证码,用户注册和登录 ,密码采用MD5加密存储 【二】
|
1月前
|
Java
Java 登录输入的验证码
Java 登录输入的验证码
22 1
|
1月前
|
C#
C# 图形验证码实现登录校验代码
C# 图形验证码实现登录校验代码
72 2
|
2月前
|
存储 JSON 前端开发
node使用token来实现前端验证码和登录功能详细流程[供参考]=‘很值得‘
本文介绍了在Node.js中使用token实现前端验证码和登录功能的详细流程,包括生成验证码、账号密码验证以及token验证和过期处理。
44 0
node使用token来实现前端验证码和登录功能详细流程[供参考]=‘很值得‘
|
3月前
|
资源调度 JavaScript API
nest.js + sms 实现短信验证码登录
本文介绍了在Nest.js框架中集成短信验证码登录的实现方案,详细阐述了使用阿里云短信服务的配置流程、资质申请、短信模板设置,并提供了API调用示例和工程代码的运行步骤。
nest.js + sms 实现短信验证码登录
|
3月前
【Azure 环境】中国区Azure B2C 是否支持手机验证码登录呢?
【Azure 环境】中国区Azure B2C 是否支持手机验证码登录呢?
|
6月前
|
安全 Java 数据库
SpringSecurity实现多种登录方式,如邮件验证码、电话号码登录
SpringSecurity实现多种登录方式,如邮件验证码、电话号码登录
1145 2
|
6月前
|
缓存 算法 NoSQL
短信验证码登录接口,如何防止恶意攻击
该文讨论了移动应用中常见的手机短信验证码登录(短验登录)的安全设计。后端通常需要提供获取短信验证码和手机短验登录两个API。为了增强机短验登录API的安全性,提出了几种无需依赖Redis等存储介质的方案:1)使用数字签名确保请求合法性;2)基于时间戳的验证,允许在一定时间范围内有效;3)应用TOTP算法生成动态码进行验证;4)利用JWTToken进行身份验证并设置有效期。文章强调了创新思考在解决安全问题中的重要性,并鼓励读者分享更多方案。
559 1