2.2 开发注册功能

161 阅读10分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

开发注册功能

image-20220707172107843

这种相对复杂的功能我们要学会拆解,使得每一部分都相对简单一点就好开发了。对于一个 web 功能我们可以按照请求去拆解,因为每一个功能都是由若干个浏览器和服务器之间的请求交互所构成的。

分析得知,开发注册功能由三个请求构成

  1. 访问注册页面
  2. 提交注册页面,服务器发送激活邮件
  3. 点击邮件中的链接激活服务

我们一次请求一次请求去写,相对来说会比较容易。

每次请求按照 数据访问层 --> 业务层 --> 视图层的顺序开发

但是注意有些功能可能会没有数据访问层,有些功能没有业务层。

1. 访问注册页面

首先写controller

@Controller
public class LoginController {

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

}

然后修改 themeleaf 资源 register.html

首先将第二行修改为下面这句使其声明为 themeleaf 模板

<html lang="en" xmlns:th="http://www.thymeleaf.org">

然后是修改开头的css文件和末尾的js文件或者图片的路径,绝对路径不用管,主要是相对路径要进行一个处理,将其交给 themeleaf 去管理。(注意:themeleaf 模板资源都是以 templates 目录为根路径的)

image-20220707175028738

image-20220707175125760

然后我们访问注册页面是通过点击首页的 “注册” 超链接点过来的,接下来我们来将首页 index.html 的 “注册” 超链接写一下。

image-20220707183248778

每一个页面头部的header区域都是一样的,如果每个页面都复制处理一下会非常的麻烦, 所以最好的方式就是让header这段代码在多个页面复用。header的复用 themeleaf 也是支持的。

# 什么是header
header标签定义文档的页眉。语法格式为“<header>内容</header>”。header元素是一种具有引导和导航作用的结
构元素,通常用来放置整个页面或页面内的一个内容区块的标题等。

# themeleaf如何复用header
模板中,经常希望从其他模板中包含⼀些部分,如⻚眉,⻚脚,公共菜单等部分,为了做到这⼀点,Thymeleaf 可以使
⽤th:fragment 属性来定义被包含的模版⽚段,以供其他模版包含,其他页面可以通过 th:replace 来使用。

给 index.html 的header起个名字

image-20220707182013176

register.html 复用 index.html 的header

image-20220707182456243

接下来启动项目看一下是否开发成功

经测试,访问注册页面成功!

image-20220707183637858

image-20220707183656937

2. 提交注册数据


前置操作

在开发之初再导入一个包,这个包主要提供一些判断字符串、集合等常用的一些数据空值的情况。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>

然后我们需要在 application.properties 里将网站的域名配置好,当然现在没有域名,配的是本机的IP地址,因为在注册的过程中要发邮件,邮件里面得带激活链接,这个链接得链到我们的网站,所以我们需要有一个链接。因为这个链接在开发阶段在测试阶段在上线的时候它是不一样的,所以我们需要把它做成可配的。

# community  下面的key即“community”也是我们自己取的,声明本机域名,当然现在我们也没有域名,我们写本机的ip地址
community.path.domain=http://localhost:8080

image-20220707185846248

然后我们还需要写一个工具类,在工具类里面提供两个方法,后面注册的时候好用,不然注册的时候现写有点麻烦。这个工具类里面的是静态方法,这个工具类就不交给容器管理了。


public class CommunityUtil {

    // 生成随机字符串(激活码、上传文件的随机名字)
    public static String generateUUID(){
        return UUID.randomUUID().toString().replaceAll("-", "");
        // 后面的 replace 方法是去掉随机字符串中的 “-”
    }

    // MD5加密(存的时候对密码进行加密,防止泄露)
    // MD5加密特点:1. 只能加密,不能解密   2. 每次加密结果都一样
    // 为了避免由于用户设的密码过于简单导致很容易被破解,
    // 我们在处理密码的时候会在密码后面加上一个随机的字符串然后用MD5加密
    public static String md5(String key){
        if(StringUtils.isBlank(key)){
            // 空串(nulll 或者 空格 或者 空串)直接返回 null
            return null;
        }
        // 不为空串则进行加密(方法参数是byte)
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }



}

image-20220707204051306

image-20220707230830559


接下来就正式开发提交注册数据

因为注册是针对用户的,所以我们可以把逻辑写到我们之前创建的 UserService 之中。

# 分析

注册时要发邮件,所以我们要把 邮件客户端(MailClient) 、模板引擎注入进来。

发邮件的时候要生成激活码,激活码当中要包含域名、项目名,所以我们也要把配置文件中的域名和项目名
都注入进来。

开发 service


@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    @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;
    }
}

image-20220708204030151

发送给用户的 themeleaf 模板 activation.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
    <title>牛客网-激活账号</title>
</head>
<body>
   <div>
      <p>
         <b th:text="${email}">xxx@xxx.com</b>, 您好!
      </p>
      <p>
         您正在注册牛客网, 这是一封激活邮件, 请点击 
         <a th:href="${url}">此链接</a>,
         激活您的牛客账号!
      </p>
   </div>
</body>
</html>

image-20220708204200084

接下来开发 controller,controller的话在之前的 LoginController 中写可以


@Controller
public class LoginController {

    @Autowired
    private UserService userService;

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

operate-result.html 操作成功之后的页面

注意:在 themeleaf 模板中

路径用 @

controller 传过来的参数接收用 $

image-20220708103213184

还需要对 register.html 模板做一个处理

因为 register.html 之前路径、header 已经做过了一次处理,所以这次主要是对内容做一个处理,内容部分其实主要就是对表单的处理。

表单提交到哪去

什么样的提交方式

image-20220708164902529

image-20220708104738299

另外,如果注册失败会返回一些错误信息,在 register.html 页面显示,所以我们还要对错误信息进行一些处理。

注意:这里确认密码和密码不一致是在前端处理的,所以我们后台无需处理。

image-20220708165024843

注:上面表单的提交方式和调教路径写错位置了,应该写在

标签中,而不是 < div> 标签中

image-20220708145041965

image-20220708145300314

注:上面的 th:text="${emailMsg}" 少加了右边的双引号

然后这里我们可以测试一下

image-20220708182450461

image-20220708182405873

image-20220708182429574

3. 激活注册账号

最后我们来开发 “激活注册账号” 的功能

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

因为之前已经写过了 dao 中的方法,所以我们接下来应该写的是 业务层(service)

激活有三种结果:

成功

重复激活

激活失败(激活码错误)

我们把这三种结果封装到常量接口里,好能够复用,得到状态。

public interface CommunityConstant {

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

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

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

}

在UserService里面去处理激活逻辑的时候,需要用到上面的接口中的常量。

所以我们让UserService去实现上面的接口CommunityConstant

然后我们在UserService写激活的方法,这个返回激活的状态,成功还是失败,int

在激活的时候,服务器会传给后台 用户id 和 激活码,所以激活方法要求参数为 userId 和 激活码

注:为了UserService的完整性,所以也把与激活账号无关的代码粘了进来,与激活账号有关的部分没就两点:1. 实现 CommunityConstant 接口 2. activation方法


@Service
public class UserService implements CommunityConstant {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    @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;
        }

    }
}

image-20220708190818814

image-20220708191059097

然后我们需要在controller里面把请求处理一下

收到的邮件就是html,那个页面我们之前已经做好了。

然后我们在 LoginController 中 再加一个方法处理请求。

我们还需要处理的是登录页面

还需要给 login.html 声明为 themeleaf 模板,然后处理一下 login.html 样式文件的路径就可以了

image-20220708195352135

image-20220708195437030

最后还要修改一下 首页index.html 的登录按钮,使其可以使用

image-20220708200028839

最后我们再处理一下登录页面 login.html 的验证码

image-20220708201428163