当前位置: 首页 > 后端技术 > Node.js

ThreadLocal源码分析

时间:2023-04-03 23:54:28 Node.js

在网站实际应用过程中,为了防止网站登录界面容易被机器人利用,产生一些无意义的用户数据,在一定程度上采用了验证码拦截。当然,我们还是采用图片验证码形式的数字和字母的组合。后面会讲到比较复杂的数字运算型图片验证码。请继续关注我的博客。实现idea博主环境:springboot3、java17、thymeleaf访问登录页面登录并验证验证码验证账号和密码。验证成功后,生成登录凭证,下发给客户端。验证失败时,跳转回登录信息,保留原来输入的信息exit将登录凭据更改为无效状态跳转到首页,进入登录页面方法上面已经说明,不再赘述详情,显示代码://登录页面@RequestMapping(path="/login",method=RequestMethod.GET)publicStringgetLoginPage(){return"/site/login";}复制代码后访问登录页面,我们需要输入信息。但是验证码信息还没有正确显示,所以,接下来我们先实现验证码的部分。需要的两张数据表的SQL代码如下:注:注册流程请参考上篇文章。本文教大家实现邮箱激活注册账号密码-掘金(juejin.cn)--用户表DROPTABLEIFEXISTS用户;SETcharacter_set_client=utf8mb4;CREATETABLEuser(idint(11)NOTNULLAUTO_INCREMENT,usernamevarchar(50)DEFAULTNULL,passwordvarchar(50)DEFAULTNULL,saltvarchar(50)DEFAULTNULL,emailvarchar(100)DEFAULTNULL,typeint(11)DEFAULTNULLCOMMENT'0-普通用户;1-超级管理员;2-版主;',statusint(11)DEFAULTNULLCOMMENT'0-未激活;1-激活;',activation_codevarchar(100)DEFAULTNULL,header_urlvarchar(200)DEFAULTNULL,create_timetimestampNULLDEFAULTNULL,PRIMARYKEY(id),KEYindex_username(username(20)),KEYindex_email(email(20)))ENGINE=InnoDBAUTO_INCREMENT=101DEFAULTCH=utf8;--登录凭证表DROPTABLEIFEXISTSlogin_ticket;SETcharacter_set_client=utf8mb4;CREATETABLElogin_ticket(idint(11)NOTNULLAUTO_INCREMENT,user_idint(11)NOTNULL,ticketvarchar(45)NOTNULL,statusint(11)DEFAULT'0'COMMENT'1-有效;0-无效的;',过期时间戳NOTNULL,PRIMARYKEY(id),KEYindex_ticket(ticket(20)))ENGINE=InnoDBDEFAULTCHARSET=utf8;复制代码Kaptcha验证码设计与验证目前使用最广泛的图片验证码是Kaptcha,它只有一个版本:2.3.2。值得注意的是,在springboot3的环境下,本插件包中使用的http包大部分不能导入到javax包中,而应该导入到jakarta包中。实现如下效果:有干扰的水纹,无干扰的鱼眼,无干扰的水纹,无干扰的阴影,有干扰的阴影,它们的文字内容限制,背景图片,文字颜色,大小,干涉样式颜色,整体(图片)高度,宽度、画面渲染效果、干扰与否均可定制。我们只需要根据需要配置相应的配置即可。当然默认没有集成到springboot中,使用前必须导入相应的依赖,如下:>2.3.2复制代码导入包成功后,我们需要按需设置配置类。其相关配置属性如下:配置类模板如下:com.google.code.kaptcha.util.Config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importjava.util.Properties;@ConfigurationpublicclassKaptchaConfig{@Beanpublic生产者kaptchaProduce(){属性属性=新属性();//图片宽度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.NoNoiseproperties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");//图像噪声颜色:默认为黑色properties.setProperty("kaptcha.noise.color","black");//图像渲染效果:默认水纹//水纹com.google.code.kaptcha.impl.WaterRippleFisheyecom.google.code.kaptcha.impl.FishEyeGimpyShadowcom.google.code.kaptcha.impl.ShadowGimpy//properties.setProperty("kaptcha.obscurificator.impl","com.google.code.kaptcha.impl.暗影金比");DefaultKaptchaKaptcha=newDefaultKaptcha();配置配置=新配置(属性);Kaptcha.setConfig(配置);returnKaptcha;}}复制代码并配置好相关属性后,我们就可以开发生成验证码的接口了。首先让Producer进入Bean工厂进行管理,之后生成验证码文本传入session中进行后续的验证码校验,然后生成对应的验证码图片,以BufferedImage的形式存储,使用HttpServletResponse和ImageIO向浏览器传输图片,其中,注意设置图片返回类型,无需手动关闭IO流,springboot会管理,实现自关闭。此时使用Get方法访问域名/imageCode,会返回对应的验证码图片。//验证码@RequestMapping(path="/imageCode",method=RequestMethod.GET)publicvoidgetImgCode(HttpServletResponseresponse,HttpSessionsession){StringcodeText=imageCodeProducer.createText();BufferedImageimageCode=imageCodeProducer.createImage(codeText);//将验证码文本存入sessionsession.setAttribute("imageCode",codeText);//设置返回类型response.setContentType("image/jpg");try{OutputStreamos=response.getOutputStream();ImageIO.write(imageCode,"jpg",os);}catch(IOExceptione){logger.error("响应验证码失败!"+e.getMessage());}}复制代码当然是为了省事用户访问流量,一些浏览器,自动停止访问获取的静态资源链接,是比较智能的。因此需要增加额外的参数来完成浏览器适配。这里使用JavaScript在每次访问验证码图片的链接中加入一个随机数参数,以保证智能省流量的问题。当然,我们不需要去controller中去获取这个参数,因为它没有意义,并不要求所有的参数都匹配。代码如下:functionrefresh_imageCode(){varpath="/imageCode?p="+Math.random();$("#imageCode").attr("src",path);}将代码复制到拿到验证码,一定要校对一下。验证码通过后,我们才能对账号和密码进行验证。验证码校对最重要的一点就是需要忽略大小写,不能苛求用户的耐心。在验证验证码失败时,不仅需要考虑发送方的验证码文本为空或者文本不一致导致的错误,还需要考虑接收方(服务器)的验证码文本是否已经存储,以防发生从通过界面。工具直接post访问该接口生成的空数据。代码如下://login@RequestMapping(path="/login",method=RequestMethod.POST)publicStringlogin(Stringusername,Stringpassword,Stringcode,booleanrememberMe,Model模型,HttpSession会话,HttpServletResponse响应){StringimageCode=(String)session.getAttribute("imageCode");//验证码if(StringUtils.isBlank(imageCode)||StringUtils.isBlank(code)||!imageCode.equalsIgnoreCase(code)){model.addAttribute("codeMsg","验证码有误!");return"/site/login";}}复制代码实现记住我的功能用户在登录的时候,经常需要勾选是否记住我的按钮。始终使用该应用程序,不会因频繁登录而失去用户。当然,也有一些用户不希望自己的用户凭证被长期保存,希望通过频繁的更新来保证一定程度的用户数据安全。这个功能实现起来并不难,只要在发送数据的时候多加一个布尔参数即可。为了代码阅读方便,增加两个常量:登录默认状态超时时间常量,记住我登录状态超时时间常量,如下://默认登录状态超时常量intDEFAULT_EXPIRED_SECONDS=3600*12;//记住状态登录凭证超时时间intREMEMBER_EXPIRED_SECONDS=360024100;复制代码后,只要在登录界面判断,rememberme布尔值为true,所以代码如下://你还记得我吗?intexpiredSeconds=rememberMe?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS;按照标准流程复制验证账号和密码的代码,先从数据访问层开始写,我们用查询语句来验证账号和密码,当然一个查询语句就可以了,没必要为两个参数创建一个查询语句,因为我们已经获得了这个对象,直接使用映射方法中的get方法即可,然后进行需要的校验工作。这里使用username为参数的查询语句获取用户对象。具体代码如下:userMapper.javaUserselectOneByUsername(@Param("username")Stringusername);复制代码fromuserwhereusername=#{username,jdbcType=VARCHAR}使用这个查询复制代码之前,首先要保证传入的账号和密码不能为空,这样查询是有意义的。获取用户对象后,我们首先验证账号是否存在。如果不存在,只返回错误信息。如果存在,则检查其账户状态是否已激活,如果没有,则返回错误信息,如果是,我们就可以进行验证工作,当然如果账户存在,则不需要验证用户名,只需要验证需要验证密码。代码如下://空值处理){map.put("passwordMsg","密码不能为空!");returnmap;}//验证账号用户user=userMapper.selectOneByUsername(username);if(user==null){map.put("usernameMsg","账号不存在");returnmap;}//验证状态if(user.getStatus()==0){map.put("usernameMsg","账号未激活!");returnmap;}//验证密码password=CommunityUtil.md5(password+user.getSalt());if(!user.getPassword().equals(password)){map.put("passwordMsg","密码为不正确!”);returnmap;}复制代码当账号密码验证成功后,只需要将登录凭证存入cookie中,设置全局可用和过期时间,只要设置登录凭证过期时间,后续客户端会自动到达时间和将登录凭据注销,以便我们可以取消登录状态。如果验证不成功,则直接返回验证信息。在登录界面调用即可//检测账号密码Mapmap=userServiceImpl.login(username,password,expiredSeconds);if(map.containsKey("loginTicket")){//SetcookieCookiecookie=newCookie("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语句如下:insertintologin_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方法将需要的参数存储到对象中,然后使用相应的insert语句插入到数据库中。注意默认有效状态为1,生成登录凭据的登录接口代码如下:.generateUUID());loginTicket.setStatus(1);loginTicket.setExpired(新日期(System.currentTimeMillis()+expiredSeconds*1000));loginTicketMapper.insertAll(loginTicket);map.put("loginTicket",loginTicket.getTicket());返回地图;有没有发现一个问题:当过期时间到了,状态依然有效。我们的登录凭证的有效状态是后续显示登录信息的关键。我们还会考虑如何在时间到期后自动修改有效状态。或者如何解决过期时间到了状态仍然有效的问题,而不做任何更改。请继续关注博主,稍后为您解答。将登录凭据发送给客户端就基本完成了登录的实现。相关代码资源已经上传,可以看到:项目代码相关bugNoprimaryorsingleuniqueconstructorfoundforinterfacejavax.servlet.http.HttpServletResponsespringboot3cannotimportjavax.servlet.httppackage,mustimportjakarta.servlet.httpwhichisalsohttp包改变了。导入jakarta.servlet.http.HttpServletResponse;导入jakarta.servlet.http.HttpSession;不能导入复制代码,否则会出错。导入javax.servlet.http.HttpServletResponse;导入javax.servlet.http.HttpSession;