共计 8379 个字符,预计需要花费 21 分钟才能阅读完成。
在网站理论利用过程中,为了避免网站登录接口被机器人轻易地应用,产生一些没有意义的用户数据,所以,采纳验证码进行肯定水平上的拦挡,当然,咱们采纳的还是一个数字与字母联合的图片验证码模式,后续会讲到更加简单的数字计算类型的图片验证码,请继续关注我的博客。
实现思路
博主环境: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 中,应用之前必须先导入对应依赖,如下:
<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;