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 属性相同。
- <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;
}
}
2万+

被折叠的 条评论
为什么被折叠?



