在网站实际应用过程中,为了防止网站登录接口被机器人轻易地使用,产生一些没有意义的用户数据,所以,采用验证码进行一定程度上的拦截,当然,我们采用的还是一个数字与字母结合的图片验证码形式,后续会讲到更加复杂的数字计算类型的图片验证码,请持续关注我的博客。
实现思路
博主环境: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 中,使用之前必须先导入对应依赖,如下:
com.github.penggle
kaptcha
2.3.2
复制代码
导包成功之后,我们就需要进行按需设置配置类了,它相关配置属性如下:
配置类模板如下:
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
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
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;