第2章 Spring Boot实践,开发社区登录模块(下)

5. 开发登录、退出功能

image-20220710214648224

1. 访问登录页面

点击顶部区域的“登录”按钮,打开登录页面。这个功能之前我们已经实现过了,所以我们无须实现

2. 登录

生成的登录凭证最终是要发送一个key给客户端,然后让它记住,下次提交给服务端,以便能够识别。但是这个登录凭证里面包括了一些敏感的数据,包括用户的id、用户名、密码,这些数据不能发送给客户端要存到服务端,我们可以存到session里或者数据库里,这里我们把它存到数据库里,将来我们重构的时候再把它存到redis里。

我们先看一些登录表

CREATE TABLE `login_ticket` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `ticket` varchar(45) NOT NULL,
  `status` int(11) DEFAULT '0' COMMENT '0-有效; 1-无效;',
  `expired` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `index_ticket` (`ticket`(20))
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

image-20220711100711300

开发顺序 数据访问层(dao)—> 业务层(service)—> 视图层(controller、themeleaf模板)

与登录表login_ticket对应的实体类

public class LoginTicket {

    private int id;
    private int userId;         // 用户id
    private String ticket;      // 登录凭证
    private int status;      // 登录状态(当前登录凭证是否有效)
    private Date expired;     // 过期时间(凭证失效时间)

		// 为了影响阅读体验,这里没有粘get、set、toString方法,实际上是有的    
}

数据访问层(dao)

dao接口

之前我们实现dao接口是新建一个mapper配置文件写sql,除了这种方式我们还可以在mapper接口中写注解去实现方法。这里我们使用注解的方式实现一下dao接口中的方法。

实现dao方法的注解之中允许书写多个字符串,用逗号隔开即可,到时候会自动拼接


@Mapper
public interface LoginTicketMapper {

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

    @Select({
            "select id, user_id, ticket, status, expired ",
            "from login_ticket where ticket = #{ticket}"
    })
    LoginTicket selectByTicket(String ticket);      // 根据凭证查询LoginTicket

    @Update({
            "update login_ticket set status = #{status} where ticket = #{ticket}"
    })
    int updateStatus(String ticket, int status);    // 更新凭证状态为status
}

image-20220711115458882

关于 if 标签如何写

image-20220711142204638

数据访问层写完之后最好做一个测试,因为这里比较容易出错。

测试类:

@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {

    @Autowired
    private LoginTicketMapper loginTicketMapper;

    @Test
    public void testInsertLoginTicket(){
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(101);
        loginTicket.setTicket("abc");
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10));
        loginTicketMapper.insertLoginTicket(loginTicket);
    }

    @Test
    public void testSelectAndUpdateTicket(){
        LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc");
        System.out.println(loginTicket);
        loginTicketMapper.updateStatus("abc", 1);       // 失效改为1
        loginTicket = loginTicketMapper.selectByTicket("abc");
        System.out.println(loginTicket);
    }
}

业务层(service)

登录是属于用户的行为,所以我们在UserService写登录的业务。

注入 LoginTicketMapper

登陆的时候可能成功、失败,失败原因有可能是账号没输入,账号不存在, 账号没有激活,等等,所以我们返回一个map,可以封装多种情况的返回结果,

// 为了保证UserService的完整性,所以全部粘过来了,其实我们这里写的只是1.注入LoginTicket 2.登录方法
@Service
public class UserService implements CommunityConstant {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    @Autowired
    private LoginTicketMapper loginTicketMapper;

    @Value("${community.path.domain}")      // @Value 注解会将 application.properties 配置文件中的指定值注入给它
    private String domain;                  // 域名

    @Value("${server.servlet.context-path}")
    private String contextPath;             // 项目名

    public User findUserById(int id){
        return userMapper.selectById(id);
    }

    // 注册方法:注册需要 username、password、email,所以传入一个 user
    // 返回的应该是相关信息,比如“账号已存在、邮箱不能为空”等等,所以为了封装多个内容,返回Map
    public Map<String, Object> register(User user){
        Map<String, Object> map = new HashMap<>();

        // 先进行空值处理 user 为 null
        // (username为null、password为null、email为null 或者 全部为 null)

        // 空值处理
        if(user == null){
            throw new IllegalArgumentException("参数不能为空");
        }

        // 账号/密码/邮箱 为空是业务上的漏洞,但是不是程序的错误,因此我们把信息封装到map里面返回给客户端
        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;
        }

        // 如果可以执行到这里说明账号/密码/邮箱都不为空,且账号/邮箱都未注册

        // 这里我们要对用户输入的密码后面加一个salt(随机字符串)(password+salt 之后md5加密之后才真正存到数据库的)
        // salt 一般 5 位就够了
        user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
        user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
        user.setType(0);            // 0 表示普通用户
        user.setStatus(0);          // 0 表示没有激活
        user.setActivationCode(CommunityUtil.generateUUID());   //  设置激活码
        // 图片url:https://images.nowcoder.com/head/0t.png   注:从 0 - 1000 都是图片
        user.setHeaderUrl(String.format("https://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); //设置图片的路径
        user.setCreateTime(new Date());     // 设置注册时间

        userMapper.insertUser(user);        // 存入数据库,本来user的id是null,直行这句之后mysql生成id赋值给了它

        // 给用户发送 html 激活邮件,好带有链接
        // 给用户发送发送邮件

        // 给 themeleaf 模板传参
        Context context = new Context();       // themeleaf 包下的 Context
        context.setVariable("email", user.getEmail());


        // 项目路径下某个功能哪个用户在激活激活码是什么
        // http://localhost:8080/community/activation/101/code
        String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();

        // 由于拼接url可能会造成空格问题,空格在浏览器地址栏中会解析成 %20 ,造成错误,所以我们要将url中的空格去掉
        url = url.replaceAll(" ", "");
        context.setVariable("url", url);


        // 调模板引擎生成动态网页   参数1:模板引擎的路径   参数2:数据
        // 会生成一个动态网页,其实就是一个字符串,模板引擎主要的作用就是生成动态网页
        String content = templateEngine.process("/mail/activation", context);

        // 发邮件    参数1:收件人    参数2:邮件标题      参数3:邮件内容
        System.out.println(user.getEmail());

        try {
            mailClient.sendMail(user.getEmail(), "激活账号", content);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 最后没有问题的话也返回map,且这里map是空的

        return map;
    }

    // 激活方法   参数1:用户id      参数2:激活码
    public 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;
        }
    }

    // 参数1:用户名  参数2:密码  参数3:过期的秒数
    public Map<String, Object> login(String username, String password, int expiredSeconds){
        Map<String, Object> map = new HashMap<>();

        // 空值处理
        if(StringUtils.isBlank(username)){
            map.put("usernameMsg", "账号不能为空!");
            return map;
        }
        if(StringUtils.isBlank(password)){
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }

        // 验证账号
        User user = userMapper.selectByName(username);
        if(user == null){
            map.put("usernameMsg", "该账号不存在!");
            return map;
        }

        // 验证状态
        if(user.getStatus() == 0){
            map.put("usernameMsg", "该账号未激活!");
            return map;
        }

        // 验证密码
        password = CommunityUtil.md5(password + user.getSalt());
        if(!user.getPassword().equals(password)){
            map.put("passwordMsg", "密码不正确!");
            return map;
        }

        // 如果上面的都正确说明 账号和密码 都正确
        // 生成登录凭证
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());
        loginTicket.setStatus(0);       // 0 表示当前凭证有效
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000)); // *1000 是从毫秒换算成秒
        loginTicketMapper.insertLoginTicket(loginTicket);

        map.put("ticket", loginTicket.getTicket());  // 返回登录凭证给客户端
        return map;
    }
}

image-20220711163608928

image-20220711163721654

image-20220711163913712

视图层(Controller、themeleaf)

因为是登录,所以我们在LoginController里写Controller

UserService以前已经注入过了,不需要重新注入了。

注:controller中的不同方法的请求路径可以相同,但是请求方式一定要区分开

@Controller
public class LoginController implements CommunityConstant {

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

    @Autowired
    private UserService userService;

    @Autowired
    private Producer kaptchaProducer;

    @Value("${server.servlet.context-path}")
    private String contextPath;

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

    // 处理请求,因为是浏览器向我们提交数据,所以请求是POST方式
    @RequestMapping(path = "/register", method = RequestMethod.POST)
    public String register(Model model, User user){
        // 注意:因为User声明在了方法的参数里,SpringMVC会自动把user存到model里

        Map<String, Object> map = userService.register(user);

        // map为空说明注册成功,我们应该提示浏览器注册成功,然后跳转到首页页面,之后激活之后才跳转到登录页面
        if(map == null || map.isEmpty()){
            model.addAttribute("msg", "注册成功,我们已经向您的邮箱发送了一封激活邮件,请尽快激活!");
            model.addAttribute("target", "/index");
            return "/site/operate-result";
        } else {
            // 有错误,传给页面信息并返回登录页面
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            model.addAttribute("emailMsg", map.get("emailMsg"));
            return "/site/register";
        }
    }

    // http://localhost:8080/community/activation/101/code
    @RequestMapping(path = "activation/{userId}/{code}", method = RequestMethod.GET)
    public String activation(Model model,
                             @PathVariable("userId") int userId,
                             @PathVariable("code") String code){
        // 这个结果的含义可以从结果中识别,所以也需让LoginController实现CommunityConstant接口
        int result = userService.activation(userId, code);
        // 无论成功还是失败,都跳转到中转页面只是返回给中转页面的提示信息不同,然后从中转页面跳转到哪里根据激活是否成功决定
        if(result == ACTIVATION_SUCCESS){
            // 激活成功跳转到登录页面
            model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了!");
            model.addAttribute("target", "/login");  // 返回给服务器,服务器跳转到登录的controller
        } else if(result == ACTIVATION_REPEAT){
            // 邮箱之前已经激活过了,重复了
            model.addAttribute("msg", "无效操作,该账号已经激活过了!");
            model.addAttribute("target", "/index");  // 跳转到展示首页的controller
        } else {
            // 激活失败
            model.addAttribute("msg", "激活失败,您提供的激活码不正确!");
            model.addAttribute("target", "/index");  // 跳转到展示首页的controller
        }
        return "/site/operate-result";
    }

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

    @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    public void getLKaptcha(HttpServletResponse response, HttpSession session){
        // 生成验证码
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);    // 将验证码传入生成图片

        // 将验证码存入session
        session.setAttribute("kaptcha", text);

        // 将图片输出给浏览器
        response.setContentType("image/png");       // 声明返回给浏览器的是什么可视的数据
        // response向浏览器做响应我们需要获取它的输出流
        try {
            ServletOutputStream os = response.getOutputStream();
            // 这个流不用关,因为是SpringMVC维护,会自动关
            ImageIO.write(image, "png", os);    // 输出哪个图片; 格式; 哪个流输出
        } catch (IOException e) {
            logger.error("响应验证码失败:" + e.getMessage());
        }
    }

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    //              参数1:用户名   参数2:密码   参数3:验证码  参数4:是否勾上(登录页面有一个"记住我",这个参数表示)
    // 用户打开登录页面生成了验证码,放到了session里,所以也要声明session,把验证码从session里取出来
    // 如果登陆成功了,我们最终要把ticket发放给客户端好让它保存,要想使用cookie,我们还需要HttpServletResponse对象
    public String login(String username, String password, String code, boolean rememberme,
                        Model model, HttpSession session, HttpServletResponse response){
        /**
         * 如果方法中有复杂参数,会自动添加到model,但是String、boolean这样的简单参数不会自动加到model里
         * 解决方法:1. 手动加到model里  2. 或者从request里面取
         */
        // 检查验证码
        // 获取session中的验证码
        String kaptcha = (String)session.getAttribute("kaptcha");
        if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){
            // 如果获取的验证码为空或者用户传入的验证码为空或者获取的验证码和用户输入的验证码不同
            model.addAttribute("codeMsg", "验证码不正确!");
            return "/site/login";       // 返回登录页面
        }

        // 检查账号、密码(交给Service处理)
        int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
        Map<String, Object> map = userService.login(username, password, expiredSeconds);
        if(map.containsKey("ticket")){
            // 成功登录
            // 给客户端发一个cookie,里面包含登录凭证
            Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
            // 设置这个cookie生效的路径,凭证有效的路径应该包含在整个项目之内
            cookie.setPath(contextPath);
            cookie.setMaxAge(expiredSeconds);       // 设置cookie的生存时间
            response.addCookie(cookie);
            // 重定向到首页
            return "redirect:/index";
        } else {
            // 登陆失败
            model.addAttribute("usernameMsg", map.get("usernameMsg"));
            model.addAttribute("passwordMsg", map.get("passwordMsg"));
            return "/site/login";       // 返回登录页面
        }
    }

}

登陆的时候没有勾上“记住我”,那依然是把它存到库里,只是存的时间短一点;如果勾上“记住我”,也是把它存到库里,但是存的时间很长, 所以我们定义两个常量,常量我们还是写到CommunityConstant这个接口中因为之前 LoginController 已经实现了CommunityConstant这个接口,所以我们可以直接开始用这个里面的两个时间常量了。

image-20220711175614042

image-20220711181711348

参数不要写死,注入 contextPath

image-20220711193610007

最后处理一下 login.html 表单,

image-20220711193823108

image-20220711193921355

param.username 相当于 request.getUserName()

开发到这里之后就可以进行测试了,看是否可以登陆成功

3. 退出

因为之前dao层已经实现过更新,所以这里直接写业务层(service)

public void logout(String ticket){
    loginTicketMapper.updateStatus(ticket, 1);      // 改变凭证状态为1,表示凭证无效
}

image-20220711195445756

接下来是Controller

@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket){
    userService.logout(ticket);
    return "redirect:/login";   // 重定向的时候默认就是get请求的login
}

image-20220711195532055

最后我们还要修改一下 index.html 的退出按钮

image-20220711195634830

之后进行测试就可以了

6. 显示登录信息

image-20220712091339538

显示登录信息的意思是在头部把登录用户的头像显示出来,另外点开头像旁边的**“倒三角”,要显示登录用户的名字,还有,根据用户登录与否我们还要调整头部显示的内容,比如“登录”“注册”**。

拦截器可以拦截浏览器的请求,它可以在请求的开始或结束插入一些代码,从而可以解决多个请求共有的业务

关于拦截器详细内容这里不过多赘述,更详细内容可以看我之前的博客:SpringBoot中文件下载、拦截器、war包部署、jar包部署

拦截器的简单使用(只是测试,不涉及项目内容)

@Component
public class AlphaInterceptor implements HandlerInterceptor {

    private static 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, 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());
    }
}

image-20220712102845535

拦截器的配置类

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AlphaInterceptor alphaInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(alphaInterceptor)       // 将需要配置的拦截器传给它
                //  /** 表示static目录下所有的文件夹,排除哪些路径不需要拦截
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
                // 明确拦截注册、登录请求
                .addPathPatterns("/register", "/login");

    }
}

image-20220712102947799

之后启动项目访问登录注册页面查看控制台是否输出拦截器内的日志即可查看测试是否成功。

用拦截器处理在页面上显示用户的登录状态

image-20220712104026516

登录成功的话上面凭证查询然后在模板上显示每次请求都要干的,因为每次请求模板上都要显示用户信息,因此这套逻辑应该用拦截器处理,而不是写多次。

cookie是从request传过来的,从request我们可以得到cookie,从request取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;
    }
}

image-20220712171916990

首先因为UserService里面没有查询凭证的方法,所以我们在UserService后面追加查询凭证的方法

public LoginTicket findLoginTicket(String ticket){
    return  loginTicketMapper.selectByTicket(ticket);
}

image-20220712172028909

在拦截其中我们本次请求中持有用户,因为浏览器对服务器是多对一,所以这里我们要考虑在多线程间隔离存这个对象, 需要用到threadLocal,这里我们封装成小工具,方便以后复用

/**
 * 持有用户信息,用于代替session对象
 */
@Component
public class HostHolder {

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

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

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

    public void clear(){
        users.remove();
    }

}

image-20220712172135212

拦截器

@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);
            // 检查凭证是否有效,1. 凭证存在   2. 凭证状态为0   3. 凭证超时时间晚于当前时间
            if(loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())){
                // 根据凭证查询用户
                User user = userService.findUserById(loginTicket.getUserId());
                // 在本次请求中持有用户
                // 因为浏览器对服务器是多对一,所以这里我们要考虑在多线程间隔离存这个对象,
                // 需要用到threadLocal,这里我们封装成小工具,方便以后复用
                hostHolder.setUser(user);
            }
        }
        return true;
    }

    // 我们之前preHandle存的user应该在模板引擎之中用,所以我们在拦截器的postHandle方法中将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);
        }
    }

    // 这个方法是在模板引擎之后执行的。在整个请求结束之后
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        hostHolder.clear();
    }
}

image-20220712172321270

image-20220712172434371

拦截器写完了,接下来我们对拦截器进行一个配置

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AlphaInterceptor alphaInterceptor;

    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(alphaInterceptor)       // 将需要配置的拦截器传给它
                //  /** 表示static目录下所有的文件夹,排除哪些路径不需要拦截
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
                // 明确拦截注册、登录请求
                .addPathPatterns("/register", "/login");

        registry.addInterceptor(loginTicketInterceptor)       // 将需要配置的拦截器传给它
                //  /** 表示static目录下所有的文件夹,排除哪些路径不需要拦截
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
                // 注:没有写addPathPatterns表示没有明确拦截哪个路径,那就是拦截所有
    }


}

image-20220712172554074

然后我们需要在模板上进行一个处理,因为所有的页面都是复用 index.html的header,所以我们需要去改写index.html的header部分

image-20220712171749946

测试:

image-20220712172630689

image-20220712172652848

7. 账号设置

image-20220713093142281

上传头像
1. 访问账号设置页面

访问很简单,就是写一个controller跳转到**“账号设置页面”,然后修改一下“账号设置页面”的路径即可,最后因为所有页面都使用了index.html,我们呢还要设置一下index.html头部的“账号设置”的路径**。

因为访问账号设置页面是属于用户的内容,所以我们新创建一个 UserController,在里面写访问“账号设置页面”的方法。

@Controller
@RequestMapping("/user")
public class UserController {

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

image-20220713095221891

账号设置页面 setting.html

image-20220713095404756

image-20220713095445354

因为所有页面都复用了 index.html 的头部,我们要修改一下 index.html 头部的“账号设置” 的链接

image-20220713095716535

访问账号设置页面写完之后建议启动项目测试一下

2. 上传头像

上传文件我们要在配置文件 application.properties 配置一下这个文件最终要存放到哪个硬盘上

注:文件上传路径必须存在,即 upload 必须存在

community.path.upload=d:/work/data/upload

image-20220713104423969

image-20220713104454709

数据访问层dao

因为之前更新头像的url之前写过,还有是把文件存到硬盘里,而不是数据库里,所以 数据访问层dao没有什么可做的。


业务层service

业务层service只处理更新路径的需求

public int updateHeader(int userId, String headerUrl){
    return userMapper.updateHeader(userId, headerUrl);
}

image-20220713104337388

表现层controller

表现层controller负责上传文件的动作


@Controller
@RequestMapping("/user")
public class UserController {
    // 日志
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);
    // 注入配置文件中的上传路径
    @Value("${community.path.upload}")
    private String uploadPath;
    // 注入配置文件中的项目的域名
    @Value("${community.path.domain}")
    private String domain;
    // 注入配置文件中的项目名
    @Value("${server.servlet.context-path}")
    private String contextPath;
    // 更新图片url需要用到 UserService
    @Autowired
    private UserService userService;
    // 当前用户得从hostHoldear里取,所以注入 HostHolder
    @Autowired
    private HostHolder hostHolder;

    // 访问“账号设置”页面
    @RequestMapping(path = "/setting", method = RequestMethod.GET)
    public String getSettingPage(){
        return "/site/setting";
    }

    // 上传文件
    // "上传"的表单的提交方式必须为POST
    // MultipartFile 用来接收上传的文件,Model 用来给模板携带数据
    @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();
        // 从 . 开始截取直到最后,截取文件后缀名
        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;
        headerUrl = headerUrl.replace(" ", "");
        userService.updateHeader(user.getId(), headerUrl);

        return "redirect:/index";
    }

}

image-20220713104608601

image-20220713104706169

image-20220713120043758

3. 获取头像

controller获取头像

UserController

// 获取头像
// 这个方法向浏览器响应的不是一个网页,不是一个字符串,而是一个图片,二进制的数据
// 我们需要通过流手动向浏览器输出,所以返回值为void,方法内部调response写
@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(".") + 1);
    // 响应图片
    response.setContentType("image/" + suffix);
    try (
        // java7语法,在这里写的会自动在后面加上finally,执行close关闭方法
        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());
    }
}

image-20220713114023904

配置setting.html表单

image-20220713120358417

8.检查登录状态

用户没登陆的时候有一些功能不能访问的,但是如果我们后台没有“检查登录状态”,在没登录的情况下通过在地址栏输入http://localhost:8080/community/user/setting 可以访问 “上传文件”的功能,这种行为会对我们的系统造成很大的安全隐患,所以我们需要去杜绝这种行为。

因此不是说不让他(她)看见就行,得一定是他(她)无论如何都访问不到才可以。

解决:在用户没有登录的时候,在访问不可以访问的功能的时候,应该在服务端进行一个判断,登陆了可以访问,没登录拒绝。

可以想到的是,很多功能都需要这样的判断,众多请求都有一样的逻辑,我们用拦截器处理。

image-20220713152102515

image-20220713152115947

拦截器拦截哪个方法有两种设置:

一种是写拦截器的配置文件

还有一种是拦截哪个的话在哪个方法/类上加上注解(这种方式我们需要自定义注解)

这两种可以只使用一种也可以两种都用,接下来我们两种方式都用开发一下检查登录状态的拦截器。

1. 自定义注解

自定义注解

我们在定义自己的注解的时候需要用元注解定义我们自己的注解

  • @Target
    • 用来声明自定义的注解可以写在哪个位置:类 / 方法
  • @Retention
    • 用来声明自定义注解保留的时间或有效时间:编译时有效 / 运行时有效
  • @Document
    • 用来声明自定义注解在生成文档时要不要把这个注解也带上去
  • @Inherited
    • 表示这个自定义注解可不可以被子类继承

自定义注解的时候前两个元注解基本上是必须写的

自定义注解:

@Target(ElementType.METHOD)         // 表示自定义的这个注解应该写在方法之上
@Retention(RetentionPolicy.RUNTIME) // 表示这个注解在程序运行的时候有效
public @interface LoginRequired {

}

image-20220713163002175

这个注解是用来标识这个方法它需不需要在登陆的状态下才能访问,

这个注解相当于”标识“的作用,打上这个标记就必须登录才能访问,不打上这个标记表示随便

标记一下,哪些请求需要登录以后才能访问呢:

image-20220713160004849

2. 书写拦截器

拦截器

接下来使用拦截器去拦截带有注解的方法,在拦截到方法之后判断登没登录,登录了可以,没登录拒绝

@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){
                // 不等于 null 表示这个方法是需要登录才能访问的
                // 但是 hostHolder.getUser() == null 表示没有登录
                // 这个时候应该拒绝后续的请求,强制重定向到登录页面 request.getContextPath() 表示取到应用的路径
                response.sendRedirect(request.getContextPath() + "/login");
                return false;
            }
        }
        return true;
    }


}

image-20220713163226270

3. 拦截器的配置文件

配置文件

最后我们配一下不拦截静态资源的请求,其他请求都拦截,从中找到带注解的。

image-20220713162810948

不去处理静态的资源,而其他动态资源都处理,但处理的时候人为的筛选了带有注解的那一部分,其他的不管,这样的好处是希望它处理谁就在谁上加注解,而不是一个一个加路径(加路径也可以,两种方式都行)。


最后建议测试一下:

在没有登陆的状态下,输入 http://localhost:8080/community/user/setting 看它是否会跳转到

http://localhost:8080/community/login 登录页面

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于Spring Boot 2.7.15的模块开发,您可以使用Spring Boot模块化特性来将您的应用程序拆分为多个模块,每个模块具有自己的功能和职责。 在Spring Boot中,您可以使用Maven或Gradle等构建工具来管理您的模块化项目。每个模块都可以是一个单独的子项目,具有自己的依赖关系和配置。 以下是一些模块开发的步骤和示例: 1. 创建一个父项目: 首先,您可以创建一个父项目,用于管理所有的子模块。这个父项目可以是一个简单的Maven或Gradle项目。 2. 创建子模块: 在父项目下,您可以创建多个子模块,每个子模块代表一个特定的功能或模块。您可以使用Maven或Gradle的子模块功能来创建和管理这些子模块。 3. 定义依赖关系: 在每个子模块的配置文件(例如pom.xml或build.gradle)中,您可以定义该模块所需要的依赖关系。这些依赖关系可以是其他子模块、第三方库或其他项目。 4. 模块间通信: 您可以使用Spring Boot提供的各种通信方式来实现模块间的交互。例如,您可以使用@RestController注解创建RESTful API,然后在其他模块中调用这些API。 5. 构建和运行: 使用构建工具(如Maven或Gradle)来构建您的模块化项目。您可以为每个子模块单独构建,也可以一次性构建所有模块。然后,您可以运行您的应用程序并测试各个模块的功能。 这只是关于Spring Boot模块开发的简单介绍。实际上,模块开发涉及到更多细节和最佳实践,您可以根据您的具体需求和项目结构进行调整和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值