- 前言
- 1、显示登录信息
- 1.1 拦截器示例
- 2.2 拦截器应用(使用线程隔离批量处理用户请求)
- 2.2.1 新建 LoginTicketInterceptor 拦截器
- 2.2.2 UserService 层
- 2.2.3 WebMvcConfig 中配置拦截器
- 2.2.4 修改 index.html 页面
- 2.2.5 测试页面
- 2、账号设置
- 2.1 上传文件
- 2.2 新建 UserController 类
- 2.3 修改 setting.html 页面
- 2.4 修改 index.html 页面
- 2.5 测试页面
- 2.6 配置资源上传的路径(上传头像)
- 2.7 UserService 层
- 2.8 UserController 层(客户端到服务器)
- 2.9 UserController 层:获取头像(服务器响应给客户端)
- 2.10 处理 settings.html 页面
- 2.11 测试页面
- 2.10 错误排查
- 3、账号设置(修改密码)
- 3.1 UserService 层
- 3.2 UserController 层
- 3.3 修改 setting.html 页面:
- 3.4 测试页面
- 3.5 错误排查
- 4、检查登录状态
- 4.1 在 annotation 包下新建 LoginRequired 类
- 4.2 新建 LoginRequiredInterceptor 类
- 4.3 配置拦截器:
- 4.4 测试页面
- 总结
前言
- 之前 SpringMVC 中已经有过关于 拦截器的讲解和配置,(尚硅谷)2021 版 SpringMVC 教程笔记,这里简单带过。
1、显示登录信息 1.1 拦截器示例
- 定义拦截器,实现 HandlerInterceptor;
@Component
public class HelloInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(HelloInterceptor.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, 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());
}
}
- 配置拦截器,为它指定拦截、排除的路径;
- 配置 MVC 的时候要实现 WebMvcConfigurer 这个类才可以;
- 将拦截器注入;
- 重写 addInterceptors 方法
- 将拦截器添加进来: registry.addInterceptor
- 排除所有静态资源:registry.excludePathPatterns
- 设置拦截哪些路径:registry.addPathPatterns
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private HelloInterceptor helloInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(helloInterceptor)
.excludePathPatterns("*.css", "*.js", "*.png", "*.jpg", "*.jpeg")
.addPathPatterns("/register", "/login");
}
}
测试一下是否拦截成功,index 页面不拦截,登录、注册页面被拦截:
2.2 拦截器应用(使用线程隔离批量处理用户请求)- 在请求开始时查询登录用户
- 在本次请求中持有用户数据
- 在模板视图上显示用户数据
- 在请求结束时清理用户数据
- 将从 cookie 中获取特定信息的方法包装成一个工具类;
public class CookieUtil {
public static String getValue(HttpServletRequest request, String name) {
if (request == null || name == null) {
throw new IllegalArgumentException("参数为空!");
}
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
- request.getCookies();返回的是所有 cookie 信息,我们要从中取我们想要的信息。
- 实现 prehandle 方法:
- 通过 cookie 得到 ticket 信息,用户登录之后会重定向到首页,这时我们可以从第二次请求中获取用户信息;
- 在 prehandle 中只能通过请求 request 来获取 cookie;
- 如果 ticket 不为空,调用 UserService 层去获取该凭证对应的所有登录信息(UserService 层要加一个方法);
- 检查凭证是否有效:LoginTicket 不为空,状态码为0,且有效时长没有超时的情况是有效的,调用 UserService 层查询 user 数据;
- 本次请求中持有用户:
//用户登录成功之后重定向到首页,这时我们可以获取用户信息,让线程持有当前用户
@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;
}
解释:
- 线程隔离存放用户信息,实现 HostHolde r类,使用 ThreadLocal 模拟 session 存放用户信息;
- ThreadLocal 以线程为 key 实现存取值;
- 将 HostHolder 注入,调用其中的 setUser 方法,将用户信息存到某一线程对应的map容器中;
@Component
public class HostHolder {
private ThreadLocal users = new ThreadLocal<>();
public void setUser(User user) {
users.set(user);
}
public User getUser() {
return users.get();
}
public void clear() {
users.remove();
}
}
- 实现 postHandler 方法:
- 调用 HostHolder 取 user 值,将当前用户存放在模板引擎中,用来显示用户信息;
//将持有的用户信息保存到模板引擎中
@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);
}
}
- 实现afterCompletion方法:
- 清掉当前用户占用的线程
//模板渲染结束后,即当前请求结束后清理县城内存放的用户
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
代码总结:
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder 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();
}
}
2.2.2 UserService 层
- 实现通过 Ticket 查询用户登录凭证信息;
@Override
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
2.2.3 WebMvcConfig 中配置拦截器
- 添加当前拦截器,同时排除所有静态页面(减少拦截器拦截的请求数量,静态页面也会发请求):
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("*.css", "*.js", "*.png", "*.jpg", "*.jpeg");
}
2.2.4 修改 index.html 页面
- 消息:登录状态下显示,th:if="${loginUser!=null}"
- 登录、注册:登录了不显示,th:if="${loginUser==null}"
- 用户头像、用户名动态显示:th:src="${loginUser.headerUrl}",th:utext="${loginUser.username}"
- 修改退出登录超链接:th:href="@{/logout}"
- 登陆之前:
- 登录之后:
- 点击退出登录
在 Controller 中处理上传文件的步骤:
- 判断控制和文件类型是否正确;
- 随机生成文件名 + 后缀;
- 确定文件上传目录,并判断该目录是否存在,不存在则创建目录;
- 最终确定文件上传路径为 上传目录 + / + 文件名;
- 利用 transferTo 方法上传文件。
- 实现 getSettingPage() 方法;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/setting",method = RequestMethod.GET)
public String getSettingPage(){
return "site/setting";
}
}
2.3 修改 setting.html 页面
- 引入 thymeleaf 文件
- 静态资源路径修改
- 头部代码复用:th:replace="index::header"
- 将账号设置的超链接进行修改:
账号设置2.5 测试页面 2.6 配置资源上传的路径(上传头像)
community.path.upload=d:/work/data/upload
- 我们只在 Controller 中处理上传文件的逻辑,而不调用 service 层,因为 MultipartFile 是表现层 SpringMVC 的一个组件,不要在 service 层调用;
- 实现更新用户头像方法,即根据用户的 userId 更新用户头像的 url 地址;
@Override
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId,headerUrl);
}
2.8 UserController 层(客户端到服务器)
-
判断文件是否为空,或者文件格式是否正确;
-
生成随机文件名: 使用生成随机字符串工具类生成 随机字符+后缀 作为文件名
-
确定文件目录是否为空,为空则创建目录;
-
确定文件存放的位置:新建一个 file,其中是上传路径+/+文件名字
-
存储文件:使用 transferTo(File file) 方法传到服务器中;
-
更新当前用户的头像的路径(web访问路径):http://localhost:8080/community/user/header/xxx.png, 调用 UserService 层更新数据库中用户头像的 url 地址;
-
重定向到首页。
@RequestMapping(value = "/setting", method = RequestMethod.POST)
public String uploadHeader(
@RequestParam(value = "headerImage") MultipartFile photo, Model model) {
//空值处理
if (photo == null || photo.isEmpty()) {
model.addAttribute("error", "您还没有选择图片!");
return "/site/setting";
}
//文件检测是否合格
String fileName = photo.getName();
String suffix = fileName.substring(fileName.lastIndexOf('.') + 1);
if (StringUtils.isBlank(suffix)) {
model.addAttribute("error", "文件的格式不正确!");
return "/site/setting";
}
//生成随机文件名
fileName = CommunityUtil.generateUUID() + suffix;
//确认文件上传路径目录是否为空
File dir = new File(uploadPath);
if (!dir.exists()){
dir.mkdir();
}
//确定图片在服务器中的存放路径
File dest = new File(uploadPath + File.separator + fileName);
try {
//上传图片
photo.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";
}
2.9 UserController 层:获取头像(服务器响应给客户端)
- 实现获取头像方法,从图片在服务器的路径中读取图片(字节输入流),使用 response 获取字节输出流向客户端写文件;
- 设置相应类型为:setContentType=("image/"+suffix);;
- 手动关闭我们自己创建的输入流(java7 中在 try 后面的小括号中写上需要关闭的资源,前提是要有 close 方法);通过 response 获取到的输出流,SpringMVC 会自动帮我们进行关闭操作。
//给客户端显示新头像
@RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)
public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
// 服务器存放路径
fileName = uploadPath + File.separator + fileName;
// 文件后缀(要作为给web的响应内容,所以不可以带.)
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
// 响应图片
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());
}
}
2.10 处理 settings.html 页面
- 上传头像的表单修改
- 手动处理如果没有文件为空的错误信息显示
2.11 测试页面上传头像
- 不上传头像(前端逻辑也过不去)
- 上传不带文件后缀的文件:
- 正确上传头像:
- 必须在请求的 form 表单中设置上传格式为:enctype="multipart/form-data",否则的话报错;
Current request is not a multipart request
-
获取上传的图片的文件名字必须使用 photo.getOriginalFilename(); 方法,这样才是获取你上传的文件的名字,否则的话是不对的;
-
记得在 UserController 上面加上 @RequestMapping("/user"),代表在上下文路径下多一层目录,同时表单请求和 index.html 文件中的账号设置请求都要在请求中添加 /user 这一层路径:
- 输入为空值的时候(前端实现了不允许为空值的情况):
- 输入新旧密码不一致的时候:
- 输入旧密码不正确的时候(虽然跳回到了设置页面,但是没有提示错误信息,检查页面接收到的响应代码没有问题,不知道为什么不显示):
下面这个是上传头像文件格式有误时候的前端代码,页面提示错误信息显示是正常的(费解!!!!):
- 输入正确情况:
并且登录凭证状态修改为1,即失效了。
3.5 错误排查- 旧密码输入不正确的时候跳回到设置页面,没有显示错误信息(问题未解决!!!!!!!!!!!)
- 在用户没有登录的情况下,拦截一些请求,放置知道访问路径的人可以通过手动修改访问路径去访问一下页面;
-
常用的元注解(主要定义前两个就可以了):
@Target:定义在类、方法、属性等上面
@Retention:编译时有效or运行时有效
@Document:生成文档的时候要不要带这个注解
@Inherited:子类要不要继承父类的这个注解 -
如何读取注解:
Method.getDeclaredAnnotations ():获取方法上的所有注解
Method.getAnnotation (ClassannotationClass):尝试获取某个类型的注解
- 其中定义一个登录之后才能访问的页面的注解
- 给所有登录后才能访问的方法,加上这个自定义注解(所有关于用户设置的,都要在方法上方加上这个注解,但是获取用户头像不用这个注解,因为在首页的时候要显示所有帖子的用户头像)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
4.2 新建 LoginRequiredInterceptor 类
- 注入 Hostholder,获取当前是否有用户信息
- 如果拦截到的是一个方法的话,将他转型
- 从方法上面取对应的自定义注解类型的注解
- 如果获取到了该注解,但是获取到的用户为空,我们就进行拦截,即返回false,同时强制重定向到登录页面
@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.3 配置拦截器:
- 将当前拦截器注入
- 排除对静态资源的拦截
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("*.css", "*.js", "*.png", "*.jpg", "*.jpeg");
}
4.4 测试页面
- 当我试图访问修改用户信息界面时,自动跳转到了登陆页面:
总结
- 修改密码的错误信息回显没有效果。可能是前端哪里出问题了。



