湖州seo排名,seo流量增加软件,世界500强,wordpress插件破解下载地址《Redis-day02-短信登陆》
0. 今日总结了解了项目大致结构和待实现的功能复习了会话及会话跟踪技术#xff0c;主要复习Cookie技术和Session技术实现了发送短信验证码业务功能实现了短信验证码登录、注册功能#xff0c;了解了mybatis-plus的基础用法深入理解了ThreadLocal的…《Redis-day02-短信登陆》0. 今日总结了解了项目大致结构和待实现的功能复习了会话及会话跟踪技术主要复习Cookie技术和Session技术实现了发送短信验证码业务功能实现了短信验证码登录、注册功能了解了mybatis-plus的基础用法深入理解了ThreadLocal的原理以及ThreadLocalMap中Entry弱引用而key强引用可能导致的内存泄露问题以及该问题的解决方案。基于ThreadLocal, Intercepter实现了登录状态校验使用redis实现了共享session登录实现了发送短信验证码并设置了有效时间为两分钟实现了短信验证码登录理解了RedisTemplate和StringRedisTemplate的区别深入理解了spirng的自动依赖注入并实现了登陆状态的校验对登录拦截器进行了优化添加了一个新的拦截器专门负责对所有请求进行token刷新操作原本的拦截器则专门进行登录状态校验1. 导入黑马点评项目后端部署在tomcat服务器上前端部署在NGINX服务器上1.1 导入数据库涉及的表1.2 导入后端项目修改数据库和Redis的配置修改mybatis-plus配置1.3 导入前端直接粘贴打包好的nginx服务器即可2. 基于Session实现登录2.1 会话及会话跟踪技术复习2.1.1 会话用户打开浏览器访问web服务器的资源会话建立直到有一方断开连接会话结束。在一次会话中可以包含多次请求和响应。一个浏览器与服务器的连接就是一个会话下图包含三给会话2.1.2 会话跟踪一种维护浏览器状态的方法服务器需要识别多次请求是否来自于同一浏览器以便在同一次会话的多次请求间共享数据。2.1.2.1 Cookie存储在客户端缺点移动端APP无法使用Cookie不安全用户可以自己禁用Cookie不能跨域请求头Cookie用于携带Cookie数据响应头Set-Cookie用于设置Cookie数据通过HttpServletResponse获得响应对象response通过response.addCookie设置CookieSet-Cookie通过HttpServletRequest获得响应对象request通过request的getCookies获取所有cookiesCookie2.1.2.2 Session存储在服务器端缺点服务器集群环境下无法直接使用Session移动端APP(Android、IOS)中无法使用Cookie用户可以自己禁用CookieCookie不能跨域Slf4jRestControllerpublicclassSessionController{GetMapping(/s1)publicResultsession1(HttpSessionsession){log.info(HttpSession-s1: {},session.hashCode());session.setAttribute(loginUser,tom);//往session中存储数据returnResult.success();}GetMapping(/s2)publicResultsession2(HttpServletRequestrequest){HttpSessionsessionrequest.getSession();log.info(HttpSession-s2: {},session.hashCode());ObjectloginUsersession.getAttribute(loginUser);//从session中获取数据log.info(loginUser: {},loginUser);returnResult.success(loginUser);}}流程客户端请求 GET /s1 ↓ 服务器: 接收到请求没有JSESSIONID ↓ 服务器: 创建新的Session对象自动生成唯一Session ID ↓ 服务器: 在Session对象中设置属性: loginUser → tom ↓ 服务器响应: Set-Cookie: JSESSIONIDA329DBD06E63DF28EBD2029916575565 ↓ 浏览器: 保存JSESSIONID到Cookie ------------------------------------------------ 客户端请求 GET /s2 ↓ 浏览器: 自动附加Cookie: JSESSIONIDA329DBD06E63DF28EBD2029916575565 ↓ 服务器: 收到JSESSIONID去Session存储表中查找 ↓ 服务器: 找到对应的Session对象 ↓ 服务器: 调用session.getAttribute(loginUser)返回tom2.1.2.3 jwt令牌技术以及拦截器和过滤器实现步骤在浏览器发起请求来执行登录操作此时会访问登录的接口如果登录成功之后我们需要生成一个jwt令牌将生成的 jwt令牌返回给前端。前端拿到jwt令牌之后会将jwt令牌存储起来JWT令牌存储在浏览器的本地存储空间local storage中。在后续的每一次请求中都会将jwt令牌携带到服务端。服务端统一拦截请求之后先来判断一下这次请求有没有把令牌带过来如果没有带过来直接拒绝访问如果带过来了还要校验一下令牌是否是有效。如果有效就直接放行进行请求的处理。Filter过滤器过滤器当中我们拦截到了请求之后如果希望继续访问后面的web资源就要执行放行操作放行就是调用 FilterChain对象当中的doFilter()方法在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。Interceptor拦截器通过实现HandlerInterceptor接口或继承HandlerInterceptorAdapter类主要重写三个方法preHandle在Controller方法之前执行。返回true则放行返回false则中断流程postHandle在Controller方法执行之后视图渲染之前执行。afterCompletion在整个请求完成视图渲染完毕之后执行常用于资源清理2.2 发送短信验证码controller获取前端发来的phone和session将phone和session传入service层service校验手机号通过RegexUtils工具类的方法生成验证码通过RandomUtil的方法调用session的setAttribute将该sessionId的数据添加一条code值为刚刚生成的验证码模拟发送验证码功能返回发送从成功2.3 短信验证码登录、注册controller接收DTO数据接收session对象service通过RegexUtils工具类检验手机号是否正确获取session中的code并将其与前端传给后端的code进行对比如果不一致则报错如果一直则根据手机号通过mybatis-plus的query()方法查询用户如果用户不存在则创建用户给用户的手机号、创建时间、昵称赋值并通过mybatis-plus的save方法将用户保存到数据库中如果用户存在则将该用户信息保存到session会话中2.4 校验登陆状态2.4.1 ThreadLocal详解2.4.1.1 ThreadLocal原理提供线程之间的局部变量不同线程的变量不会相互干扰下面是其基本原理Thread 类中有一个成员变量 ThreadLocalMap它是一个 Map 结构ThreadLocalMap 的 key 是 ThreadLocal 对象的弱引用value 是具体的值当调用 ThreadLocal 的 set(T value) 方法时会先获取当前线程然后将值存储在当前线程的 ThreadLocalMap 中当调用 ThreadLocal 的 get() 方法时会从当前线程的 ThreadLocalMap 中获取值publicvoidset(Tvalue){// 获取当前线程ThreadtThread.currentThread();// 获取当前线程的 ThreadLocalMapThreadLocalMapmapgetMap(t);// 如果 map 存在则直接设置值if(map!null)map.set(this,value);else// 否则创建 map 并设置值createMap(t,value);}publicTget(){// 获取当前线程ThreadtThread.currentThread();// 获取当前线程的 ThreadLocalMapThreadLocalMapmapgetMap(t);// 如果 map 存在if(map!null){// 获取与当前 ThreadLocal 对象关联的 EntryThreadLocalMap.Entryemap.getEntry(this);if(e!null){SuppressWarnings(unchecked)// 返回值Tresult(T)e.value;returnresult;}}// 如果 map 不存在或 entry 不存在则返回初始值returnsetInitialValue();}publicvoidremove(){// 获取当前线程的 ThreadLocalMapThreadLocalMapmgetMap(Thread.currentThread());// 如果 map 存在则从中删除当前 ThreadLocal 对应的 entryif(m!null)m.remove(this);}2.4.1.2 内存泄漏问题和解决方法原因ThreadLocal 使用不当可能导致内存泄漏主要原因有两点ThreadLocalMap 的 Entry 是弱引用ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key这意味着当没有强引用指向 ThreadLocal 变量时它会被垃圾回收。但是对应的 value 是强引用如果没有手动删除就无法被回收。假设你在方法中创建了一个ThreadLocal局部变量并使用set()存入一个大对象。方法结束后ThreadLocal实例的强引用消失。在下次垃圾回收GC时由于 key 是弱引用它会被回收于是这个Entry的 key 变为null。然而value 因为仍是强引用所以不会被 GC 回收。只要这个线程例如 Web 服务器中的工作线程本身不死比如被线程池回收复用这个线程强引用 →ThreadLocalMap→Entry→ value 的强引用链就会一直存在导致这个 value 对应的对象永远无法被回收造成内存泄漏 。如果这种情况频繁发生就可能耗尽内存。一般的应用都是强引用弱引用是用WeakReference类创建的对象// 强引用ObjectstrongObjnewObject();// 弱引用WeakReferenceObjectweakObjnewWeakReference(newObject());线程池中的线程生命周期很长在使用线程池的场景下线程的生命周期可能很长甚至与应用程序的生命周期一样长。如果不清理 ThreadLocal 变量那么这些变量会随着线程一直存在于内存中。解决方案正因为上述原因在使用完ThreadLocal后必须手动调用remove()方法。这个方法会直接清除当前线程的ThreadLocalMap中对应 key 的整个Entry从而彻底打破引用链让 value 能够被 GC 回收 。2.4.2 登录状态校验实现拦截器实现实现HandlerInterceptor类并重写preHandle和afterCompletion方法preHandle方法内通过request.getSession获得请求体的session然后调用getAttribute获得服务器中的session保存的user的值如果user为空则表示不存在该用户return false进行拦截如果user不为空则表示该session中确实存在user则意味着已经完成了登录则将当前用户信息保存到ThreadLocal中并放行拦截当前线程结束后移除ThreadLocal中的内容以避免内存泄露配置类实现自动拦截拦截器会在springboot项目启动时自动拦截Controller类中的请求除了上述排除的请求ThreadLocal实现saveUser方法调用了ThreadLocal的set方法将传入的User对象保存在当前线程中getUser方法调用了ThreadLocal的get方法用于获取当前线程中的User对象removeUser方法调用了ThreadLocal的remove方法用于清空当前线程2.4.3 优化将登录过程存到Session中user的对象由User改为UserDTO以减小存储压力并同时修改了一系列由于该改动引发的问题3. 集群的session共享问题3.1 共享问题**session共享问题**多台Tomcat并不共享session存储空间当请求切换到不同tomcat服务时导致数据丢失的问题。session的替代方案应该满足:数据共享内存存储key、value结构3.2 替代方案使用Redis替代session3.2.1 Redis数据结构选择验证码可以用String来存储key表示电话号码value表示验证码用户信息可以用Hash来存储key表示随机tokenvalue表示用户对象各个字段及字段对应值这样可以更加方便的获取或修改具体字段实现流程发送短信验证码后将发送验证码的手机号作为key并将验证码作为值存储在redis中通过短信验证码登录、注册进行校验验证码时根据提交的手机号和验证码去redis中进行比对如果用户存在则将用户保存到redis如果用户不存在则创建新用户将用户保存到数据库接着保存到rediskey为随机tokenvalue为用户对象的各个字段和数据接着将token返回给客户端如果在此之前已经存在token则当前token会覆盖之前的token在校验登录状态时从客户端获取token直接从redis获取用户信息4. 基于Redis实现共享session登录4.1 发送短信验证码校验手机号生成验证码将验证码保存到Redis保存类型为String类型key“login:code:phone”,valuecode值并设置有效时间为两分钟4.2 短信验证码登录、注册OverridepublicResultlogin(LoginFormDTOloginForm,HttpSessionsession){//1. 校验手机号StringphoneloginForm.getPhone();if(RegexUtils.isPhoneInvalid(phone)){//2. 如果不符合返回错误信息returnResult.fail(手机号格式错误);}//3. 从redis获取验证码并校验StringcacheCodestringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEYphone);StringcodeloginForm.getCode();if(cacheCodenull||!cacheCode.toString().equals(code)){//3. 不一致直接报错returnResult.fail(验证码错误);}//4. 根据手机号查询用户Useruserquery().eq(phone,phone).one();//5. 判断用户是否存在if(usernull){//6. 不存在创建新用户usercreateUserWithPhone(phone);}//7. 存在保存用户信息到redis中//7.1 随机生成tokenStringtokenUUID.randomUUID().toString(true);//7.2 将User对象转为HashMap存储UserDTOuserDTOBeanUtil.copyProperties(user,UserDTO.class);MapString,ObjectuserMapBeanUtil.beanToMap(userDTO,newHashMap(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)-fieldValue.toString()));//7.3 存储数据到redisStringtokenKeyRedisConstants.LOGIN_USER_KEYtoken;stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);//7.4设置token有效期stringRedisTemplate.expire(tokenKey,30,TimeUnit.MINUTES);//8. 返回tokenreturnResult.ok(token);}校验手机号从redis中获取验证码调用stringRedisTemplate的opsForValue方法操作字符串类型的redis数据再调用get方法获取key login:code:phone的value值保存为cacheCode如果cacheCodenull说明redis中该手机号不存在对应的验证码如果cacheCode!code说明Redis中的验证码和用户端发送给服务端的验证码不一致以上两种情况都报错如果验证码成功则根据手机号查询用户并判断用户是否存在不存在则创建新用户存在或创建完新用户之后随机生成token调用Beanutil的copyProperties将user转换为userDTO对象为了能够一次性将userDTO中的所有字段一次性保存到Redis的Hash结构中要将userDTO转化为Map但是由于stringRedisTemplate所有键和值都必须是字符串或可转为字符串的形式而userDTO中的id是Long类型会出现类型转换异常因此在将userDTO转化为Map时要将所有字段都转化为String类型调用BeanUtil的beanToMap方法将userDTO转化为hashMap结构CopyOptions.create()精细修改map中的每个字段setIgnoreNullValue忽略空值。如果userDTO的某个属性值为null它将不会放入Map中.setFieldValueEditor((fieldName, fieldValue) - fieldValue.toString())将value转化为String即将map中每个字段都转化为StringstringRedisTemplate和RedisTemplate的区别stringRedisTemplate支持手动序列化RedisTemplate只能自动序列化stringRedisTemplate所有键和值都必须是字符串或可转为字符串的形式RedisTemplate则不需要或改为以下形式不用工具类而是挨个字段put并将id转化为String字符串一次性将map存储到redis将token返回给前端4.3 校验登陆状态创建stringRedisTemplate对象并通过构造器初始化因为该类不是spring容器在创建该类的对象时是直接new出来的因此不能通过autowierd自动注入你的LoginInterceptor不能注入StringRedisTemplate问题出在MvcConfig的addInterceptors方法中你是通过new LoginInterceptor(stringRedisTemplate)来创建拦截器实例的。这个new关键字创建的是一个全新的、普通的Java对象不是Spring容器管理的Bean。因此即使在LoginInterceptor类内部使用了AutowiredSpring也不会为这个手动创建的对象执行依赖注入流程 。其内部的StringRedisTemplate字段自然是null。获取请求头中的token基于token获取redis中的用户用entries方法获得map集合判断用户是否存在如果存在则转化为userDTO对象并将用户保存在ThreadLocal中刷新token有效期4.4 登录拦截器的优化问题当前拦截器只会拦截部分业务如果用户登录完成后始终停留在没有被拦截的界面则不会启动token有效期自动刷新解决方案再加一层拦截器拦截所有的请求但是只在该拦截器进行token刷新业务不进行实际拦截packagecom.hmdp.interceptor;importcn.hutool.core.bean.BeanUtil;importcn.hutool.core.util.StrUtil;importcom.hmdp.dto.UserDTO;importcom.hmdp.entity.User;importcom.hmdp.utils.RedisConstants;importcom.hmdp.utils.UserHolder;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.web.servlet.HandlerInterceptor;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;importjava.util.Map;importjava.util.concurrent.TimeUnit;publicclassRefreshTokenInterceptorimplementsHandlerInterceptor{privateStringRedisTemplatestringRedisTemplate;publicRefreshTokenInterceptor(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplatestringRedisTemplate;}OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{//1.获取请求头中的tokenStringtokenrequest.getHeader(authorization);if(StrUtil.isBlank(token)){returntrue;}//2.基于Token获取redis中的用户MapObject,ObjectuserMapstringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEYtoken);//3.判断用户是否存在if(userMap.isEmpty()){returntrue;}//5.将查询到了Hash数据转换为UserDTO对象UserDTOuserDTOBeanUtil.fillBeanWithMap(userMap,newUserDTO(),false);//6.存在保存用户信息 到ThreadLocalUserHolder.saveUser((UserDTO)userDTO);//7.刷新token有效期stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEYtoken,30,TimeUnit.MINUTES);//8.放行returntrue;}OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{//线程执行结束后处理UserHolder.removeUser();}}上述拦截器仅负责token刷新业务上述拦截器仅判断当前ThreadLocal是否有用户如果没有则拦截