社区项目2

1.发送邮件

邮箱设置 :启用客户端SMTP服务,程序中所填的邮箱密码要为开通pop3/smtp服务时所给的授权码。
Spring Email:导入jar包,邮箱参数设置,使用JavaMailSender发送邮件
模板引擎:使用Thymeleaf发送HTML邮件

2.开发注册功能

web项目每一个功能可以按照请求去分解,浏览器与服务器之间的交互
每次请求用三层架构去开发
访问注册页面

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

提交注册数据

  • 通过表单提交数据
<!--action规定当提交表单时向何处(@{/register})发送表单数据,提交的方式是post-->
<form class="mt-5" method="post" th:action="@{/register}">
  • 服务端验证账号是否存在、邮箱是否注册
  • 服务端发送激活邮件

激活注册账号

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

StringUtils 判断为空的情况
throw new IllegalArgumentException(“参数不能为空”);????

点击注册按钮,submit表示把信息提交给服务器端
立即注册

<a> 标签定义超链接,用于从一个页面链接到另一个页面。
<a> 元素最重要的属性是 href 属性,它指定链接的目标。

<a href="http://www.abc.com">访问!</a>

1.register页面上,输入的值是user的属性,自动注入user中传给loginservice

2.通过TemplateEngine 和Context 的配合,我们可以使用thymeleaf模版来生产html文件。

//context中的内容通过thymeleaf,传给HTML,生成动态的模板
Context context = new Context();
context.setVariable("email", user.getEmail());
//http://localhost:8080/community/activation/101/code(激活码)
// 激活路径自己设置的样子,下面要自己动态的拼出来
String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
 context.setVariable("url", url);
String content = templateEngine.process("/mail/activation", context);

Context是给thymeleaf模版提供变量的。

3.判断密码是否一致,是在底层js中进行判断的

4.<label> 标签为 input 元素定义标注(标记)。

label 元素不会向用户呈现任何特殊效果。不过,它为鼠标用户改进了可用性。如果您在 label 元素内点击文本,就会触发此控件。就是说,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上。

<label> 标签的 for 属性应当与相关元素的 id 属性相同。

  1. <Input>
    (https://www.runoob.com/tags/tag-input.html)
    name 属性规定 元素的名称。
    name 属性用于在 JavaScript 中引用元素,或者在表单提交后引用表单数据。
    注意:只有设置了 name 属性的表单元素才能在提交表单时传递它们的值。
    Spring MVC基于同名原则,把input里的name属性值传给controller的user参数里的同名属性
<input type="text" 
 th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
 th:value="${user!=null?user.username:''}"
 id="username" name="username" placeholder="请输入您的账号!" required>
@RequestMapping(path = "/register", method = RequestMethod.POST)
public String register(Model model, User user) { }

input 中输入 name=“username” 提交数据的时候把数据提交到user的username属性里

6.Session:记录一系列状态
Session与cookie功能效果相同。Session与Cookie的区别在于Session是记录在服务端的,而Cookie是记录在客户端的
7.response.setContentType(MIME)的作用是使客户端浏览器,区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。【就是在设置响应编码】
8.HttpServletResponse对象代表服务器的响应。这个对象中封装了向客户端发送数据、发送响应头,发送响应状态码的方法。查看HttpServletResponse的API,可以看到这些相关的方法
9. 跳转到登陆界面的时候,会通过th:src="@{/kaptcha} 找到相应的controller,生成相应的验证码图片

<!--从th:src="@{/kaptcha}"处将图像链接到 HTML 页面上。<img> 标签的作用是为被引用的图像创建占位符-->
<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/>
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session) {

3.会话管理

  • HTTP基本性质
    HTTP是无状态的,有会话的
    HTTP是简单的
    HTTP是可扩展的
  • Cookie
    浏览器第一次访问服务器时,服务器会产生cookie,然后把cookie放到返回信息的头部里,浏览器把cookie保存起来
    浏览器再次发送请求给服务器时,会把cookie放到发送头部里,服务器就知道浏览器的状态了
    存到客户端中的信息是不安全的,cookie每次都发送会占用空间
    在这里插入图片描述
// cookie示例

    @RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
    @ResponseBody
    public String setCookie(HttpServletResponse response) {
        // 创建cookie
        Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
        // 设置cookie生效的范围,在下面这个路径和子路径时
        cookie.setPath("/community/alpha");
        // 设置cookie的生存时间(不设置时默认是关闭浏览器后cookie会消失)
        cookie.setMaxAge(60 * 10);
        // 发送cookie,放到response的头部字段里
        response.addCookie(cookie);
        
        return "set cookie";
    }

    @RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
    @ResponseBody
    //@CookieValue("code") 从cookie中取到键为code的这个值,放到String code里
    public String getCookie(@CookieValue("code") String code) {
        System.out.println(code);
        return "get cookie";
    }
  • Seesion
    是javaEE的标准,用于在服务端记录用户的信息
    数据存放在服务端更安全,但是会增加服务端的内存压力
    seesion本身是依赖cookie的
    在这里插入图片描述
    // session示例
    @RequestMapping(path = "/session/set", method = RequestMethod.GET)
    @ResponseBody
    //session不用自己手动创建,和model一样,声明spring MVC就会注入进来
    public String setSession(HttpSession session) {
        session.setAttribute("id", 1);
        session.setAttribute("name", "Test");
        return "set session";
    }

    @RequestMapping(path = "/session/get", method = RequestMethod.GET)
    @ResponseBody
    public String getSession(HttpSession session) {
        System.out.println(session.getAttribute("id"));
        System.out.println(session.getAttribute("name"));
        return "get session";
    }

分布式部署为什么使用session会有问题?

生成验证码

  • Kaptcha
    导入jar包
    编写Kaptcha配置类
    生成字符串、生成图片
<!--Kaptcha-->
		<dependency>
			<groupId>com.github.penggle</groupId>
			<artifactId>kaptcha</artifactId>
			<version>2.3.2</version>
		</dependency>
//这是一个配置类
@Configuration
public class KaptchaConfig {
    //@Bean注解的可以被Spring容器管理、装配
    @Bean
    //Kaptcha的核心接口是Producer
    //方法名就是Bean的名字
    public Producer kaptchaProducer() {
        //properties类去设置相应的配置,再放入下面的config中,再放入到kaptcha中
        Properties properties = new Properties();
        //图宽度、高度、字号、字颜色、随机字符范围、随机字符的长度、没有干扰
        properties.setProperty("kaptcha.image.width", "100");
        properties.setProperty("kaptcha.image.height", "40");
        properties.setProperty("kaptcha.textproducer.font.size", "32");
        properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");

        //DefaultKaptcha是producer接口的实现类
        //把传入的数据放到config对象中
        //config依赖properties来装入数据
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

 @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    //<img th:src="@{/kaptcha}" id="kaptcha" />
    //通过URL映射,访问这个方法,产生图片
    //返回void,因为向浏览器返回的是一个特殊的东西(图片,不是字符串也不是网页),因此要用response对象手动输出
    // 跨请求的信息用session存下来
    public void getKaptcha(HttpServletResponse response, HttpSession session) {
        // 生成验证码文字和图片,根据配置类的情况
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);

        // 将验证码存入session
        session.setAttribute("kaptcha", text);

        // 将验证码图片输出给浏览器
        // 人工输出先声明返回什么类型的数据
        response.setContentType("image/png");
        try {
            //生成输出流
            OutputStream os = response.getOutputStream();
            // 把图片image编码成png格式字符串,然后把数据写入到os数据流中
            ImageIO.write(image, "png", os);
        } catch (IOException e) {
            logger.error("响应验证码失败:" + e.getMessage());
        }
    }

开发登陆、退出功能

  • 访问登录页面
    点击顶部区域链接,打开登录页面
  • 登录
    验证账号、密码、验证码
    成功时,生成登录凭证,发放给客户端
    失败时,跳转回登录页面
  • 退出
    将登录凭证修改为失效状态
    跳转网页首页

登录:实体类LoginTicket封装从数据库得到的表属性信息–>LoginTicketMapper来进行相应的数据库操作–>UserService编写相应的业务处理逻辑(login)–>UserController进行相应的登录判断(login)<–>表单信息提交到controller层,controller层处理完以后结果返回到页面

	//参数是实体Spring MVC会把实体自动装入到model里,而如果是普通的字符串等的值,Spring不会把参数放到model里
    //两个方法1.认为放到model里;2.从request中去取
    @RequestMapping(path = "/login", method = RequestMethod.POST)
    //<form class="mt-5" method="post" th:action="@{/login}"> 表单里的内容会通过post提交到这里来
    //rememberme,页面上为是否记住密码,cookie保存登录凭证,创建cookie需要HttpServletResponse对象
    public String login(String username, String password, String code, boolean rememberme,
                        Model model, HttpSession session, HttpServletResponse response) {
        // 检查验证码
        String kaptcha = (String) session.getAttribute("kaptcha");
        //kaptcha.equalsIgnoreCase(code) 不区分大小写
        if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
            model.addAttribute("codeMsg", "验证码不正确!");
            return "/site/login";
        }

        // 检查账号,密码 (REMEMBER_EXPIRED_SECONDS是CommunityConstant接口中的常量)
        //expiredSeconds 登录凭证超时时间,根据是否勾选记住我来区分
        int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
        Map<String, Object> map = userService.login(username, password, expiredSeconds);
        //登录成功时,map中使有登录凭证的
        if (map.containsKey("ticket")) {
            //cookie里存ticket(登录凭证)
            Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
            //cookie 有效的范围
            cookie.setPath(contextPath);
            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";
        }
    }

    @RequestMapping(path = "/logout", method = RequestMethod.GET)
    //把ticket传过来,并进行相应的退出操作,把
    public String logout(@CookieValue("ticket") String ticket) {
        userService.logout(ticket);
        //重定向默认是get请求
        return "redirect:/login";
    }
<!--${param.username},通过request获取到的值相当于request.getparameter(username)-->
<!--th:value 里面的值是当再次回到这个页面时,应该留有当时填入的值-->
<!--class="|form-control ${usernameMsg!=null?'is-invalid':''}|,首次进入这个页面时是form-control,再次回来时去判断是否is-invalid,是is-invalid会访问下面账号不存在的节-->
	<input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"
	 	th:value="${param.username}"
	 	id="username" name="username" placeholder="请输入您的账号!" required>
	<div class="invalid-feedback" th:text="${usernameMsg}">
		该账号不存在!
	</div>

显示登录信息

  • 拦截器实例
    定义拦截器,实现HandlerInterceptor
    配置拦截器,为它指定拦截、排除的路径
  • 拦截器应用
    在请求开始时查询登录用户
    在本次请求中持有用户数据(存到内存里)
    在模板视图上显示用户数据(存入model里返回给模板)
    在请求结束时清理用户数据

两种方式配置拦截器拦截的内容:1. 配置拦截器配置类,为它指定拦截、排除的路径 2.使用自定义注解,拦截有注解的方法

//定义拦截器类
//拦截器要实现HandlerInterceptor接口
@Component
public class AlphaInterceptor implements HandlerInterceptor {
    //产生一个日志对象
    private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);

    // 在Controller之前执行,在请求的代码之前执行,return false的话controller不会执行
    // Object handler:是配置类中配置的拦截的目标
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.debug("preHandle: " + handler.toString());
        return true;
    }
    // 在Controller之后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.debug("postHandle: " + handler.toString());
    }
    // 在TemplateEngine之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.debug("afterCompletion: " + handler.toString());
    }
}
//拦截器的配置类,这个配置类不是为了实现bean,而是实现接口WebMvcConfigurer
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private AlphaInterceptor alphaInterceptor;
    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;
    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;
    //利用传进来的参数对象来实现Interceptor
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //addInterceptor拦截的内容,excludePathPatterns排除在外不拦截的内容,addPathPatterns拦截的路径,不是这些路径的不拦截
        registry.addInterceptor(alphaInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
                .addPathPatterns("/register", "/login");

        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");

        registry.addInterceptor(loginRequiredInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }

}

账号设置

  • 上传文件
    请求:必须是post请求
    表单:enctype = “multipart/form - data”
    Spring MVC:通过MultipartFile处理上传文件
  • 开发步骤
    访问账号设置页面
    上传头像
    获取头像

响应图片的形式
response.setContentType(“image/” + suffix);

    //访问账号设置页面
    @LoginRequired //自定义注解
    @RequestMapping(path = "/setting", method = RequestMethod.GET)
    public String getSettingPage() {
        return "/site/setting";
    }
    //上传头像,上传时表单提交方式必须为post
    //Spring MVC:通过MultipartFile处理上传文件
    @LoginRequired
    @RequestMapping(path = "/upload", method = RequestMethod.POST)
    public String uploadHeader(MultipartFile headerImage, Model model) {
        if (headerImage == null) {
            model.addAttribute("error", "您还没有选择图片!");
            return "/site/setting";
        }
        //给文件生成随机名字,但后缀不变
        String fileName = headerImage.getOriginalFilename();//获得原始文件名
        String suffix = fileName.substring(fileName.lastIndexOf("."));//从最后一个点的索引向后取
        if (StringUtils.isBlank(suffix)) {//如果没有后缀
            model.addAttribute("error", "文件的格式不正确!");
            return "/site/setting";
        }
        // 生成随机文件名
        fileName = CommunityUtil.generateUUID() + suffix;
        // 确定文件存放的路径,这是本地路径
        File dest = new File(uploadPath + "/" + fileName);
        try {
            // 存储文件
            headerImage.transferTo(dest);//把上传的文件写入到目标文件
        } catch (IOException e) {
            logger.error("上传文件失败: " + e.getMessage());
            throw new RuntimeException("上传文件失败,服务器发生异常!", e);
        }
        // 更新当前用户的头像的路径(web访问路径),不是存下来的自己本地路径
        // http://localhost:8080/community/user/header/xxx.png
        User user = hostHolder.getUser();//获取当前用户
        String headerUrl = domain + contextPath + "/user/header/" + fileName;
        userService.updateHeader(user.getId(), headerUrl);

        return "redirect:/index";
    }

    //访问的路径是,更新后的web访问路径
    @RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)
    //向浏览器响应的不是字符串也不是网页,是二进制的图片,通过流手动输出到浏览器
    public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
        // 服务器存放路径
        fileName = uploadPath + "/" + fileName;
        // 文件后缀,向网页输出的文件类型
        String suffix = fileName.substring(fileName.lastIndexOf("."));
        // 响应图片
        response.setContentType("image/" + suffix);
        try (
                FileInputStream fis = new FileInputStream(fileName); //手动创建的输入流需要自己关闭,放在这里会自动关闭
                OutputStream os = response.getOutputStream();
        ) {
            byte[] buffer = new byte[1024];//每次读取缓冲区大小的内容
            int b = 0;
            while ((b = fis.read(buffer)) != -1) {//==-1说明没有内容了
                os.write(buffer, 0, b);
            }
        } catch (IOException e) {
            logger.error("读取头像失败: " + e.getMessage());
        }
    }

检查登录状态

  • 使用拦截器
    在方法前标注自定义注解
    拦截所有请求,只处理带有该注解的方法
  • 自定义注解
    常用的元注解:@Target、@Retention、@Document、@Inherited
    如何读取注解:Method.getDeclaredAnnotations()、Method.getAnnotation(Class annotationClass)
//接口
@Target(ElementType.METHOD)//注解写在方法上
@Retention(RetentionPolicy.RUNTIME)//程序运行时才有效
public @interface LoginRequired {
}
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
    @Autowired
    private HostHolder hostHolder;
    //
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 拦截到的目标是方法的话,则其应该是HandlerMethod类型,Spring MVC隐含的规则
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();//获取拦截到的method对象(方法对象)
            //从方法对象上去取注解
            LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
            //如果取到了注解,并且没有user的话,说明没登录不能访问这个方法
            if (loginRequired != null && hostHolder.getUser() == null) {
                // 利用response重定向,request.getContextPath()从请求中直接取到域名路径
                response.sendRedirect(request.getContextPath() + "/login");
                return false;
            }
        }
        return true;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值