在网站实际运用过程中,为了防止网站登录接口被机器人轻易地运用,产生一些没有意义的用户数据,所以,选用验证码进行一定程度上的阻拦,当然,咱们选用的仍是一个数字与字母结合的图片验证码方式,后续会讲到愈加杂乱的数字计算类型的图片验证码,请继续重视我的博客。
完结思路
博主环境:springboot3 、java17、thymeleaf
-
拜访登录页面
-
登录
- 验证验证码
- 验证账号、暗码
- 验证成功时,生成登录凭据,发放给客户端
- 验证失利时,跳转回登录信息,并保留原有填入信息
-
退出
- 将登录凭据修正为失效状况
- 跳转至首页
拜访登录页面的办法已经在前文阐明过了
就不多加赘述了,展示一下代码:
// 登录页面
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String getLoginPage() {
return "/site/login";
}
拜访完登录页面,咱们就要进行信息输入,然而,现在,还没有把验证码信息正确展现出来,所以,接下来,咱们先来完结验证码的部分。
所需两个数据表 SQL 代码如下:
注:注册流程可看前文.一文教你学会完结以邮件激活的注册账户代码 – ()
-- 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;