【项目】开发社区登录模块

社区项目中的登录模块功能开发。主要包含内容:利用Spring Email发送邮件(发送邮件、开发注册功能),Cookie、Session的使用(会话管理、生成验证码、登录和退出),Kaptcha生成验证码工具的使用(生成验证码),拦截器的使用(显示登录信息、检查登录状态),TheadLocal的使用(显示登录信息),MultipartFile上传文件(账号设置)。

发送邮件

  • 邮箱设置:启用客户端SMTP服务

  • Spring Email:

    • 导入jar包

    • 邮箱参数配置

      域名、端口号、邮箱账号、邮箱密码、邮箱协议(smtps表示安全的smtp)、邮箱采用安全连接

    • 使用JavaMailSender发送邮件

      src/main/java/community/util/包下新建工具类:MailClient.class

      变量:

      • mailSender: JavaMailSender类型,Spring Framework 中的一个接口。
      • form:注入发送人的邮箱。

      函数:

      • sendMail函数:

        调用mailSender的createMimeMessage()创建邮件主体。MimeMessageHelper是辅助类,用来构建Mime邮件消息。分别设置了发件人,收件人,邮件主题,邮件内容。

        最后调用mailSender的send()函数发送邮件。

  • 模板引擎:使用Thymeleaf发送HTML邮件

    html模板:

    利用模板引擎格式化html邮件:

开发注册功能

dao层:数据访问层

dao层属于一种比较底层,比较基础的操作,具体到对于某个表的增删改查,也就是说某个DAO一定是和数据库的某一张表一 一对应的,其中封装了增删改查基本操作,建议DAO只做原子操作,增删改查。

负责与数据库进行联络的一些任务都封装在此,dao层的设计首先是设计dao层的接口,然后在Spring的配置文件中定义此接口的实现类,然后就可以再模块中调用此接口来进行数据业务的处理,而不用关心此接口的具体实现类是哪个类,显得结构非常清晰,dao层的数据源配置,以及有关数据库连接参数都在Spring配置文件中进行配置。

service层:服务层

粗略的理解就是对一个或多个DAO进行的再次封装,封装成一个服务,所以这里也就不会是一个原子操作了,需要事物控制。

service层主要负责业务模块的应用逻辑应用设计。同样是首先设计接口,再设计其实现类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用service接口来进行业务处理。service层的业务实,具体要调用已经定义的dao层接口,封装service层业务逻辑有利于通用的业务逻辑的独立性和重复利用性。程序显得非常简洁。

controller层

Controler负责请求转发,接受页面过来的参数,传给Service处理,接到返回值,再传给页面。

controller层负责具体的业务模块流程的控制,在此层要调用service层的接口来控制业务流程,控制的配置也同样是在Spring的配置文件里进行,针对具体的业务流程,会有不同的控制器。我们具体的设计过程可以将流程进行抽象归纳,设计出可以重复利用的子单元流程模块。这样不仅使程序结构变得清晰,也大大减少了代码量。

三层的关系:

Service层是建立在DAO层之上的,建立了DAO层后才可以建立Service层,而Service层又是在Controller层之下的,因而Service层应该既调用DAO层的接口,又要提供接口给Controller层的类来进行调用,它刚好处于一个中间层的位置。每个模型都有一个Service接口,每个接口分别封装各自的业务处理方法。

  • 访问注册页面:点击顶部区域内的链接打开注册页面

    在src/main/java/community/Controller包下新建LoginController.class类,声明注册功能访问路径(get方法)

  • 提交注册数据(实现UserService中的register方法):

    • 服务端验证账号是否已经存在,邮箱是否已经注册

      src/main/java/community/Service包下新建UserService.class类

      public Map<String,Object> register(User user)函数:

      • 验证提交数据是否为空:

      • 验证账号合法性:

      • 注册用户:

    • 服务端发送激活邮件

      还是在public Map<String,Object> register(User user)函数内:

  • 通过表单提交数据(LoginController中实现post方法):

  • 激活注册账号(UserService中activation方法和LoginCotroller中activation方法):

    点击邮件中的链接,访问服务端激活服务:

    • UserService中activation方法提供判断用户是否应该被激活以及激活用户的功能:

    • LoginCotroller中activation方法利用服务端返回的激活状态显示不同的文字:

会话管理

  • HTTP基本性质:

    • HTTP是简单的
    • HTTP是可扩展的
    • HTTP是无状态,有会话的
  • Cookie

    • 是服务器发送到浏览器并保存在浏览器端的一小块数据

    • 浏览器下次访问该服务器时,会自动携带该块数据,将其发送给服务器

  • Session

    • 是JavaEE的标准,用于在服务端记录客户端信息,依赖于Cookie

    • 数据存放在服务端更加安全,但是也会增加服务端的内存压力

生成验证码

  • Kaptcha

    • 编写Kaptcha配置类

      src/main/java/community/Config包下新建KaptchaConfig.class

    • 生成随机字符、生成图片

      LoginController.class类中新建getKaptcha(HttpServletResponse response, HttpSession session)方法

      验证码不能存在客户端,容易被盗取,所以要存在服务器。利用Session存取Kaptcha生成的验证码

      login.html文件中需要做如下改动:

      实现的js方法:

登录和退出功能

Dao层需要实现LoginTicketMapper.class类的增改查功能:

  • 访问登录页面

    • 点击顶部区域内链接,打开登陆页面
  • 登录:实现UserService类中的Map<String,Object> login(String username,String password,long expiredSeconds)方法以及LoginController类中的String login(String username,String password,String code,boolean rememberme,Model model,HttpSession session,HttpServletResponse response)方法

    • 验证账号、密码、验证码

      UserService中的login方法验证账号密码:

      LoginController中的login方法验证验证码和账号密码:

    • 成功时生成登陆凭证(ticket),发放给客户端

      UserService中的login方法:

    • 失败时跳回登录页

      失败后跳回登录页表单中还包含上次填写的数据,html文件中部分逻辑如下:

  • 退出

    • 将登陆凭证修改为失效状态

      UserService类中实现void logout(String ticket)方法修改用户状态:

    • 跳转至网站首页

      loginController类中实现String logout(@CookieValue("ticket") String ticket)方法跳转到首页:

显示登录信息

主要使用拦截器来实现功能。此处注意如果同时配置一个类继承WebMvcConfigurationSupport和一个类实现WebMvcConfigurer或者WebMvcConfigurerAdapter,就会导致只有一个生效。解决办法:将这些配置都在一个类中设置。

拦截器:

spring中拦截器主要分两种,一个是HandlerInterceptor,一个是MethodInterceptorHandlerInterceptor是springMVC项目中的拦截器,它拦截的目标是请求的地址,比MethodInterceptor先执行。

实现一个HandlerInterceptor拦截器可以直接实现HandlerInterceptor接口,也可以继承HandlerInterceptorAdapter类。这两种方法殊途同归,其实HandlerInterceptorAdapter也就是声明了HandlerInterceptor接口中所有方法的默认实现,而我们在继承他之后只需要重写必要的方法。

preHandle: 用来拦截处理器的执行,preHandle方法将在Controller处理之前调用的。SpringMVC里可以同时存在多个interceptor,它们基于链式方式调用,每个interceptor都根据其声明顺序依次执行。这种链式结构可以中断,当方法返回false时整个请求就结束了。

方法的返回值是布尔类型,方法如果返回false,那后面的interceptor和Controller都不会执行(通常都会响应一个自定义的 Http 错误码给客户端)。如果返回值为true,则接着调用下一个interceptor的preHandle方法。如果当前是最后一个interceptor,接下来就会直接调用Controller的处理方法。

postHandle: postHandle 会在Controller方法调用之后,但是在DispatcherServlet 渲染视图之前调用。因此我们可以在这个阶段,对将要返回给客户端的ModelAndView进行操作。

afterCompletion: afterCompletion在当前interceptor的preHandle方法返回true时才执行。该方法会在整个请求处理完成后被调用,就是DispatcherServlet渲染视图完成以后,主要是用来进行资源清理工作。需要注意的是,afterCompletion在interceptor链式结构中以相反的顺序执行,也就是说先申明的interceptor返回会后调用。

拦截器内方法的执行顺序依次是:preHandle -> postHandle -> afterCompletion。

  • 配置拦截器:为拦截器指定拦截、排除的路径

    Config包下新建WebMvcConfig类实现WebMvcConfigurer接口。

  • 拦截器应用

    在Controller包下新建Interceptor包,在Interceptor包中新建LoginTicketInterceptor类实现HandlerInterceptor接口。

    • 在请求开始时查询登录用户

      在LoginTicketInterceptor类中实现boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)方法:

      注意:hostHolder用于代替Session对象来作为一个容器存储user对象,hostHolder中利用ThreadLocal实现线程隔离。

      ThreadLocal:

      ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

      ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的线程中有不同的副本。这里有几点需要注意:

      因为每个线程内有自己的实例副本,且该副本只能由当前Thread使用。这是也是ThreadLocal命名的由来。 既然每个Thread有自己的实例副本,且其它Thread不可访问,那就不存在多线程间共享的问题。 ThreadLocal提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal相对的实例副本都可被回收。

      img

      ThreadLocal与Synchronized的区别: ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。

      但是ThreadLocal与synchronized有本质的区别:

      • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

      • Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

      一句话理解ThreadLocal,Threadlocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值 Entry(Threadlocal,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。 ThreadLocal用法和原理:

      ThreadLocal中主要的方法就是set()、get()、clear()。

      • set()

        ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

      • get()

      • clear()

      ThreadLocal常用场景:

      ThreadLocal主要用于:

      • 每个线程需要有自己单独的实例
      • 实例需要在多个方法中共享,但不希望被多线程共享

      常见的情况有:

      • 存储用户Session
      • 数据库连接,处理数据库事务
      • 数据跨层传递(controller,service, dao)
      • Spring使用ThreadLocal解决线程安全问题
    • 在本次请求中持有用户数据

      在LoginTicketInterceptor类中实现void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)方法:

    • 在模板视图上显示用户数据

    • 在请求结束时清理用户数据

账号设置

  • 上传文件

    • 请求:必须是POST请求
    • 表单:enctype="multipart/form-data"
    • Spring MVC:通过MultipartFile处理上传文件
  • 开发步骤

    • 访问账号设置页面

      在Controller包下新建UserController类,声明String getSettingPage()方法:

    • 上传头像

      配置上传头像的资源位置:

      在application.properties中增加如下配置:

      在UserService类中增加int updateHeader(int userId,String headerUrl)方法:

      在UserController类中增加String uploadHeader(MultipartFile headerImage, Model model)方法上传文件:

    • 获取头像

      在UserController类中增加void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response)方法获取用户头像:

      并在setting.html文件中做上传头像部分相应的修改:

检查登录状态

  • 使用拦截器

    在/user/setting的功能中使用拦截器,如果检测到当前页面不是登录状态,则不允许访问该页面,直接跳转到设置页面。

    • 在方法前标注自定义注解

      在Community下新建annotation包,在该包下新建LoginRequired接口类,声明注解的位置、生效时间:

    • 拦截所有请求,只处理带有该注解的方法

      在UserController类中的getSettingPage和uploadHeader方法上声明两个注解用来拦截请求:

      在Interceptor包下新建LoginRequiredInterceptor类读取注解并做处理:

      并且在Config包下的WebMvcConfig类中声明拦截器作用范围:

  • 自定义注解

    • 常用的元注解:

      @Target@Retention@Document@Inherited

    • 如何读取注解:

      Method.getDeclaredAnnotations()

      Method.getAnnotation(Class annotationClass)

0%