互助交流论坛系统 第二章 Spring Boot实践,开发社区登录模块

发送邮件

1. 邮箱设置

启用客户端SMTP服务

2. Spring Email

2.1 导入 jar 包

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    <version>2.1.5.RELEASE</version>
</dependency>

2.2 邮箱参数配置

#MailProperties
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.username=xxxxxxxxxxx@qq.com
spring.mail.password=xxxxxxxxx # 这里不是邮箱密码,是smtp的设备码
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true

2.3使用 JavaMailSender 发送邮件

// 发邮件
// to: 发给谁 subject: 主题 content: 标题
public void sendMail(String to, String subject, String content){
    // 利用JavaMailSender:构建MimeMessage类,就可以用其中的方法
    try {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message);
        helper.setFrom(from);
        helper.setTo(to);
        helper.setSubject(subject);
        helper.setText(content, true); // true表示支持html文本
        mailSender.send(helper.getMimeMessage());
    } catch (MessagingException e) {
        e.printStackTrace();
        // 记录错误日志
        logger.error("发送邮件失败:"+ e.getMessage());
    }
}

3. 模板引擎

使用 Thymeleaf 发送 HTML 邮件

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>邮件示例</title>
</head>
<body>
<p>欢迎你, <span style="color:red;" th:text="${username}"></span>!</p>
</body>
</html>
//  主动调用模板引擎
@Autowired
private TemplateEngine templateEngine;

@Test
public void testHtmlMail(){
    Context context = new Context();
    context.setVariable("username", "sunday");
    String content = templateEngine.process("/mail/demo", context);
    System.out.println(content);

    mailClient.sendMail("3338302451@qq.com", "Html", content);
}

开发自动注册功能

按照请求去拆解,然后按照三层去开发:数据访问层 - 业务层 - 视图层

1. 访问注册页面

  • 点击顶部区域内的链接,打开注册页面。
    利用Thymeleaf复用html标签
<!--便于复用-->
<header class="bg-dark sticky-top" th:fragment="header">

2. 提交注册数据

  • 导入commons Lang,便于处理字符串
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
   <version>3.9</version>
</dependency>
  • 配置网站的域名
# 配置网站的域名
# community
community.path.domain=http://localhost:8080
  • 写工具类
    • 生成随机字符串
    • MD5加密
public static String generateUUID(){
    return UUID.randomUUID().toString().replaceAll("-", "");
}

// MD5加密 - 使用MD5算法对密码进行加密
// MD5算法只能加密,不能解密,每次加密的值都一样
// hello -> abc123def456
// hello ->? abv123def456adc (在密码后加一个随机字符串,提高安全性)
public static String md5(String key){
    // 判定key是否为空
    if(StringUtils.isBlank(key)){
        return null;
    }
    // md5算法加密成16进制的字符串,再转换回来
    return DigestUtils.md5DigestAsHex(key.getBytes());
}

接下来就可以进行业务层的处理了

2.1 通过表单提交数据

UserService.java

  • 配置网站的域名和项目名,注入相应的数据
@Autowired
private MailClient mailClient;

@Autowired
private TemplateEngine templateEngine;

// 注入配置文件中的域名和项目名
// 注入固定的值使用“value"注解
@Value("${community.path.domain}")
private String domain;

@Value("${server.servlet.context-path}")
private String contextPath;
  • 编写业务方法
  • /**
    1. 对参数进行判断:是不是为空?属性有无问题?
    1. 验证账号:把传入的username和邮箱去库里查一下看有没有
    1. 如果前两步都没有问题,第三步就是注册用户:把用户注册到库里
    1. 发送激活邮件
      */

2.2 服务端验证账号是否已存在、邮箱是否已注册。

2.3 服务端发送激活邮件

public Map<String, Object> register(User user){
    Map<String, Object> map = new HashMap<>();

    // 空值处理
    if (user == null) {
        throw new IllegalArgumentException("参数不能为空!");
    }
    if (StringUtils.isBlank(user.getUsername())) {
        map.put("usernameMsg", "账号不能为空!");
        return map;
    }
    if (StringUtils.isBlank(user.getPassword())) {
        map.put("passwordMsg", "密码不能为空!");
        return map;
    }
    if (StringUtils.isBlank(user.getEmail())) {
        map.put("emailMsg", "邮箱不能为空!");
        return map;
    }

    // 验证账号
    User u = userMapper.selectByName(user.getUsername());
    if(u != null){
        map.put("usernameMsg", "该账号已存在");
        return map;
    }
    // 验证邮箱
    u = userMapper.selectByEmail(user.getEmail());
    if (u != null) {
        map.put("emailMsg", "该邮箱已被注册!");
        return map;
    }

    // 注册用户
    user.setSalt(CommunityUtil.generateUUID().substring(0, 6));
    user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
    user.setType(0);
    user.setActivationCode(CommunityUtil.generateUUID()); // 生成激活码
    user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); // 生成随机头像
    user.setCreateTime(new Date());
    userMapper.insertUser(user);

    // 激活邮件
    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(); //  userMapper.insertUser(user)
    context.setVariable("url", url);
    String content = templateEngine.process("/mail/activation", context);
    mailClient.sendMail(user.getEmail(), "激活账号", content);
    
    return map;
}

3. 激活注册账号

点击邮件中的链接,访问服务端的激活服务。
如果提交不成功,需要对register页面对错误信息进行处理

定义激活的接口并实现

CommunityConstant

ublic interface CommunityConstant {

    /**
     * 激活成功
     */
    int ACTIVATION_SUCCESS = 0;

    /**
     * 重复激活
     */
    int ACTIVATION_REPEAT = 1;

    /**
     * 激活失败
     */
    int ACTIVATION_FAILURE = 2;

}

实现激活的方法

返回激活的信息

/**
 * 先查用户,再判断激活码对不对
 */
private int activation(int userId, String code){
    User user = userMapper.selectById(userId);
    // 已经激活过了
    if(user.getStatus() == 1){
        return ACTIVATION_REPEAT;
    }else if(user.getActivationCode().equals(code)){
        userMapper.updateStatus(userId, 1);
        return ACTIVATION_SUCCESS;
    }else {
        return ACTIVATION_FAILURE;
    }

}
```java 
实现注册后页面跳转 
// http://localhost:8080/community/activation/101/code
// 返回激活结果的页面
@RequestMapping(path = "/activation/{userId}/{code}", method = RequestMethod.GET)
//@PathVariable: 从路径里取值
public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {
    int result = userService.activation(userId, code);
    if(result == ACTIVATION_SUCCESS) {
        model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了!");
        model.addAttribute("target", "/login");
    } else if (result == ACTIVATION_REPEAT) {
        model.addAttribute("msg", "无效操作,该账号已经激活过了!");
        model.addAttribute("target", "/index");
    } else {
        model.addAttribute("msg", "激活失败,您提供的激活码不正确!");
        model.addAttribute("target", "/index");
    }
    return "/site/operate-result";
}

访问注册页面

@RequestMapping(path = "/login", method = RequestMethod.GET)
public String getLoginPage() {
    return "/site/login";
}

会话管理

1. HTTP的基本性质

  • HTTP是简单的
  • HTTP是可扩展的
  • HTTP是无状态的,有会话的
    无状态:两个执行成功的请求之间没有关系,因此用户没有办法在同一个网站进行连续的交互
    HTTP Cookies可以解决这个问题:把Cookles添加到头部,创建一个会话让每次请求都能共享相同的上下文信息,达成相同的状态
    HTTP本质是无状态的,使用Cookies可以创建有状态的会话

2. 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,不需要的就不会发送cookie
    // 在该路径和子路径下有效
    cookie.setPath("/community/alpha");
    // 设置cookie的生存时间
    cookie.setMaxAge(60 * 10);
    // 发送cookie
    response.addCookie(cookie);

    return "set cookie";
}

在这里插入图片描述

获取cookie

@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
    System.out.println(code);
    return "get cookie";
}
@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
    System.out.println(code);
    return "get cookie";
}

3. Session

cookie的数据是存在在客户端的,每次访问服务器都会对流量造成影响 -> session
是JavaEE的标准,用于在服务端记录客户端信息。
数据存放在服务端更加安全,但是也会增加服务端的内存压力。
在这里插入图片描述

示例

// 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";
}

在分布式部署下使用session

存在问题

https://segmentfault.com/a/1190000022404396

  • 客户端存储
  • session复制
  • session绑定
  • 基于redis存储session方案

生成验证码

Kaptcha

1. 导入 jar 包

<!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
<dependency>
   <groupId>com.github.penggle</groupId>
   <artifactId>kaptcha</artifactId>
   <version>2.3.2</version>
</dependency>

2. 编写 Kaptcha 配置类

@Configuration
public class KaptchaConfig {

    @Bean
    public Producer KaptchaProducer() {
        
        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 kaptcha = new DefaultKaptcha();
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

3. 生成随机字符、生成图片

需要把验证码信息存到sessionl里

// 生成验证码的方法
// 验证码不能存在浏览器端 -> 使用session
@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKacha(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();
        ImageIO.write(image, "png", os);
    } catch (IOException e) {
        logger.error("响应验证码失败:" + e.getMessage());
    }

}

实现js方法,刷新验证码

<!--实现Js方法 刷新验证码:这个js将调用这个方法,实现逻辑-->
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>

<script>
   function refresh_kaptcha() {
      var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
      $("#kaptcha").attr("src", path);
   }
   }
</script>

开发登录、退出功能

1. 访问登录页面

  • 点击顶部区域内的链接,打开登录页面。(上一步已经实现)

2. 登录

2.1 dao层

  • 实现实体类LoginTicket
  • 实现接口(这里在接口文件里写sql语句)
@Mapper
public interface LoginTicketMapper {

    // 插入数据
    @Insert({
            "insert into login_ticket(user_id,ticket,status,expired) ",
            "values(#{userId},#{ticket},#{status},#{expired})"})
    @Options(useGeneratedKeys = true, keyProperty = "id") // 能让key自增
    int insertLoginTicket(LoginTicket loginTicket);

    @Select({
            "select id,user_id,ticket,status,expired ",
            "from login_ticket where ticket=#{ticket}"
    })
    LoginTicket selectByTicket(String ticket);

    // 修改凭证的状态
    @Update({
            "<script>" //script标签表示脚本
            "update login_ticket set status=#{status} where ticket=#{ticket} ",
            "<if test=\"ticket!=null\"> ",
            "and 1=1 ",
            "</if>",
    })
    int updateStatus(String ticket, int status);

}

写完之后进行测试

2.2 业务层

验证账号、密码、验证码
  • 空值处理
  • 验证账号、状态、密码是否和数据库一样(密码是明文,需要+salt后再调用md5算法)
    用map返回错误信息(和注册时的一样)
成功时,生成登录凭证,发放给客户端。

(把敏感信息存到数据库里login_ticket)
expired:过期时间-凭证什么时候过期?
Status: 0-有效 1-无效
[图片]

这个表起到类似session的作用,只需要ticket作为登录凭证,只要在map里存ticket

退出
  • 将登录凭证修改为失效状态,status状态改为1(失效)
  • 转至网站首页
public void logout(String ticket) {
        loginTicketMapper.updateStatus(ticket, 1);
    }

2.3controller层

  • 设置默认登录凭证和记住状态的登录凭证超时时间
/**
     * 默认状态的登录凭证的超时时间
     */
    int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

    /**
     * 记住状态的登录凭证超时时间
     */
    int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
  • 检查验证码是否正确
  • 看用户是否勾选了“记住我”
    失败时,跳转回登录页。,如果没勾选,就是默认状态,如果勾选,就是记住状态
// 检查账号密码:根据rememberme检查
    int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
    Map<String, Object> map = userService.login(username, password, expiredSeconds);
    if(map.containsKey("ticket")) {
        // 向客户端发送一个cookie,带ticket
        Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
        cookie.setPath(contextPath);
        cookie.setMaxAge(expiredSeconds);
        response.addCookie(cookie);
        return "redirect:/index";
    }else{
        // 如果错误,就返回错误信息给model
        model.addAttribute("usernameMsg", map.get("usernameMsg"));
        model.addAttribute("passwordMsg", map.get("passwordMsg"));
        return "/site/login";
    }
}
  • 修改html表单信息
  • 表单默认值处理: th:value=“${param.username}”
退出
  • 获得存在cookie里的ticket,调用service层中的logout方法,从而把数据库中的ticket的status改为失效
  • 重定向到login
  • 修改index.html中的链接
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    return "redirect:/login";
}

显示登录信息

1. 拦截器简介

根据登录与否需要调整头部显示的内容,每个请求都需要进行统一的处理 -> 拦截器
以非常低的耦合度解决通用的问题
拦截器属于请求,放在controller里

  • 拦截器示例
    • 定义拦截器,实现HandlerInterceptor
    • 配置拦截器,为它指定拦截、排除的路径
  • 拦截器应用
    • 在请求开始时查询登录用户
    • 在本次请求中持有用户数据
    • 在模板视图上显示用户数据
    • 在请求结束时清理用户数据

2. 代码示例

定义拦截器,实现HandlerInterceptor

@Component
public class AlphaInterceptor implements HandlerInterceptor {
    // HandlerInterceptor 不强制实现所有的方法

    private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);

    // 在Controller之前执行。false就是不往下执行了,一般返回true
    @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());
    }

    // 在模板引擎执行之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.debug("afterCompletion: " + handler.toString());

    }
}

配置拦截器,为它指定拦截、排除的路径

// 一般实现config是为了配置bean,但是拦截器是为了实现接口
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 注入拦截器
    @Autowired
    private AlphaInterceptor alphaInterceptor;

    // 利用调用的对象注册interceptor
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(alphaInterceptor)
            //    排除所有目录下的css文件 双星号**表示在任意目录下
            .excludePathPatterns("/**/*.css\", \"/**/*.js\", \"/**/*.png\", \"/**/*.jpg\", \"/**/*.jpeg")
            //    添加拦截的路径
            .addPathPatterns("/register", "login");
    }
}

3. 项目实现

在这里插入图片描述

一个工具:持有用户信息,用于代替session对象

因为实际上可能有多个用户访问,是多线程,所以需要线程隔离-利用Threadlocal实现(Threadlocal是通过map实现线程隔离的)

/**
 * 持有用户信息,用于代替session对象
 * Threadlocal能实现线程隔离(通过map实现)
 */
@Component
public class HostHolder {

    private ThreadLocal<User> users = new ThreadLocal<>();

    public void setUser(User user) {
        users.set(user);
    }

    public User getUser() {
        return users.get();
    }
}

定义拦截器

  • preHandle:
    查询凭证,根据凭证查询用户,再存入hostholder里
  • postHandle
    postHandle是在模板引擎之前调用的,从hostholder里得到当前线程user,把它存进model里
  • afterCompletion
    在模板执行完之后,清空hostHolder
@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获得ticket
        String ticket = CookieUtil.getValue(request, "ticket");
        if(ticket != null) {
            // 查询凭证
            LoginTicket loginTicket = userService.findLoginTicket(ticket);
            // 检查凭证是否有效:不为空 && 状态=0 && 超时时间晚于当前时间
            if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                // 根据凭证查询用户
                User user = userService.findUserById(loginTicket.getUserId());
                // 在本次请求中持有用户(要考虑多线程的情况,考虑线程的隔离)处理完请求之后线程被销毁,其中的时间里线程一直都在
                hostHolder.setUser(user);
            }
        }
        return true;
    }

    /**
     * postHandle是在模板引擎之前调用的,
     * 可以从hostholder里得到当前线程user,把它存进model里
     */
    @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);
        }
    }

    /**
     * 在模板执行完之后,清空hostHolder
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        hostHolder.clear();
    }
}

配置拦截器

  • 注入
  • 配置
registry.addInterceptor(loginTicketInterceptor)
        //    排除所有目录下的css文件 双星号**表示在任意目录下
        .excludePathPatterns("/**/*.css\", \"/**/*.js\", \"/**/*.png\", \"/**/*.jpg\", \"/**/*.jpeg")

在模板上处理

  • 改写index-header
    th:if="${loginUser!=null}
  • 换掉头像的url和当前登录用户的用户名
    th:src=“ l o g i n U s e r . h e a d e r U r l " t h : u t e x t = " {loginUser.headerUrl}" th:utext=" loginUser.headerUrl"th:utext="{loginUser.username}”

用th:text不会解析html,用th:utext会解析html,在页面中显示相应的样式

账号设置

1. 上传文件

  • 请求:必须是POST请求
  • 表单:enctype=“multipart/form-data”
  • Spring MVC:通过 MultipartFile 处理上传文件

2. 开发步骤

  • 访问账号设置页面
  • 上传头像
  • 获取头像

3. 代码实现

上传文件

  • 设置上传路径
community.path.upload=d:/work/data/upload
  • 请求:必须是POST请求
  • 表单:enctype=“multipart/form-data”
  • Spring MVC:通过 MultipartFile 处理上传文件
    上传之后,改变用户的Url

service层

// 更新用户头像
public int updateHeader(int userId, String headerUrl) {
    return userMapper.updateHeader(userId, headerUrl);
}

controller层

  • 生成随机的名字
    • 获得文件名的后缀:.jpg .png之类的
    • 生成随机的文件名
    • 拼接前两个
  • 确定文件的存放路径
  • 存储文件
  • 更新当前用户的头像的路径(是web访问路径)
  • 重定向到首页
@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();
    // lastIndexOf() 方法返回调用String 对象的指定值最后一次出现的索引
    // 获得文件的后缀 substring(int beginIndex)
    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";
}
  • 向浏览器响应图片
/**
 * 向浏览器响应图片
 */
@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 (
            // what the hell ? -> java7语法 似乎可以自动关闭
            FileInputStream fis = new FileInputStream(fileName);
            OutputStream os = response.getOutputStream();
    ){
        // 每次最多输入1024字节个数据
        byte[] buffer = new byte[1024];
        int b = 0;
        // b != -1 代表读到数据,可以继续读下去,=-1就代表结束
        while ((b = fis.read(buffer)) != -1){
            os.write(buffer, 0, b);
        }
    } catch (IOException e) {
        logger.error("读取头像失败:" + e.getMessage());
    }
}
配置setting.html页面的表单
<form class="mt-5" method="post" enctype="multipart/form-data" th:action="@{/user/upload}">

检查登录状态

如果不检查登录状态的话,假设用户知道路径,就可以直接通过路径访问敏感信息
如果用户试图访问这些信息,进行判断:没登录拒绝,登录同意

使用注解

  • 使用拦截器
    • 在方法前标注自定义注解
    • 拦截所有请求,只处理带有该注解的方法
  • 自定义注解
    自定义注解需要用元注解进行定义
    • 常用的元注解:
      • @Target:声明自定义注解可以写在哪个位置,作用于什么类型
      • @Retention:声明自定义注解保留的时间
      • @Document:声明生成文档时要不要把这个注解带上去
      • @Inherited:声明子类和父类的关系,如果父类有注解,那么子类要不要继承下来
  • 如何读取注解:
  • Method.getDeclaredAnnotations ()
  • Method.getAnnotation (Class annotationClass)

代码实现

  • 新建注解包:annotation
    使用元注解定义这个注解
@Target(ElementType.METHOD) // 注解可以写在方法上
@Retention(RetentionPolicy.RUNTIME) // 声明这个注解在程序运行时才有效
public @interface LoginRequired {
    
}
  • 在controller的方法上加注解
  • 使用拦截器拦截这个带有注解的方法
@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;
            // getMethod方法可以获取拦截到的method对象
            Method method = handlerMethod.getMethod();
            // 从method对象中取注解
            LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
            if(loginRequired != null && hostHolder.getUser() == null) {
                // 强制重定向到页面
                response.sendRedirect(request.getContextPath() + "/login");
                return false;
            }
        }
        return true;
    }
}
  • 对拦截器进行配置
registry.addInterceptor(loginRequiredInterceptor)
        //    排除所有目录下的css文件 双星号**表示在任意目录下
        .excludePathPatterns("/**/*.css\", \"/**/*.js\", \"/**/*.png\", \"/**/*.jpg\", \"/**/*.jpeg");
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值