目录
3.1 拦截器拦截带有注解 @LoginRequired 的方法,判断是否登录
1.账号设置
- 上传文件:请求(必须是 POST 请求)、表单(enctype = "multipart/form-data")、Spring MVC(通过 MultipartFile 处理上传文件)
- 开发步骤:访问账号设置、上传头像、获取头像
1.1 访问账号设置页面
需要开发 controller 声明模板存放路径,在 controller 包下新建 Usertroller 类(用户相关业务):
- 添加方法使得浏览器通过方法访问到设置的页面
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(path = "/setting", method = RequestMethod.GET)
//添加方法使得浏览器通过方法访问到设置的页面
public String getSettingPage() {
return "/site/setting";
}
}
1.2 修改模板
配置 setting 模板:
修改 index.xml:
1.3 上传文件
需要配置上传资源的位置,在 resources 资源文件下的 application.properties 中添加配置:
#上传文件配置位置
community.path.upload=e:/forum-system/data/upload
上传文件请求按照服务器三重架构,从下往上开发:数据访问层(上传文件存储到某一个位置,不是数据库中,因此数据访问层没有修改的)、业务层(上传文件然后需要更新用户的 headurl)、上传文件在 controller 中实现(MultipartFile 处理文件,属于表现层的对象)
在 service 包下的 Userservice 类中添加:
- 添加方法:更新修改头像路径,返回更新行数(int)
//更新修改头像路径
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId, headerUrl);
}
在 controller 类下的 UserController 类中添加:
- 追加请求:处理本次上传文件请求
- 注入上传路径、域名、项目的访问路径
- 注入 UserService
- 注入 HostHeader:更新当前用户头像
- 加入方法:处理上传逻辑,是一个 POST 请求
- 使用 MultipartFile 接收一个文件
- 如果没有上传文件,给予提示并且返回当前页面
- 给文件生成随机名字(不可以使用原文件名,避免重复),后缀不可以变
- 首先得到原始文件名,然后截取后缀(截取最后一个“ . ”的索引)
- 再去判断截取是否为空:为空给予提示并且返回当前页面
- 然后生成随机文件
- 确定文件存放路径
- 将当前文件中的内容写到目标文件中
- 更新当前用户的头像路径(web 访问路径):http://localhost:8080/community/user/header/xxx.png
- 1.得到当前用户 2.得到 headerUrl:域名 + 项目名 + 当前 controller 访问路径 + 文件名(随机生成的文件名)
- 调用 Userservice,传入当前用户和更新后的图像路径,重定向到首页
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.example.demo.util.CommunityUtil;
import com.example.demo.util.HostHolder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Controller
@RequestMapping("/user")
public class UserController {
//打印日志
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
//注入 UserService
@Autowired
private UserService userService;
//注入 HostHeader:更新当前用户头像
@Autowired
private HostHolder hostHolder;
//注入上传路径
@Value("${community.path.upload}")
public String uploadPath;
//注入域名
@Value("${community.path.domain}")
public String domain;
//注入项目访问路径
@Value("${server.servlet.context-path}")
private String contextPath;
//加入方法:处理上传逻辑,是一个 POST 请求
@RequestMapping(path = "/upload", method = RequestMethod.POST)
//使用 MultipartFile 接收一个文件
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("上传文件失败,服务器发生异常");
}
//更新当前用户的头像路径(web 访问路径):http://localhost:8080/community/user/header/xxx.png
//1.得到当前用户 2.得到 headerUrl:域名 + 项目名 + 当前 controller 访问路径 + 文件名(随机生成的文件名)
User user = hostHolder.getUser();
String headerUrl = domain + contextPath + "/user/header" + fileName;
//调用 Userservice,传入当前用户和更新后的图像路径,重定向到首页
userService.updateHeader(user.getId(), headerUrl);
return "redirect:/index";
}
}
1.4 获取头像
在 controller 类下的 UserController 类中添加:
- 添加获取头像访问路径:header/{fileName}(http://localhost:8080/community/user/header/xxx.png),是一个 GET 请求,只取头像路径
- 添加获取头像方法:返回 void,是一个图片,二进制数据,需要通过流手动向浏览器输出
- 添加注解 @PathVariable:解析 fileName 参数;向浏览器响应特殊东西,需要用response 去拼
- 找到服务器存放的路径,然后带上本地路径的文件
- 向浏览器输出图片:首先是格式(解析后缀),接下来是响应图片(二进制,使用字节流),获取字节流得到输出流,输出文件(创建文件得到输入流去边读边输出)
- 建立缓冲区,一次输出 1024 字节
- spring mvc 会自动的关闭输出流,输入流是自己创建的需要手动关闭
//添加获取头像访问路径:header/{fileName}(http://localhost:8080/community/user/header/xxx.png),是一个 GET 请求,只取头像路径
@RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)
//添加注解 @PathVariable:解析 fileName 参数;向浏览器响应特殊东西,需要用response 去拼
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) {
os.write(buffer, 0, b);
}
} catch (IOException e) {
logger.error("读取头像失败: " + e.getMessage());
}
}
1.5 处理页面表单
2.修改密码
当用户在账号设置页面修改密码时,输入完原始密码以及新密码、确认密码后,当点击【立即保存】后,若用户原始密码输入正确,新密码与确认密码一致时,会跳转至登录页面重新登录,若输入有误,则显示对应的错误信息
UserController:
//修改密码
@RequestMapping(value = "/updatePassword",method = RequestMethod.POST)
//@CookieValue("ticket") String ticket:从浏览器器中得到cookie
public String updatePassword(String oldPassword, String newPassword, String confirmNewPassword,
Model model,@CookieValue("ticket") String ticket){
//对传入参数进行判断,不能为空
if(StringUtils.isBlank(oldPassword)){
model.addAttribute("oldPasswordMsg","请输入原始密码!");
return "site/setting";
}
if(StringUtils.isBlank(newPassword)){
model.addAttribute("newPasswordMsg","请输入新密码!");
return "site/setting";
}
if(StringUtils.isBlank(confirmNewPassword)){
model.addAttribute("confirmNewPasswordMsg","请输入确认密码!");
return "site/setting";
}
//首先通过所持有的的用户对象获取当前用户
User user = hostHolder.getUser();
//判断用户输入的原密码是否与存储的原密码一致
//首先对用户输入的原密码进行加密处理
oldPassword = CommunityUtil.md5(oldPassword+user.getSalt());
if(!oldPassword.equals(user.getPassword())){
model.addAttribute("oldPasswordMsg","该密码与原密码不符!");
return "site/setting";
}
//判断新输入密码与原密码是否一致
//对新密码进行加密
newPassword=CommunityUtil.md5(newPassword+user.getSalt());
if(newPassword.equals(user.getPassword())){//判断
model.addAttribute("newPasswordMsg","新密码与原密码一致!");
return "site/setting";
}
//对确认密码进行加密
confirmNewPassword=CommunityUtil.md5(confirmNewPassword+user.getSalt());
if(!newPassword.equals(confirmNewPassword)){//判断
model.addAttribute("confirmNewPasswordMsg","两次密码不一致!");
return "site/setting";
}
userService.updatePassword(user.getId(),newPassword);
//修改密码后,用户需要重新登陆,所以在本次持有中释放用户
userService.logout(ticket);
return "redirect:/login";
}
UserService:
通过当前的用户id,和传入的密码,修改用户的密码
//修改密码
public int updatePassword(int userId,String password){
return userMapper.updatePassword(userId,password);
}
setting.html
<!-- 修改密码 -->
<h6 class="text-left text-info border-bottom pb-2 mt-5">修改密码</h6>
<form class="mt-5" method="post" th:action="@{/user/updatePassword}">
<div class="form-group row mt-4">
<label for="old-password" class="col-sm-2 col-form-label text-right">原密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${oldPasswordMsg!=null ? 'is-invalid':'' }|"
name="oldPassword"
id="old-password" placeholder="请输入原始密码!" required>
<div class="invalid-feedback" th:text="${oldPasswordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="new-password" class="col-sm-2 col-form-label text-right">新密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${newPasswordMsg!=null ? 'is-invalid':'' }|"
name="newPassword"
id="new-password" placeholder="请输入新的密码!" required>
<div class="invalid-feedback" th:text="${newPasswordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="confirm-password" class="col-sm-2 col-form-label text-right">确认密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${confirmNewPasswordMsg!=null ? 'is-invalid':'' }|"
name="confirmNewPassword"
id="confirm-password" placeholder="再次输入新密码!" required>
<div class="invalid-feedback" th:text="${confirmNewPasswordMsg}">
两次输入的密码不一致!
</div>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10 text-center">
<button type="submit" class="btn btn-info text-white form-control">立即保存</button>
</div>
</div>
</form>
3.检查登录状态
没有登陆之前:访问 localhost:8080/community/index :看不到个人设置相关内容
但是访问 localhost:8080/community/user/setting :
上述就是一个漏洞,这是不安全的,需要让用户访问不到
- 使用拦截器:在方法前标注自定义注解、拦截所有请求,只处理带有该注解的方法
- 自定义注解:常见元注解(@Target、@Retention、@Document、@Inherited)、如何让读取注解(Method.getDeclearAnnotations()、Method.getAnnotation(Class<T> annotationClass))
自定义注解先创建一个 annotation 包,创建一个 LoginRequired 类(注解类):
- 使用 @Target,描述注解使用在方法上
- 使用 @Retention 声明注解有效时机
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
哪些请求是登录以后才能访问:setting、upload
在 Usertroller 类中添加注解:
@LoginRequired
@RequestMapping(path = "/setting", method = RequestMethod.GET)
@LoginRequired
@RequestMapping(path = "/upload", method = RequestMethod.POST)
3.1 拦截器拦截带有注解 @LoginRequired 的方法,判断是否登录
在 controller 包下的 interceptor 包中新建 LoginRequiredInterceptor 类(拦截器):
- 添加注解 @Component,实现接口 HandlerInterceptor
- 在请求最初,判断是否登录:重写 preHandle 方法
- 判断是否登录:尝试获取当前用户,注入 HostHolder
- 判断拦截目标是否为方法,是方法接着处理
- 获取拦截到的对象
- 从获取到的对象取注解
- 判断注解是否为空
- 注解不为空,但是用户没有登录,拒绝访问,重定向到 login(登录页面) 路径(应用路径 + login 路径)
package com.example.demo.controller.interceptor;
import com.example.demo.annotation.LoginRequired;
import com.example.demo.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
//添加注解 @Component,实现接口 HandlerInterceptor
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
//在请求最初,判断是否登录:重写 preHandle 方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
//判断拦截目标是否为方法,是方法接着处理
if (handler instanceof HandlerMethod) {
判断是否登录:尝试获取当前用户,注入 HostHolder
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取拦截到的对象
Method method =handlerMethod.getMethod();
//从获取到的对象取注解
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
//判断注解是否为空,注解不为空,但是用户没有登录,拒绝访问,重定向到 login 路径(应用路径 + login 路径)
if (loginRequired != null && hostHolder.getUser() == null) {
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
}
return true;
}
}
3.2 配置拦截器
在 config 包下的 WebMvcConfig 拦截器配置类中拦截静态资源:
- 注入拦截器 LoginRequireInterceptor
- 拦截所有静态资源
//登录状态拦截器
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
处理所有动态资源,人为筛选带有注解的一部分
访问 localhost:8080/community/user/setting 直接跳转到登陆页面: