Login based on Kaptcha verification code verification should be implemented like this

In the actual application process of the website, in order to prevent the website login interface from being easily used by robots and generate some meaningless user data, the verification code is used to intercept to a certain extent. Of course, we still use a combination of numbers and letters. In the form of picture verification code, we will talk about more complex digital calculation type picture verification code later, please continue to pay attention to my blog.

Implementation ideas

Blogger environment: springboot3, java17, thymeleaf

  1. Visit the login page

  2. Log in

    • Verify Captcha
    • Verify account and password
    • When the verification is successful, a login credential is generated and issued to the client
    • When the verification fails, jump back to the login information and keep the original filled information
  3. quit

    • Modify the login credentials to an invalid state
    • Jump to homepage

The method of accessing the login page has been explained above, so I won’t go into details here. Let’s show the code:

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

After visiting the login page, we have to enter information. However, the verification code information has not been displayed correctly yet, so, next, let's implement the verification code part first.

The required SQL codes for the two data tables are as follows:

Note: The registration process can be seen above. This article teaches you how to realize the registered account code activated by email - yumuing's blog - CSDN blog

-- 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 verification code design and verification

At present, Kaptcha is the most widely used picture verification code. It has only one version: 2.3.2. It is worth noting that in the springboot 3 environment, most of the http packages used by this plug-in package cannot be imported into the javax package. , but should be imported in the jakarta package.

It can achieve the following effects: water pattern with interference, fisheye without interference, water pattern without interference, shadow without interference, shadow with interference

Captcha style

Among them, their text content limit, background image, text color, size, interference style color, overall (picture) height, width, image rendering effect, and interference or not can all be customized. We only need to configure the corresponding configuration as needed. Of course, it is not integrated into springboot by default, and the corresponding dependencies must be imported before use, as follows:

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

After successfully importing the package, we need to set the configuration class on demand, and its related configuration properties are as follows:

Configuration class attribute information

The configuration class template is as follows:

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

After configuring the relevant properties, we can develop the interface for verification code generation. First, let the Producer enter the Bean factory for management, and then generate the verification code text and pass it into the session for subsequent verification code verification. , and then generate the corresponding verification code image, store it in the form of BufferedImage, and use HttpServletResponse and ImageIO to transfer the image to the browser. Among them, pay attention to setting the return type of the image, and there is no need to manually close the IO stream. Springboot will manage it to realize self-closing . At this time, access the domain name/imageCode with the Get method, and the corresponding verification code image will be returned.

//验证码
@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());
    }
    
}

Of course, in order to save user access traffic, some browsers intelligently stop accessing the acquired static resource links automatically. Therefore, additional parameters need to be added to complete browser adaptation. Here, JavaScript is used to convert each access verification code Add a random number parameter to the picture link to ensure intelligent traffic saving. Of course, we don't need to go to the controller to get this parameter, because it is meaningless, and it is not required that all parameters must match. code show as below:

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

After obtaining the verification code, we must proofread it. Only after the verification code is passed can we verify the account and password. The most important point of verification code proofreading is that capitalization needs to be ignored, and the user's patience cannot be demanded. When verifying that the verification code fails, not only need to consider the error caused by the sender’s verification code text being empty or the text is inconsistent, but also need to consider whether the recipient’s (server) verification code text has been stored to prevent it from passing through the interface. The tool directly post accesses the empty data generated by this interface. code show as below:

//登录
@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";
    }
}

Implementation of the remember-me function

When users log in, they often need to check whether to remember the button. This is to ensure that users use the application for a long time without losing the number of users due to frequent logins. Of course, some users do not want their user credentials to be stored for a long time, and hope to ensure a certain degree of user data security through frequent updates. It is not difficult to realize this function, as long as one more Boolean parameter is added when sending data. In order to facilitate code reading, two constants are added: login default status timeout time constant, remember me login status timeout time constant, as follows:

// 默认登录状态超时常量
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

// 记住状态的登录凭证超时时间
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;

After that, just judge in the login interface. Remember that my Boolean value is true, so the code is as follows:

// 是否记住我
int expiredSeconds = rememberMe ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;

Verify account and password

According to the standard process, start writing from the data access layer first. We need to use query statements to verify accounts and passwords. Of course, one query statement is enough. There is no need to create two query statements for two parameters, because we have already obtained For this object, you can directly use the get method in the mapping method, and then perform the required verification work. Here, the query statement with username as the parameter is used to obtain the user object. The specific code is as follows:

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>

Before using this query statement, we must first ensure that the account and password passed in cannot be empty, so that the query is meaningful. After obtaining the user object, we first verify whether the account exists. If it does not exist, return an error message. If If it exists, check whether its account status is activated. If not, return an error message. If yes, we can perform the verification work. Of course, if the account exists, the user name does not need to be verified, only the password needs to be verified That's it. code show as below:

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

When the account password verification is successful, just store the login credentials in the cookie, set the global availability, and the expiration time, as long as the login credentials expiration time is set, the subsequent client will automatically arrive at the time and log out the login credentials so that We cancel the login status. If the verification is unsuccessful, the verification information is returned directly. You can call it in the login interface

// 检测账号密码
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";
}

Generate login credentials

Let’s start with the data access layer first, and pay attention to generating an auto-increment id. The specific xml statement is as follows:

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

It is in the form of a random string of mixed letters and numbers, which is generated using java.util.UUID. Use the set method to store the required parameters into the object, and then use the corresponding insert statement to insert into the database. Note that the default effective state is 1. The specific login interface code for generating login credentials is as follows:

//生成登录凭证
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;

I don’t know if you have noticed a problem: the status is still valid when the expiration time is up. The valid status of our login credentials is the key to the subsequent display of login information. We will also consider how to automatically modify the valid status after the time expires. Or how to solve the problem that the status is still in effect when the expiration time is up without making any changes. Please continue to pay attention to the blogger and answer for you later.

Sending the login credentials to the client basically completes the login implementation.

Please follow and like
Relevant code resources have been uploaded, see: project code

related bugs

No primary or single unique constructor found for interface javax.servlet.http.HttpServletResponse

springboot3 cannot import javax.servlet.http package, must import jakarta.servlet.http

That is, the http package has changed again.

import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

Can not be guided, otherwise errors will occur.

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

Guess you like

Origin blog.csdn.net/yumuing/article/details/129502314