栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

(仿牛客论坛项目)05 - 显示登录信息 - 账号设置 - 检查登录状态

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

(仿牛客论坛项目)05 - 显示登录信息 - 账号设置 - 检查登录状态

文章目录
  • 前言
  • 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 测试页面
  • 总结


前言
  1. 之前 SpringMVC 中已经有过关于 拦截器的讲解和配置,(尚硅谷)2021 版 SpringMVC 教程笔记,这里简单带过。

1、显示登录信息 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());
    }
}
  1. 配置拦截器,为它指定拦截、排除的路径;
  2. 配置 MVC 的时候要实现 WebMvcConfigurer 这个类才可以;
  3. 将拦截器注入;
  4. 重写 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 拦截器应用(使用线程隔离批量处理用户请求)
  • 在请求开始时查询登录用户
  • 在本次请求中持有用户数据
  • 在模板视图上显示用户数据
  • 在请求结束时清理用户数据

2.2.1 新建 LoginTicketInterceptor 拦截器
  1. 将从 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 信息,我们要从中取我们想要的信息。
  1. 实现 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();
    }
}
  1. 实现 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);
    }
}
  1. 实现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 层
  1. 实现通过 Ticket 查询用户登录凭证信息;
@Override
public LoginTicket findLoginTicket(String ticket) {
    return loginTicketMapper.selectByTicket(ticket);
}
2.2.3 WebMvcConfig 中配置拦截器
  1. 添加当前拦截器,同时排除所有静态页面(减少拦截器拦截的请求数量,静态页面也会发请求):
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginTicketInterceptor)
            .excludePathPatterns("*.css", "*.js", "*.png", "*.jpg", "*.jpeg");
}
2.2.4 修改 index.html 页面
  1. 消息:登录状态下显示,th:if="${loginUser!=null}"
  2. 登录、注册:登录了不显示,th:if="${loginUser==null}"
  3. 用户头像、用户名动态显示:th:src="${loginUser.headerUrl}",th:utext="${loginUser.username}"
  4. 修改退出登录超链接:th:href="@{/logout}"

2.2.5 测试页面
  1. 登陆之前:

  1. 登录之后:

  1. 点击退出登录

2、账号设置 2.1 上传文件

在 Controller 中处理上传文件的步骤:

  1. 判断控制和文件类型是否正确;
  2. 随机生成文件名 + 后缀;
  3. 确定文件上传目录,并判断该目录是否存在,不存在则创建目录;
  4. 最终确定文件上传路径为 上传目录 + / + 文件名;
  5. 利用 transferTo 方法上传文件。

2.2 新建 UserController 类
  1. 实现 getSettingPage() 方法;
@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping(value = "/setting",method = RequestMethod.GET)
    public String getSettingPage(){
        return "site/setting";
    }
}
2.3 修改 setting.html 页面
  1. 引入 thymeleaf 文件
  2. 静态资源路径修改
  3. 头部代码复用:th:replace="index::header"
2.4 修改 index.html 页面
  1. 将账号设置的超链接进行修改:
账号设置
2.5 测试页面

2.6 配置资源上传的路径(上传头像)
community.path.upload=d:/work/data/upload
  • 我们只在 Controller 中处理上传文件的逻辑,而不调用 service 层,因为 MultipartFile 是表现层 SpringMVC 的一个组件,不要在 service 层调用;
2.7 UserService 层
  1. 实现更新用户头像方法,即根据用户的 userId 更新用户头像的 url 地址;
@Override
public int updateHeader(int userId, String headerUrl) {
    return userMapper.updateHeader(userId,headerUrl);
}
2.8 UserController 层(客户端到服务器)
  1. 判断文件是否为空,或者文件格式是否正确;

  2. 生成随机文件名: 使用生成随机字符串工具类生成 随机字符+后缀 作为文件名

  3. 确定文件目录是否为空,为空则创建目录;

  4. 确定文件存放的位置:新建一个 file,其中是上传路径+/+文件名字

  5. 存储文件:使用 transferTo(File file) 方法传到服务器中;

  6. 更新当前用户的头像的路径(web访问路径):http://localhost:8080/community/user/header/xxx.png, 调用 UserService 层更新数据库中用户头像的 url 地址;

  7. 重定向到首页。

@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 层:获取头像(服务器响应给客户端)
  1. 实现获取头像方法,从图片在服务器的路径中读取图片(字节输入流),使用 response 获取字节输出流向客户端写文件;
  2. 设置相应类型为:setContentType=("image/"+suffix);;
  3. 手动关闭我们自己创建的输入流(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 页面
  1. 上传头像的表单修改
  2. 手动处理如果没有文件为空的错误信息显示

上传头像
请上传图片!
2.11 测试页面
  1. 不上传头像(前端逻辑也过不去)

  1. 上传不带文件后缀的文件:

  1. 正确上传头像:

2.10 错误排查
  1. 必须在请求的 form 表单中设置上传格式为:enctype="multipart/form-data",否则的话报错;
Current request is not a multipart request
  1. 获取上传的图片的文件名字必须使用 photo.getOriginalFilename(); 方法,这样才是获取你上传的文件的名字,否则的话是不对的;

  2. 记得在 UserController 上面加上 @RequestMapping("/user"),代表在上下文路径下多一层目录,同时表单请求和 index.html 文件中的账号设置请求都要在请求中添加 /user 这一层路径:

  1. 上传不带文件后缀的文件时报错,表示文件没有后缀名的时候 lastIndexOf 这个方法默认为 -1 ,这时使用 substring 方法来进行字符串截取就会报索引越界异常:
java.lang.StringIndexOutOfBoundsException: begin -1, end 23, length 23

将 UserController 修改如下:

int separator = 0;
String suffix = fileName.substring(separator = fileName.lastIndexOf('.')==-1?fileName.length():separator);    
3、账号设置(修改密码) 3.1 UserService 层
  1. 实现修改密码功能:
  • 空值处理:输入旧密码不能为空,输入的新密码不能为空,输入的新密码的确认密码不能为空;
  • 验证状态:
    • 判断两次输入的新密码是否一致,验证输入的旧密码是否正确;
    • 将用户输入的旧密码使用 MD5 加密算法进行加密得到加密后的密码,将这个密码和当前用户的密码进行比较,如果不一致,代表输入的密码错误
  • 使登录凭证失效:
    • 调用 DAO 层修改登录凭证状态码为 1 ;
@Override
public Map updatePassword(User user,String oldPassword, String newPassword, String confirmPassword,String ticket) {
    Map map = new HashMap<>();
    //空值处理
    if (StringUtils.isBlank(user.getPassword())) {
        map.put("oldPasswordMsg", "请输入您原来的密码!");
        return map;
    }
    if (StringUtils.isBlank(newPassword)) {
        map.put("newPasswordMsg", "请输入您的新密码!");
        return map;
    }
    if (StringUtils.isBlank(confirmPassword)) {
        map.put("confirmPasswordMsg", "请再次输入您的新密码!");
        return map;
    }
    //两次输入的新密码是否一致
    if (!newPassword.equals(confirmPassword)) {
        map.put("confirmPasswordMsg", "两次输入密码不一致!");
        return map;
    }
    //验证旧密码是否正确,注册的时候在末尾加盐,这里也一样
    oldPassword = CommunityUtil.md5(oldPassword + user.getSalt());
    if (!user.getPassword().equals(oldPassword)){
        map.put("oldPasswordMsg", "您输入的密码不正确!");
        return map;
    }
    //修改密码
    newPassword = CommunityUtil.md5(newPassword + user.getSalt());
    userMapper.updatePassword(user.getId(),newPassword);
    //修改用户登录状态,修改密码之后让用户重新登陆,所以要将原来的登录状态改为退出
    loginTicketMapper.updateStatus(ticket, 1);

    return map;
}
3.2 UserController 层
  1. 实现修改密码的功能:
  2. 从当前线程获取用户信息
  3. 调用 UserService 层进行密码的校验;
  4. 根据返回的 map 集合信息判断要跳转的页面,并且显示错误信息,或者修改成功的信息。
//修改密码
@RequestMapping(path = "/update", method = RequestMethod.POST)
public String updatePassword(Model model, String oldPassword,
                             String newPassword, String confirmPassword,
                             @CookieValue("ticket") String ticket
) {
    User user = hostHolder.getUser();
    Map map = userService.updatePassword(user, oldPassword, newPassword, confirmPassword, ticket);
    if (map == null || map.isEmpty()) {
        model.addAttribute("msg", "密码修改成功,请重新登录!");
        model.addAttribute("target", "/login");
        return "site/operate-result";
    }else {
        model.addAttribute("oldPasswordMsg",map.get("oldPasswordMsg"));
        model.addAttribute("newPasswordMsg",map.get("newPasswordMsg"));
        model.addAttribute("confirmPasswordMsg",map.get("confirmPasswordMsg"));
        return "/site/setting";
    }
}
3.3 修改 setting.html 页面:
  1. 首先是前端对两次输入密码不一致的判定,这里参考 register.html 注册页面的验证方式,新增了一个 setting.js 文件,实现验证两次密码不一致时的错误信息显示:
$(function(){
    $("form").submit(check_data);
    $("input").focus(clear_error);
});

function check_data() {
    var pwd1 = $("#new-password").val();
    var pwd2 = $("#confirm-password").val();
    if(pwd1 != pwd2) {
        $("#confirm-password").addClass("is-invalid");
        return false;
    }
    return true;
}

function clear_error() {
    $(this).removeClass("is-invalid");
}
  1. 在 setting.html 最后引入该文件:

  1. 修改 form 表单的方法和行为:th:action="@{/user/update}" method="post";

  2. 修改所有输入文本框的 name 属性,必须和 UserController 中形参定义的参数名称一致,否则接收不到 form 表单提交的值;

  3. 错误信息展示:

    1. 用户输入的值还显示在页面上,可以从 request 中取值:th:value="${param.oldPassword}";
    2. 显示错误信息:th:text="${oldPasswordMsg}"
    3. 显示错误信息样式:th:

修改密码
密码长度不能小于8位!
密码长度不能小于8位!
两次输入的密码不一致!
3.4 测试页面
  1. 输入为空值的时候(前端实现了不允许为空值的情况):

  1. 输入新旧密码不一致的时候:

  1. 输入旧密码不正确的时候(虽然跳回到了设置页面,但是没有提示错误信息,检查页面接收到的响应代码没有问题,不知道为什么不显示):

下面这个是上传头像文件格式有误时候的前端代码,页面提示错误信息显示是正常的(费解!!!!):

  1. 输入正确情况:

并且登录凭证状态修改为1,即失效了。

3.5 错误排查
  1. 旧密码输入不正确的时候跳回到设置页面,没有显示错误信息(问题未解决!!!!!!!!!!!)
4、检查登录状态
  1. 在用户没有登录的情况下,拦截一些请求,放置知道访问路径的人可以通过手动修改访问路径去访问一下页面;

  • 常用的元注解(主要定义前两个就可以了):
    @Target:定义在类、方法、属性等上面
    @Retention:编译时有效or运行时有效
    @Document:生成文档的时候要不要带这个注解
    @Inherited:子类要不要继承父类的这个注解

  • 如何读取注解:
    Method.getDeclaredAnnotations ():获取方法上的所有注解
    Method.getAnnotation (Class annotationClass):尝试获取某个类型的注解

4.1 在 annotation 包下新建 LoginRequired 类
  1. 其中定义一个登录之后才能访问的页面的注解
  2. 给所有登录后才能访问的方法,加上这个自定义注解(所有关于用户设置的,都要在方法上方加上这个注解,但是获取用户头像不用这个注解,因为在首页的时候要显示所有帖子的用户头像)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {

}
4.2 新建 LoginRequiredInterceptor 类
  1. 注入 Hostholder,获取当前是否有用户信息
  2. 如果拦截到的是一个方法的话,将他转型
  3. 从方法上面取对应的自定义注解类型的注解
  4. 如果获取到了该注解,但是获取到的用户为空,我们就进行拦截,即返回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 配置拦截器:
  1. 将当前拦截器注入
  2. 排除对静态资源的拦截
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginRequiredInterceptor)
            .excludePathPatterns("*.css", "*.js", "*.png", "*.jpg", "*.jpeg");
}
4.4 测试页面
  1. 当我试图访问修改用户信息界面时,自动跳转到了登陆页面:



总结
  1. 修改密码的错误信息回显没有效果。可能是前端哪里出问题了。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1003311.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号