本篇博客对应“2.2 开发注册功能”小结
对应视频:
开发注册功能
开发注册功能-续
注册功能是相对比较复制的功能,对于一个相对复杂的功能,可以把这个功能进行拆解。把这个功能的流程想清楚,就知道怎么拆解了:
也可以按照请求进行拆解,注册过程一共发生三次请求,对应服务器产生三次响应:
我们一次请求,一次请求的把它搞定,就可以开发出整个功能了。
每一次请求按照:数据访问层、业务层、视图层。三层架构进行实现。
当然,有一些功能可能只有其中的一层或两层,写代码的时候就知道了。
访问注册页面
点击顶部区域的链接,打开注册页面。
新建一个LoginController
创建处理获取注册页面,返回注册视图的方法
@RequestMapping(path = "/register", method = RequestMethod.GET)public String getRegisterPage() {return "/site/register";}
需要对register模板进行修改:
提交注册数据
准备工作
导入工具包Commons Lang,该工具包可以帮助我们判断字符串、集合是否为空、数据是否符合规范等其他功能。
引入mavaen依赖
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version></dependency>
引入jar包之后,还需要在配置文件中,把网站的域名配置号。
为什么要配置域名呢?
因为注册的过程中要发邮件,邮件里面得带上激活链接,这个激活链接得链接到我们的网站,而这个链接在开发、 测试、上线阶段是不同的,所以我们需要把他做成可配的。
application.properties文件中加入:
community:path:domain: http://localhost:8080
这三个都是自定义的,在程序里面用 @Value(“${属性名}”) 获取即可。
再写一个工具类,在工具类里面提供两个方法,注册的时候方便调用。
util包下新建一个工具类CommunityUtil
提供静态的方法:
生成随机字符串:生成激活码,每次是一个随机的字符串;给上传头像或上传文件的功能,每次上传时需要给图片或文件生成一个随机的名字,防止重复。使用java util包下的UUID类生成随机字符串,这是java自带的功能。
UUID生成的随机字符串会有横线,我们不需要
// 生成随机字符串public static String generateUUID() {return UUID.randomUUID().toString().replaceAll("-", "");}
除此之外,我们还要封装一个方法,叫做MD5加密,采用MD5算法对密码进行加密。
为什么要加密?
因为用户在注册的时候,提交的密码是明文形式的,我们在存储到数据库中,需要将密码加密,这样,即使数据库泄露,别人也不会知道用户的密码是什么。
MD5加密算法的特点:
- 任意长度的信息,经处理后,输出为128位的信息
- 不同的输入得到不同的结果,唯一性
- 不可逆性,由于通过散列函数,hash算法,在计算过程中,原文的信息是部分丢失了的。
MD5加密算法只能加密,不能解密,而且每次加密都是这个值。
例如: hello --> dhasdhqofhasjfdhas
每次hello对应的密文都是dhasdhqofhasjfdhas,而且不可以解密。
如果用户密码设置的过于简单,比如就是hello。盗取密码的黑客也会知道dhasdhqofhasjfdhas,因为他有一个简单密码的库,hello啊生日啊都包含在内,他也会把这个明文加密成密文,所以进行破解。
因此不管用户输入的是什么密码,都加上一个随机的字符串,例如hello + 3edr4, 那么加密之后的密文就是:dasdjoqiwhdoqwhfdsh(假设),由于3edr4是随机的,黑客密码库中没有对应的明文-密文记录,所以是很难进行破解的。提高了用户密码的安全性。
为什么要加盐?
加盐表示的含义是加噪声,因为人们在设置密码时,通常都是在某个长度之内,不会过于复杂,虽然MD5本时时不可逆的,无法通过密文知道原文是什么。但是攻击者可以构造一个对照表,将明文和密文全部列举出来存到一个对照表中,然后采用穷举法,一个一个比较密文是哪一个,如果有密文是相同的,就可以去对照表中查明文,由于前面说的MD5唯一性的特性,这个明文一定是用户输入的密码,这就进行了破解。
加盐操作可以预防这个攻击方式,通过加噪声数据,可以极大的增加密码的随机性和复杂性,如果要使用对照表穷举的方式,需要消耗大量的计算机资源,这在现实中是不可行的。
用spring自带的工具就可以实现MD5加密,org.springframework.util.DigestUtils。加密方法如下:
// MD5加密// hello -> abc123def456// hello + 3e4a8 -> abc123def456abcpublic static String md5(String key) {/** 判断key是否为空,空串、null、空格* */if (StringUtils.isBlank(key)) {return null;}return DigestUtils.md5DigestAsHex(key.getBytes());}
有了这些准备之后,可以开发真正的注册业务了。
通过表单提交数据
在UserService中写逻辑
需要注入的对象:
@Autowiredprivate UserMapper userMapper;@Autowiredprivate MailClient mailClient;@Autowiredprivate TemplateEngine templateEngine;@Value("${community.path.domain}")private String domain;@Value("${server.servlet.context-path}")private String contextPath;
创建register方法:
public Map<String, Object> register(User user) {}
空值处理
// 空值处理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, 5));user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));user.setType(0);user.setStatus(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);
知道了为什么要加盐之后,我们来看为什么需要 user.setSalt(),将这个salt值保存下来?
因为在用户成功注册,下一次登录的时候,用户需要再次输入账号以及明文的密码,但此时,数据库存储的是经MD5加密之后的密文,而且MD5是不可逆的,所以,需要将这个该用户的盐值取出来,然后和用户明文密码拼接,经过MD5算法再一次加密得到一个密文,将该密文与数据库中这个用户对应的密文进行字符串对比,如果相等,说明用户密码正确,予以登录,这是由于MD5加密算法的唯一性可以做出的判断。
总结:保存salt值是为了下一次登录密码比较使用。
为什么需要setActivationCode,设置激活码?
如果没有激活码的话,人为地构造一个请求也可以进行账号激活,就失去了通过邮件激活的意义,因为用户大可以用一个假邮箱注册,但是通过构造url进行注册。所以,只有激活码这种方式,激活码是一个随机字符串,用户不好认为构造,只能写一个真实的邮箱,然后点击邮件中的激活链接,邮件中的激活链接已经带上了用户id 和 激活码。所以直接点击,然后服务器会获取请求,从数据库对用的用户id取出激活码,判断这两个激活码是否一致。
服务端发送激活邮件
// 激活邮件Context context = new Context();context.setVariable("email", user.getEmail());// http://localhost:8080/community/activation/101/codeString url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();context.setVariable("url", url);String content = templateEngine.process("/mail/activation", context);mailClient.sendMail(user.getEmail(), "激活账号", content);
通过上下文对象、模板、模板引擎将动态数据渲染到html页面中,生成视图(html页面),就是字符串。
将字符串设置位邮件的content,然后发送给用户。
controller层 LoginController中添加register方法:
注册失败返回注册页面需要将错误信息现实在注册页面上。
激活注册账号
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;}}