文章列表:
1、初识Spring Boot,开发社区首页
2、开发社区登录模块
3、开发社区核心功能
开发社区登录模块
1.从产品的角度思考一个功能该怎么去实现 ,细节怎么完善
2.实践上一章学到的SSM知识,为后面做准备
1 发送邮件
登陆模块要实现的第一个功能是注册,注册要求网页像用户发验证码邮件
1.1 邮箱设置
启动客户端STMP服务:163邮箱-设置-更多设置-POP3/SMTP/IMAP-开启
1.2 Spring Email
1.2.1 导入jar包
我们的老朋友https://mvnrepository.com/
为了防止版本不匹配我用的和老师一个版本(卑微)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
1.2.2 邮箱参数配置
注意:邮箱密码要写邮箱开通POP3/SMTP的授权码
1.2.3 使用JavaMailsender发送邮件
1.3 模板引擎
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>邮件示例</title>
</head>
<body>
<p>欢迎你,<span style="color:#ff0000" th:text="${username}"></span>!</p>
</body>
</html>
@Autowired
private TemplateEngine templateEngine;
@Test
public void testHtmlMail(){
Context context = new Context();
context.setVariable("username","uam");
String content = templateEngine.process("mail/demo", context);
System.out.println(content);
mailClient.sendMail("@qq.com","html",content);
}
输出
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>邮件示例</title>
</head>
<body>
<p>欢迎你,<span style="color:red">uam</span>!</p>
</body>
</html>
模板引擎本身也可以被注入,深刻体会到了模板引擎的动态显示!
2 开发注册功能
复杂的功能要拆解成相对简单的功能,这样好开发
web项目可以按照请求去拆解
1.点注册,打开注册页面,就是一次请求
2.填信息,点立即注册,又把数据传给服务器
3.服务器保存账号之后会发邮件让点链接激活,点链接完成激活是第三个请求
每一次请求还是按照之前的思路:先开发数据访问层,再开发业务层,最后视图层
有些功能可能没有完整的三层。
记得加注解!
2.1 访问注册页面
2.1.1 LoginController
1.写Controller注释
2.写RequestMapping访问路径
3.在Controller里写方法返回注册页面
4.用thymeleaf改index与register,然后我们的首页和注册按钮就生效啦!
以后运行都是调试运行的,方便如果出错就打断点
鼠标放在按钮上看页面左下角的网址对不对(神奇)
2.2 提交注册数据
1.导包,好朋友https://mvnrepository.com/
2.配置,给网站整一个域名(目前是http://localhost:8080)
3.写一个工具类,提供两个方法(生成随机字符串+加密密码),后面注册时好用
2.2.1 UserService
注册:
1.返回错误信息:账号为空、密码为空、邮箱为空、账号已存在等(这些信息用map封装)
2.注册用户
3.发激活邮件:用thymeleaf生成动态网页,用上小节实现的mailClient发送动态网页邮件
2.2.2 LoginController
1.如果map为空,表示注册账号没有错误,转到操作成功界面
2.否则,在注册界面输出错误信息
3.修改完善操作成功界面和注册界面
2.3 激活注册账号
2.3.1 UserService
1.将状态常量放在接口里,要用到状态常量的类去继承该接口
2.激活方法:三种状态:若已激活;若相等;若不等
2.3.2 LoginController
要想在首页上访问login页面:
1.LoginController视图层写好映射和访问方法
2.login将静态页面改为动态页面
3.index里写好链接
3 会话管理
//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");
//设置cookie生效时间,默认浏览器关掉cookie就无了
cookie.setMaxAge(60 * 10);
//发送cookie
response.addCookie(cookie);
return "set success";
}
@RequestMapping(path = "/cookie/get",method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code){
System.out.println(code);
return "get cookie";
}
优:
cookie可以弥补HTTP无状态的特点
缺:
cookie存在客户端,没有安全性
cookie需要发送给服务端,占用流量
可以用session来解决cookie的问题。
缺点:session存在服务端,占用内存
//session示例
@RequestMapping(path = "session/set",method = RequestMethod.GET)
@ResponseBody
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";
}
4 生成验证码
官网:https://code.google.com/archive/p/kaptcha/(需翻墙)
4.1 导入jar包
4.2 编写配置类
在Config目录下写Kaptcha配置类
1.注解
2.设置验证码的长、宽、大小等
4.3 生成随机字符
5 开发登陆、退出功能
5.1 访问登陆页面
使用thymeleaf动态链接到登陆页面,之前已实现
5.2 登陆
查看表的定义语句
id:主键
user_id
ticket:凭证,随机字符串,不重复,唯一的标识
status:状态,表示凭证有效还是无效(过期)
expired:凭证的有效期
1.先写数据访问层
1.1 实体类 名称和表名相对应
表里有什么字段,实体类里就增加几个属性与之对应
get/set方法
toString方法
1.2 dao里写Mapper类
声明增删改查的方法
用注解写sql语句,如 增:@Insert({" "," "})
,双引号之间会自动拼在一起,每个双引号最后记得打一个空格,方便拼接
1.3 测试
2.再写业务层 UserService
实现登录功能:可能登陆失败,需要返回具体失败情况,可以返回Map来封装返回信息
2.1 处理空值
2.2 验证账户是否存在
2.3 验证账户是否激活
2.4 验证密码是否正确
2.5 登陆成功,生成凭证
3.最后写视图层 LoginController
3.1 判断验证码是否正确(验证码存入session里)
3.2 检查账号密码 - 调用业务层来实现
知识点:
- 将对象转为字符串:
obj.toString()
- 用@Value将application.properties的值注入进来的方法:
@Value("${server.servlet.context-path}")
private String contextPath;
- 错误的时候要将错误信息带给网页:往model里存
model.addAttribute("usernameMsg", map.get("usernameMsg"));
//如果不是usernameMsg的问题,得到的是none,没有影响
model.addAttribute("passwordMsg", map.get("passwordMsg"));
4.处理页面 login.html
处理如图所示的表单
4.1 表单里每个框的name要与传入controller的参数名一致
4.2 错误信息的显示
(1)旧值显示在框里th:value="${param.password}"
(2)错误信息显示在框下
<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>
5.3 退出
把凭证改为失效
1.业务层
2.视图层
3.配置“退出登录”按钮的链接 index.html
<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
6 显示登陆信息
登陆后显示:登陆头像、用户名称、消息数量
不登陆显示:注册、登陆按钮
不同的显示可以调用不一样的controller,但是一个网站都用这个方式处理的话可能有几百个上千个请求
拦截器:拦截浏览器访问过来的多个请求,在请求的开始或是结束部分插入代码,从而可以批量解决多个请求共有的内容。
6.1 拦截器示例
6.1.1 定义拦截器
1.加注解@Component
2.实现接口HandlerInterceptor
package com.nowcoder.community.controller.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class AlphaInterceptor implements HandlerInterceptor {
//实例化日志组件
private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
//在Controller之前执行
@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, @Nullable ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: "+handler.toString());
}
//在模板引擎之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
logger.debug("afterCompletion: "+handler.toString());
}
}
6.1.2 配置拦截器
1.写配置类
2.实现WebMvcConfigurer接口
package com.nowcoder.community.config;
import com.nowcoder.community.controller.interceptor.AlphaInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "login");
}
}
6.1.3 测试
注意以后启动服务器要用debug哈,养成好习惯
判断是否按注释顺序执行:在LoginController里进入login打断点,在AlphaInterceptor三个方法里打断点,刷新登陆页面
6.2 拦截器应用
在请求开始时查询登录用户
在本次请求中持有用户数据
//在请求开始之初,通过凭证找到用户,并将用户存到hostHolder里
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从cookie中获取凭证
String ticket = CookieUtil.getValue(request, "ticket");
if(ticket!=null){
//查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
//检查凭证是否有效:不为空、有效、超时时间晚于当前时间
if(loginTicket!=null && loginTicket.getStatus()==0 && loginTicket.getExpired().after(new Date())){
//根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
//在本次请求中持有用户
hostHolder.setUser(user);
}
}
return true;
}
在模板视图上显示用户数据
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user=hostHolder.getUser();
if(user!=null || modelAndView!=null){
modelAndView.addObject("loginUser", user);
}
}
在请求结束时清理用户数据
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
写配置
改模板引擎 index里的头部
<li class="nav-item ml-3 btn-group-vertical" th:if="${logininUser==null}">
<a class="nav-link" th:href="@{/login}">登录</a>
</li>
7 账号设置
账号设置主要有两个功能:一个是上传头像,一个是修改密码。修改密码作为课后作业
7.1 上传头像
7.1.1 访问账号设置页面
1.视图层UserController设置访问路径和页面
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage(){
return "/site/setting";
}
}
2.配置/site/setting页面成动态页面
最开始加xmlns:th="http://www.thymeleaf.org"
改相对路径成thymeleaf语句th:href="@{/css/global.css}"
复用头部th:replace="index::header"
3.在index.html添加链接
<a class="dropdown-item text-center" th:href="@{/user/setting}">账号设置</a>
7.1.2 上传头像
还是按照三层来写
1.数据层:没啥事
2.业务层:提供改变用户头像的功能
public int updateHeader(int userId,String headerUrl){
return userMapper.updateHeader(userId, headerUrl);
}
3.视图层:上传图片
@RequestMapping(path = "/upload",method = RequestMethod.POST)//上传时表单的提交方式为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://local: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";
}
7.1.3 获取头像
1.视图层
//获取头像
@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(
OutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(fileName);
) {
byte[] buffer = new byte[1024];
int b=0;
while(( b=fis.read(buffer) ) != -1){
os.write(buffer,0,b);
}
} catch (IOException e) {
logger.error("读取头像失败"+e.getMessage());
}
}
2.页面配置
7.2 修改密码(课后作业)
8 检查登陆状态
想拦截哪个方法就在该方法上加个注解,加了注解就拦截,不加注解就不拦截
问题:1.注解怎么定义?2.怎么 识别当前方法有没有加这个注解?
想自己定义注解,就需要用元注解定义自定义注解常用的元注解有:
@Target:自定义注解可以作用在哪个类型上,类型如:类、方法、属性
@Retention:有效时间,是编译时有效还是运行时有效
@Document:自定义注解在生成文档的时候要不要把自定义注解带上去
@Inherited:子类继承父类,父类上有自定义注解,子类要不要继承
前两个一定要用
1.写自定义注解@LoginRequired
//有该注解的方法都是登陆后才能访问
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)//声明有效时期:程序运行时就有效
public @interface LoginRequired {
}
2.在Controller里需要登陆才能访问的方法前加上注解@LoginRequired
3.写拦截器:未登录且访问被写注解的方法时,需要拦截
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){//只拦截方法不拦截静态资源
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if(loginRequired!=null && hostHolder.getUser()==null){
response.sendRedirect(request.getContextPath()+"/login");
return false;
}
}
return true;
}
}
4.写拦截器配置:不处理静态资源
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}