基于 Kaptcha 验证码检验的登录就该这么实现

Posted yumuing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 Kaptcha 验证码检验的登录就该这么实现相关的知识,希望对你有一定的参考价值。

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

文章目录

实现思路

博主环境:springboot3 、java17、thymeleaf

  1. 访问登录页面

  2. 登录

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

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

访问登录页面的方法已经在前文说明过了,就不多加赘述了,展示一下代码:

// 登录页面
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String getLoginPage() 
    return "/site/login";

访问完登录页面,我们就要进行信息输入,然而,现在,还没有把验证码信息正确展现出来,所以,接下来,我们先来实现验证码的部分。

所需两个数据表 SQL 代码如下:

注:注册流程可看前文。一文教你学会实现以邮件激活的注册账户代码_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 验证码设计和校验

目前使用图片验证码较为广泛的是 Kaptcha ,它只有一个版本:2.3.2,值得注意的是,在 springboot 3的环境下,使用该插件包大部分会使用到的 http 包,不能导入 javax 包内的,而是应该导入jakarta 包内的。

它能够实现以下效果:水纹有干扰、鱼眼无干扰、水纹无干扰、阴影无干扰、阴影有干扰

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

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

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

配置类模板如下:

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

<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 对象之后,我们先验证账户存不存在,如果不存在,返回错误信息就行了,如果存在的话,检查它的账户状态是否是激活状态,不是的话,返回错误信息,是的话,我们就能进行校验工作了,当然,账户存在,用户名就不用校验了,只需要校验密码就行了。代码如下:

//空值处理
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 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>

采用的是字母和数字混合的随机字符串的形式,利用的是 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;

java 实现登录验证码 (kaptcha 验证码组件)

验证码的作用:

1、防止广告机注册和发帖、评论。
2、防止暴力破解密码,特别是有管理员权限的密码。

在这里介绍一种非常实用的验证码生成工具:kaptcha 

这个工具,可以生成各种样式的验证码,因为它是可配置的。

而 kaptcha工作的原理,是调用 com.google.code.kaptcha.servlet.KaptchaServlet,生成一个图片。同时将生成的验证码字符串放到 HttpSession中,直接从session中获取这张验证码图片,而不会占用实际内存。

使用 kaptcha 可以方便的配置如下属性:

kaptcha.border           是否有边框 默认为true 我们可以自己设置yes,no
kaptcha.border.color        边框颜色 默认为Color.BLACK
kaptcha.border.thickness         边框粗细度 默认为1
kaptcha.producer.impl           验证码生成器 默认为DefaultKaptcha
kaptcha.textproducer.impl        验证码文本生成器 默认为DefaultTextCreator
kaptcha.textproducer.char.string      验证码文本字符内容范围 默认为abcde2345678gfynmnpwx
kaptcha.textproducer.char.length     验证码文本字符长度 默认为5
kaptcha.textproducer.font.names     验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
kaptcha.textproducer.font.size      验证码文本字符大小 默认为40
kaptcha.textproducer.font.color     验证码文本字符颜色 默认为Color.BLACK
kaptcha.textproducer.char.space     验证码文本字符间距 默认为2
kaptcha.noise.impl            验证码噪点生成对象 默认为DefaultNoise
kaptcha.noise.color           验证码噪点颜色 默认为Color.BLACK
kaptcha.obscurificator.impl         验证码样式引擎 默认为WaterRipple
kaptcha.word.impl             验证码文本字符渲染 默认为DefaultWordRenderer
kaptcha.background.impl         验证码背景生成器 默认为DefaultBackground
kaptcha.background.clear.from        验证码背景颜色渐进 默认为Color.LIGHT_GRAY
kaptcha.background.clear.to      验证码背景颜色渐进 默认为Color.WHITE
kaptcha.image.width           验证码图片宽度 默认为200
kaptcha.image.height            验证码图片高度 默认为50
kaptcha.session.key           session中存放验证码的key键

 

所实用的框架:SSM

所需的验证码的 jar 包:kaptcha-2.3.2.jar,可以到官网上下载:http://code.google.com/p/kaptcha/

applicationContext.xml 需要配置验证码的相关属性:

 1 <!-- 验证码 -->
 2     <bean id="captchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha">
 3         <property name="config">
 4             <bean class="com.google.code.kaptcha.util.Config">
 5                 <constructor-arg>
 6                     <props>
 7                         <!--这里的颜色只支持标准色和rgb颜色,不可使用十六进制的颜色-->
 8                         <!-- 是否有边框 -->
 9                         <prop key="kaptcha.border">no</prop>
10                         <!-- 验证码文本字符颜色 -->
11                         <prop key="kaptcha.textproducer.font.color">black</prop>
12                         <!-- 验证码图片宽度 -->
13                         <prop key="kaptcha.image.width">92</prop>
14                         <!-- 验证码图片高度 -->
15                         <prop key="kaptcha.image.height">36</prop>
16                         <!-- 验证码文本字符大小 -->
17                         <prop key="kaptcha.textproducer.font.size">24</prop>
18                         <!-- session中存放验证码的key键 -->
19                         <prop key="kaptcha.session.key">code</prop>
20                         <!-- 验证码噪点颜色 -->
21                         <prop key="kaptcha.noise.color">white</prop>
22                         <!-- 验证码文本字符间距 -->
23                         <prop key="kaptcha.textproducer.char.space">3</prop>
24                         <!-- 验证码样式引擎 -->
25                         <prop key="kaptcha.obscurificator.impl">com.google.code.kaptcha.impl.ShadowGimpy</prop>
26                         <!-- 验证码文本字符长度 -->
27                         <prop key="kaptcha.textproducer.char.length">4</prop>
28                         <!-- 验证码文本字体样式 -->
29                         <prop key="kaptcha.textproducer.font.names">宋体,楷体,微软雅黑</prop>
30                     </props>
31                 </constructor-arg>
32             </bean>
33         </property>
34     </bean>

 

生成二维码图片的控制类:CaptchaController.java

 1 /**
 2      * com.krry.web 
 3      * 方法名:生成二维码控制类
 4      * 创建人:krry 
 5      * @param request
 6      * @param response
 7      * @return
 8      * @throws Exception 
 9      * 返回类型:ModelAndView
10      * @exception 
11      * @since  1.0.0
12     */
13     @RequestMapping("/code")
14     public ModelAndView getKaptchaImage(HttpServletRequest request,HttpServletResponse response) throws Exception {
15         HttpSession session = request.getSession();
16         //获取验证码
17         //    String code = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
18         //    String code = (String) session.getAttribute("Kaptcha_Code");
19         //清除浏览器的缓存
20         response.setDateHeader("Expires", 0);
21         // Set standard HTTP/1.1 no-cache headers.
22         response.setHeader("Cache-Control","no-store, no-cache, must-revalidate");
23         // Set IE extended HTTP/1.1 no-cache headers (use addHeader).
24         response.addHeader("Cache-Control", "post-check=0, pre-check=0");
25         // Set standard HTTP/1.0 no-cache header.
26         response.setHeader("Pragma", "no-cache");
27         // return a jpeg
28         response.setContentType("image/jpeg");
29         //浏览器记忆功能-----当前过浏览器和服务器交互成功以后下载的图片和资源会进行缓存一次。下次刷新的时候就不会在到服务器去下载。
30         // 获取KAPTCHA验证的随机文本
31         String capText = captchaProducer.createText();
32         // 将生成好的图片放入会话中
33         session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
34         // create the image with the text
35         BufferedImage bi = captchaProducer.createImage(capText);
36         ServletOutputStream out = response.getOutputStream();
37         // write the data out
38         ImageIO.write(bi, "jpg", out);
39         try {
40             out.flush();
41         } finally {
42             out.close();//关闭
43         }
44         return null;
45     }

 

前台调用:

1 <input type=‘text‘ placeholder=‘请输入验证码...‘ maxlength=‘4‘ autocomplete=‘off‘ class=‘inp kr_code‘ id=‘code‘/>
2 <img src=‘"+basePath+"/kaptcha/code.do‘ class=‘yanz_img‘ onclick=‘changeyanz($(this));‘>

 

js方法:

点击验证码图片换验证码时,<img> 的 onclick 里面做的就是改变 <img> 标签的 src 属性。

所以要给 url 带一个随机数,这样每次点击验证码图片时,都会由于 src 改变而重新请求 jsp

1 function changeyanz(obj){
2     obj.attr("src",basePath+"/kaptcha/code.do?d="+new Date().getTime());
3 }

 






















以上是关于基于 Kaptcha 验证码检验的登录就该这么实现的主要内容,如果未能解决你的问题,请参考以下文章

java 实现登录验证码 (kaptcha 验证码组件)

SpringSecurity-6-基于Filter实现图形验证码

全网最新Redis结合Kaptcha实现验证码功能篇二(前后端分离)

基于Spring+SpringMVC+MyBatis开发书评网Kaptcha组件配置

work_04_谷歌验证码工具Kaptcha

Google Kaptcha验证码的使用