项目进展:
已经完成了springboot + MVC + mybatis 框架搭建,外加常态的错误异常定义、正确的返回值类型定义。
接下来还需完成模型能力管理,其包括用户模型、商品模型、下单模型、秒杀活动模型。即领域模型管理(如 user 对象就是用户级别的一个领域模型,包括完整的生命周期。比如对用户来说,注册为其生命周期的第一步,用户信息的浏览登录为其第二步等)。
用户信息管理,包括用户注册:otp 短信获取、otp 注册用户、用户手机号登录。
otp验证码获取
- 在UserController.java中添加用户获取otp短信接口
- 在UserController.java中接入对应的HTTPSession
@RequestMapping("getOtp")
@ResponseBody
//用户获取otp短信接口 一下定义该接口的出参入参
//遵循CommonReturnType的返回方式,接口名取为getOtp,@RequestParam标明接口的入参(手机号)
public CommonReturnType getOtp(@RequestParam(name = "telphone") String telphone){
//需要按照一定的规则生成otp验证码(采取随机数的生成方式) random:随机的
Random random = new Random();
int randomInt = random.nextInt(99999); //此处随机数取值[0,99999)
randomInt += 10000; //此处随机数取值[10000,99999)
String otpCode = String.valueOf(randomInt); //将整数类型转换为字符串,valueOf(int i):返回int参数的字符串表示形式。
//将otp验证码同对应用户的手机号关联(一般使用Redis处理,此处采用HTTPSession模仿实现,使用httpsession的方式绑定手机号与OTPCDOE)
httpServletRequest.getSession().setAttribute(telphone,otpCode);
//将otp验证码通过短信通道发送给用户(在该项目中该步骤省略)
//按照以上三步以完成对应otp短信到用户的一个触达的过程
//为了方便程序调试,直接打印到控制台上
System.out.println("telphone = " + telphone + " & otpCode = " + otpCode);
return CommonReturnType.create(null);
}
注: 对于用户来说,otpCode为敏感信息,不能直接被打印到控制台上
//接入对应的HTTPSession
//使用spring注入方式注入一个HttpServletRequest对象(即通过bean的方式注入进来)。
//通过spring bean包装的HttpServletRequest对象的本质是proxy(代理人)模式,它的内部拥有ThreadLocal(绑定)方式的map,去让用户在每个线程中处理自己对应的request,并且有ThreadLocal清除的机制。(Spring能实现在多线程环境下,将各个线程的request进行隔离,且准确无误的进行注入,奥秘就是ThreadLocal)
//(spring在注入HttpServletRequest时,发现如果注入的是一个ObjectFactory类型的对象时,就会将注入的bean替换成一个JDK动态代理对象,代理对象在执行HttpServletRequest对象里的方法时,就会通过RequestObjectFactory.getObject()获取一个新的request对象来执行。)
@Autowired
private HttpServletRequest httpServletRequest;
接下来通过接口调用的方式简单的调试一下
出现这个异常说明了跳转页面的url无对应的值,是控制器的URL路径书写出现了问题。实际路径如下所示:
出现该错误的原因是缺少对应的请求参数---telphone
@RequestParam注解使用:将请求参数绑定到控制器的方法参数上。
@RequestParam( value = "参数名", required = "true/false", defaultValue = "")
①value:请求参数名(必须配置)
②required:是否包含该参数,默认为true,表示该请求路径中必须包含该参数,如果不包含将会抛出异常(可选配置)
③defaultValue:默认参数值,如果设置了该值,required 将自动设为 false,无论是否配置了required,配置了什么值,都是 false。(可选配置)如果没有传参数,就使用该默认值。
参考文章:
getotp页面实现
先搭好骨架(需要实现的功能),再实现对应的交互,并且附上css元素(样式)
1、完成 getotp.html 页面样式的编写。
<html>
<head>
<meta charset="UTF-8"> <!-- 非常重要,没有这个,许多中文在页面上显示都是乱码 -->
</head>
<body>
<div>
<h3>获取otp信息</h3>
<div>
<label>手机号</label>
<div>
<!-- 这里为什么又要用name,又要用id?因为我们的代码采取前后端分离的设计模式,使用ajax请求(而不是采用form post的方式) -->
<input type="text" placeholder="手机号" name="telphone" id="telphone"/>
</div>
</div>
<div>
<!-- 提交的button -->
<button id="getotp" type="submit"> <!-- 这个id是我们后面需要绑定otp的click(点击)事件 -->
获取otp短信
</button>
</div>
</div>
</body>
</html>
2、完成 button 提交业务的编写。
先实现界面交互,再实现界面的美化。使用jQuery进行界面交互。首先,引入 jquery自己的一个 js。
<head>
<meta charset="UTF-8"> <!-- 非常重要,没有这个,许多中文在页面上显示都是乱码 -->
<!-- 引入jquery的版本 -->
<script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script>
</head>
然后编写 js 代码,实现 button 的 click 事件,用于向后端发送获取短信验证码的请求。
①绑定 otp 的 click 事件用于向后端发送获取短信验证码的请求
②使用 ajax 方式发送异步请求完成业务。
③为 Controller 中的 @RequestMapper 添加指定的 method,consumes(将其声明到控制层基类中)
在UserController.java文件中指定method,报错:
Incompatible types. Found: 'java.lang.String', required: 'org.springframework.web.bind.annotation.RequestMethod[]'
解决方法:按住Ctrl键,同时点击method,参考对应正确的参数(如下图所示),可以看出RequestMethod是个数组,最终改为method = {RequestMethod.POST}
指定method,意味着对应的getOtp的方法必须映射到HTTP的POST请求中才能生效。
继续在getotp.html中编写jQuery,如下所示:
按住Ctrl键,同时点击@RequestMapping,查看其中有个consumes(指定处理请求的提交内容类型(Content-Type))。
"application/x-www-form-urlencoded"——浏览器默认的编码格式,用于键值对参数,参数之间用&间隔。
参考文章:
Post请求的两种编码格式:application/x-www-form-urlencoded和multipart/form-data - 简书
在BaseController.java中声明一个变量 public static final String CONTENT_TYPE_FORMED="application/x-www-form-urlencoded"; 注:final修饰的常量定义一般都有书写规范,被final修饰的常量名称,所有字母都大写。 final修饰的变量只能在显示初始化或者构造函数初始化的时候赋值一次,以后不允许更改。 参考:https://www.runoob.com/w3cnote/java-extends.html
之后在所有的controller层都可以引用CONTENT_TYPE_FORMED。
getotp.html
<html>
<!-- 用于解决一些动态请求的问题 -->
<script>
//所有 jQuery 函数位于一个 document ready 函数中
//所有的 jQuery 动态绑定元素必须在 jQuery 的一个document ready里面完成
jQuery(document).ready(function () {
//使用jQuery的id标签getotp,绑定otp的click事件,用于向后端发送获取手机验证码的请求
$("#getotp").on("click",function () {
//使用ajax方式发送异步请求完成业务
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"http://localhost:8090/user/getotp", //至此完成了ajax请求头,接下来解决传参;对应UserController.java中的getOtp方法,我们定义的参数就是手机号等于一个具体的String。
//ajax内部的传参
data:{
"telphone":$("#telphone").val(),
},
//定义两个回调,分别是ajax请求成功和ajax请求失败
success:function (data) {
if (data.status == "success"){ //说明我们的业务逻辑请求处理成功了
alert("otp已经发送到了您的手机上,请注意查收");
}else{
alert("otp发送失败,原因为" + data.data.errMsg);
}
},
error:function (data) {
alert("otp发送失败,原因为" + data.responseText);
}
});
});
});
</script>
</html>
④发现在getotp.html中没有做判空处理,要把判空处理加上去,程序设计逻辑要严谨一点。
<script>
jQuery(document).ready(function () {
$("#getotp").on("click",function () {
//注意判空处理
var telphone = $("#telphone").val();
if (telphone == null || telphone == ""){
alert("手机号不能为空");
return false;
}
$.ajax({
});
return false;
});
});
</script>
注意:button 自定义 click 方法后,该方法内应该返回 false,因为使用 ajax 发送请求处理业务,不需要常见的事件冒泡给 form 提交 POST 方法。
ajax方法return false的目的是按照js冒泡事件传递思想,会传递到on click上一层(对应的form post处理流里),此时把on click事件捕获,不传递上一层。
报错:getotp.html:1 Access to XMLHttpRequest at 'http://localhost:8090/user/getotp' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这是个跨域请求的错误。因为是从本地 html 中发送请求到 localhost 的 url,因此会出现跨域安全异常,虽然请求能够到达(服务端)控制层,并成功被控制层方法处理业务并返回,但是 ajax 请求会认为该请求是不安全的,因此走不到 ajax 请求的 success 块中,并会给浏览器报错。
在 springboot 中处理 ajax 跨域请求的方式:只要让 response 时刻能够返回 'Access-Control-Allow-Origin' 这个header为所有的域,即 * 即可。
SpringBoot 提供给我们一个简单注解方式 @CrossOrigin 实现所有 SpringBoot 中所有 web 请求返回对象带上一个 Access-Control-Allow-Origin 标签,即可实现跨域处理。@CrossOrigin 可使用在 controller 上,或方法级别上,也可以同时使用,Spring将合并两个注释属性以创建合并的CORS配置。
该注解可以有2个参数:origins :允许可访问的域列表;maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。
在UserController.java中加上@CrossOrigin注解
解决方法:
先通过查看请求路径,获得调用了后端的相应接口,在IDEA中查看其接口方法名
使用"Ctrl + Shift + R":替换文本,通过这个来查看相关方法;使用"Ctrl + Shift + N":查找文件(enter file name)
然后发现路径设置错误,修改如下:
服务端正确的处理了相应的请求,在控制台成功打印出验证码:
没有在UserController.java中加上@CrossOrigin注解:
在UserController.java中加上@CrossOrigin注解后,请求头中加上了'Access-Control-Allow-Origin':
因为粗心造成的错误,浪费了自己很多时间,不会找错!!!!!
getotp页面美化
修改getotp.html代码
<html>
<head>
<meta charset="UTF-8">
<!-- 引入css样式 -->
<link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/global/css/component.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/>
<script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script>
</head>
<body class="login">
<div class="content">
<h3 class="form-title">获取otp信息</h3>
<div class="form-group">
<label class="control-label">手机号</label>
<div>
<input class="form-control" type="text" placeholder="手机号" name="telphone" id="telphone"/>
</div>
</div>
<div class="form-actions">
<!-- 提交的button -->
<button class="btn btn-info" id="getotp" type="submit">
获取otp短信
</button>
</div>
</div>
</body>
<script>
jQuery(document).ready(function () {
。。。。。。。。。
});
</script>
</html>
class="btn btn-primary"
class="btn btn-success"
class="btn btn-warning"
class="btn btn-danger"
用户注册功能实现
在UserController.java中添加用户注册接口
报错:Cast to 'java.lang.String' 需要将 object 类型强制转换为 java.lang.String 代表的字符串类型。
使用对应StringUtils类库里的equals;按住Ctrl,同时点击equals,查看其对应的实现,其内部已经做了判空处理,便于使用。
com.alibaba.druid.util.StringUtils.equals():druid的equals实现了空判断。
报错:Add exception to method signature。向方法名添加异常。
抛出异常:将光标移到该代码行,同时使用 Alt+Enter。
增加UserService.java中的接口方法
使用"Ctrl + Shift + N":查找文件(enter file name)
public interface UserService {
UserModel getUserById(Integer id);
//在用户注册流程中,需要一个service,去处理对应用户注册的请求
void register(UserModel userModel);
}
在UserServiceImpl.java中实现register方法
抛出异常后
@Override
public void register(UserModel userModel) throws BusinessException {
//必传参数校验
if (userModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
}
public interface UserService {
UserModel getUserById(Integer id);
void register(UserModel userModel) throws BusinessException;
}
引入做输入校验的依赖
pom.xml
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
在UserServiceImpl.java继续添加register方法的相应实现
判断输入参数的合法性,isEmpty用来判断String类型的参数是否为空。
![]()
使用对应StringUtils类库里的isEmpty;按住Ctrl,同时点击isEmpty,查看其对应的实现,其内部已经做了判空处理,便于使用。
Apache Common的StringUtils.isEmpty(str) 同时判断字符串是否为 null 以及字符串长度是否为0。
UserServiceImpl.java
@Override
public void register(UserModel userModel) throws BusinessException {
//必传参数校验
if (userModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
if(org.apache.commons.lang3.StringUtils.isEmpty(userModel.getName())
|| userModel.getGender() == null
|| userModel.getAge() == null
|| StringUtils.isEmpty(userModel.getTelphone())){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR); //入参不合法
}
//把对应的用户注册信息注册进去
//实现model->dataobject(DO)方法(因为dao层使用数据模型Data Object)
//insertSelective相对于insert方法,不会覆盖掉数据库的默认值
UserDO userDO = convertFromModel(userModel);
userDOMapper.insertSelective(userDO);
UserPasswordDO userPasswordDO = convertPasswordFromModel(userModel);
userPasswordDOMapper.insertSelective(userPasswordDO);
return;
}
//model->dataobject(DO)方法
private UserDO convertFromModel(UserModel userModel){
if(userModel == null){
return null;
}
UserDO userDO = new UserDO();
BeanUtils.copyProperties(userModel,userDO);
return userDO;
}
//model->dataobject(DO)方法
public UserPasswordDO convertPasswordFromModel(UserModel userModel){
if (userModel == null){
return null;
}
UserPasswordDO userPasswordDO = new UserPasswordDO();
userPasswordDO.setEncrptPassword(userModel.getEncrptPassword());
userPasswordDO.setUserId(userModel.getId());
return userPasswordDO;
}
数据库设计,尽量使用 not null,给定默认值。因为 java 的程序代码在处理一些空指针的时候非常的脆弱;同时null字段对于前端的展示来说是没有任何意义的,null代表未定义,只在程序级别有效。
数据库字段何时用not null。以手机号为例,如果业务上它是必须的,那么可以设置为not null,并加唯一索引,包括第三方登陆也要强绑定手机号。如果业务上不是必须的,那么建议用null,此时加唯一索引时,null不受唯一索引约束。
表设计的时候,如果有些字段并不是一定要有值,而且还想在这个字段上添加索引加快查询速率,那么就不要把这个字段设置为不能为null,因为null不影响索引的生成。
添加注解@Transactional
在spring或springboot等框架中,关系到数据库的增删改时,也都会使用 @Transactional 注解来表示事务的开启。可以放在类上,表示全部方法开启事务,也可放在某个方法上面,表示这个方法开启事务,一般都放在service层里。
注意:只有来自外部的方法调用才会引起事务行为,类内部方法调用本类内部的其他方法并不会引起事务行为。
在UserController.java中添加用户注册流程
//用户注册接口
@RequestMapping(value = "/register",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType register(@RequestParam(name = "telphone") String telphone,
@RequestParam(name = "otpCode") String otpCode,
@RequestParam(name = "name") String name,
@RequestParam(name = "gender") Byte gender,
@RequestParam(name = "age") Integer age,
@RequestParam(name = "password") String password) throws BusinessException {
//验证手机号和对应的otpCode相符合
String inSessionOtpCode = (String) this.httpServletRequest.getSession().getAttribute(telphone);
if (!com.alibaba.druid.util.StringUtils.equals(otpCode, inSessionOtpCode)) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"短信验证码不符合");
}
//用户的注册流程
UserModel userModel = new UserModel();
userModel.setName(name);
userModel.setGender(new Byte(String.valueOf(gender.intValue())));
userModel.setAge(age);
userModel.setTelphone(telphone);
userModel.setRegisterMode("byphone");
//MD5加密 MD5Encoder.encode(需要加密的密码.getBytes())
userModel.setEncrptPassword(MD5Encoder.encode(password.getBytes()));
userService.register(userModel);
return CommonReturnType.create(null);
}
当用户getotp操作成功之后,界面要跳转到对应的register界面上:
register.html
<html>
<head>
<meta charset="UTF-8">
<link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/global/css/component.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/>
<script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script>
</head>
<body class="login">
<div class="content">
<h3 class="form-title">用户注册</h3>
<div class="form-group">
<label class="control-label">手机号</label>
<div>
<input class="form-control" type="text" placeholder="手机号" name="telphone" id="telphone"/>
</div>
</div>
<div class="form-group">
<label class="control-label">验证码</label>
<div>
<input class="form-control" type="text" placeholder="验证码" name="otpCode" id="otpCode"/>
</div>
</div>
<div class="form-group">
<label class="control-label">用户昵称</label>
<div>
<input class="form-control" type="text" placeholder="用户昵称" name="name" id="name"/>
</div>
</div>
<div class="form-group">
<label class="control-label">性别</label>
<div>
<input class="form-control" type="text" placeholder="性别" name="gender" id="gender"/>
</div>
</div>
<div class="form-group">
<label class="control-label">年龄</label>
<div>
<input class="form-control" type="text" placeholder="年龄" name="age" id="age"/>
</div>
</div>
<div class="form-group">
<label class="control-label">密码</label>
<div>
<input class="form-control" type="password" placeholder="密码" name="password" id="password"/>
</div>
</div>
<div class="form-actions">
<!-- 提交的button -->
<button class="btn btn-info" id="register" type="submit"> <!-- 这个id是我们后面需要绑定otp的click(点击)事件 -->
提交注册
</button>
</div>
</div>
</body>
<!-- 用于解决一些动态请求的问题 -->
<script>
jQuery(document).ready(function () {
//使用jQuery的id标签register,绑定register的click事件
$("#register").on("click",function () {
var telphone = $("#telphone").val();
var otpCode = $("#otpCode").val();
var name = $("#name").val();
var gender = $("#gender").val();
var age = $("#age").val();
var password = $("#password").val();
if (telphone == null || telphone == ""){
alert("手机号不能为空");
return false;
}
if (otpCode == null || otpCode == ""){
alert("验证码不能为空");
return false;
}
if (name == null || name == ""){
alert("用户昵称不能为空");
return false;
}
if (gender == null || gender == ""){
alert("性别不能为空");
return false;
}
if (age == null || age == ""){
alert("年龄不能为空");
return false;
}
if (password == null || password == ""){
alert("密码不能为空");
return false;
}
//使用ajax方式发送异步请求完成业务,映射到后端@RequestMapping(value = "/register",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED})
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"http://localhost:8090/user/register", //至此完成了ajax请求头,接下来解决传参
//ajax内部的传参
data:{
"telphone":telphone,
"otpCode":otpCode,
"name":name,
"gender":gender,
"age":age,
"password":password,
},
//定义两个回调,分别是ajax请求成功和ajax请求失败
success:function (data) {
if (data.status == "success"){ //说明我们的业务逻辑请求处理成功了
alert("注册成功");
}else{
alert("注册失败,原因为" + data.data.errMsg);
}
},
error:function (data) {
alert("注册失败,原因为" + data.responseText);
}
});
return false;
});
});
</script>
</html>
出现问题:跨越请求session不能共享
@CrossOrigin注解解决跨域:
DEFAULT_ALLOW_CREDENTIALS = true,需配合前端设置 xhrFields授信后使得跨域session共享。
@CrossOrigin(origins = "http://localhost:63342",allowCredentials = "true",allowedHeaders = "*")
前端ajax请求设置:xhrFields:{withCredentials:true}
getotp.html
register.html
使用Debug调试一下,点击下图标红的地方,前端写的东西都能传过来,跨域问题解决了,就是注册不成功
发现视频中有些代码,我没有跟着敲出来:
修改UserController.java
//用户注册接口 @RequestMapping(value = "/register",method = {RequestMethod.GET},consumes = {CONTENT_TYPE_FORMED}) @ResponseBody public CommonReturnType register(@RequestParam(name = "telphone") String telphone, @RequestParam(name = "otpCode") String otpCode, @RequestParam(name = "name") String name, @RequestParam(name = "gender") Byte gender, @RequestParam(name = "age") Integer age, @RequestParam(name = "password") String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException { //验证手机号和对应的otpCode相符合 String inSessionOtpCode = (String) this.httpServletRequest.getSession().getAttribute(telphone); if (!com.alibaba.druid.util.StringUtils.equals(otpCode, inSessionOtpCode)) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"短信验证码不符合"); } //用户的注册流程 UserModel userModel = new UserModel(); userModel.setName(name); userModel.setGender(Byte.valueOf(gender)); //将字符串转换为字节型 userModel.setAge(Integer.valueOf(age)); //将字符串转换为整数类型 userModel.setTelphone(telphone); userModel.setRegisterMode("byphone"); //MD5加密 MD5Encoder.encode(需要加密的密码.getBytes()) userModel.setEncrptPassword(this.EncodeByMd5(password)); userService.register(userModel); return CommonReturnType.create(null); } //密码加密 public String EncodeByMd5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException { //确定计算方法 MessageDigest md5 = MessageDigest.getInstance("MD5"); BASE64Encoder bese64en = new BASE64Encoder(); //加密字符串 String newstr = bese64en.encode(md5.digest(str.getBytes("utf-8"))); return newstr; }
注意:在 UserController.java 和 register.html 中,我都将请求方式改为 GET。
- GET 方式无请求体,使用 @RequestBody 接收数据时,前端不能使用 GET 方式提交数据,而是用 POST 方式进行提交。同时,使用 @RequestParam 接受数据时,前端应使用 GET 方式。
- 在后端的同一个接收方法里,@RequestBody 与@RequestParam可以同时使用,@RequestBody最多只能有一个,而@RequestParam可以有多个。
如果没有写 userModel.setId(userDO.getId()); 运行也会报错,因为这样 getId 不能复制给userModel,就没有办法转化给 userPasswordDO ,userPasswordDO.setUserId(userModel.getId()); 这行代码就不起作用,最后userId的值就无法成功插入到userPasswordDO中去。与此同时,数据库表的设计都是 not null ,这样程序就会报错。
最后运行结果:
还是有报错,在application.properties中添加相关配置:
#在控制台打印sql mybatis.configuration.log-impl= org.apache.ibatis.logging.stdout.StdOutImpl
查看控制台打印的sql语句,发现根本没有插入user_Id。
原因是:我们在数据库中添加了 user_info 和 user_password 两个表,我们前端输入的数据不包括 user_info 表中的 id 字段,两个表格中的 id 字段都是设置的自动递增的,字段 user_id 对应 user_info 表中的 id 字段。但是在 UserDOMapper.xml 的 insertSelective 方法中,并没有指定数据库表的主键,所以 id 为null,没有自增。
最终解决方法: 指定自增主键 id
![]()
我们没有对手机号做唯一性的限制,可以对同一个号码进行第二次注册。
手机号做限制,不允许二次注册;对数据库中的 telphone 字段设置索引,解决重复注册问题。
修改UserServiceImpl.java
//把对应的用户注册信息注册进去
//实现model->dataobject(DO)方法(因为dao层使用数据模型Data Object)
UserDO userDO = convertFromModel(userModel);
//insertSelective相对于insert方法,不会覆盖掉数据库的默认值
try{
userDOMapper.insertSelective(userDO);
}catch (DuplicateKeyException ex){ //DuplicateKeyException为重复异常
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"手机号已重复注册");
}
相当于
用户登录功能实现
在UserController.java中添加用户登录接口
//用户登录接口
@RequestMapping(value = "/login",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType login(@RequestParam(name = "telphone") String telphone,
@RequestParam(name = "password") String password) throws BusinessException {
//入参校验
if (org.apache.commons.lang3.StringUtils.isEmpty(telphone)
|| StringUtils.isEmpty(password)){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
//用户登录服务,用来校验用户登录是否合法
}
在UserService.java中增加验证登录的接口方法
//登录验证方法,入参为telphone和password
void validateLogin(String telphone,String password);
在UserServiceImpl.java中实现validateLogin方法
@Override public void validateLogin(String telphone, String password) { //通过用户的手机获取用户信息 //比对用户信息内加密的密码是否和传输进来的密码相匹配 }
在UserDOMapper.xml中添加根据手机号查询数据的sql方法
<select id="selectByTelphone" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from user_info where telphone = #{telphone,jdbcType=VARCHAR} </select>
在UserDOMapper.java中添加根据手机号查询数据的sql方法的映射
UserDO selectByTelphone(String telphone);
在EmBusinessError.java中添加错误码
//通用错误类型10001 PARAMETER_VALIDATION_ERROR(10001,"参数不合法"), UNKNOWN_ERROR(10002,"未知错误"), //20000开头为用户信息相关错误定义 USER_NOT_EXIST(20001,"用户不存在"), USER_LOGIN_FAIL(20002,"用户手机号或密码不正确") //对应这个注册用户的手机号不存在,这个信息不能提示的太明显,如果明确显示出来,可能会被人异常攻击! ;
继承了方法“validateLogin”。 是否要在整个方法层次结构中为方法签名添加异常?
UserServiceImpl.java @Override public void validateLogin(String telphone, String password) throws BusinessException { //通过用户的手机获取用户信息 UserDO userDO = userDOMapper.selectByTelphone(telphone); //这个手机号的用户不存在 if (userDO == null){ throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL); } UserPasswordDO userPasswordDO = userPasswordDOMapper.selectByUserId(userDO.getId()); UserModel userModel = convertFromDataObject(userDO,userPasswordDO); //比对用户信息内加密的密码是否和传输进来的密码相匹配 }
修改UserService.java
//登录验证方法,入参为telphone和password /* 修改一下 * telphone:用户注册手机 * password:用户加密后的密码 * */ void validateLogin(String telphone,String encrptPassword) throws BusinessException;
修改UserServiceImpl.java
@Override public void validateLogin(String telphone, String encrptPassword) throws BusinessException { //通过用户的手机获取用户信息 UserDO userDO = userDOMapper.selectByTelphone(telphone); //这个手机号的用户不存在 if (userDO == null) { throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL); } UserPasswordDO userPasswordDO = userPasswordDOMapper.selectByUserId(userDO.getId()); UserModel userModel = convertFromDataObject(userDO, userPasswordDO); //比对用户信息内加密的密码是否和传输进来的密码相匹配 if (!StringUtils.equals(encrptPassword, userModel.getEncrptPassword())) { throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL); } }
在UserController.java中添加
//用户登录服务,用来校验用户登录是否合法 //对密码进行MD5加密 userService.validateLogin(telphone,this.EncodeByMd5(password)); //将登录凭证加入到用户登录成功的session内。在这里假设用户登录是基于Session的单点登录。 this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true); //如果用户的会话标识里面有对应的 IS_LOGIN 标识,那用户已经登录成功了 this.httpServletRequest.getSession().setAttribute("LOGIN_USER",)
修改UserService.java,在validateLogin方法中返回UserModel
UserModel validateLogin(String telphone,String encrptPassword) throws BusinessException;
修改UserServiceImpl.java
@Override public UserModel validateLogin(String telphone, String encrptPassword) throws BusinessException { //通过用户的手机获取用户信息 //比对用户信息内加密的密码是否和传输进来的密码相匹配 if (!StringUtils.equals(encrptPassword, userModel.getEncrptPassword())) { throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL); } //如果用户登录成功,就将 userModel 返回给controller层(外围调用validateLogin服务的层次) return userModel; }
//用户登录接口 @RequestMapping(value = "/login",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED}) @ResponseBody public CommonReturnType login(@RequestParam(name = "telphone") String telphone, @RequestParam(name = "password") String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException { //入参校验 if (org.apache.commons.lang3.StringUtils.isEmpty(telphone) || StringUtils.isEmpty(password)){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR); } //用户登录服务,用来校验用户登录是否合法 //对密码进行MD5加密 UserModel userModel = userService.validateLogin(telphone,this.EncodeByMd5(password)); //将登录凭证加入到用户登录成功的session内。在这里假设用户登录是基于Session的单点登录。 this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true); //如果用户的会话标识里面有对应的 IS_LOGIN 标识,那用户已经登录成功了 this.httpServletRequest.getSession().setAttribute("LOGIN_USER",userModel); //返回给前端一个正确信息 return CommonReturnType.create(null); }
一个网络服务器可以发送一个隐藏的HTML表单域和一个唯一的session ID,如:<input type="hidden" name="sessionid" value="12345">。这个条目意味着,当表单被提交时,指定的名称和值将会自动包含在GET或POST数据中。每当浏览器发送一个请求,session_id的值就可以用来保存不同浏览器的轨迹。
用户在登录的时候需要输入用户名和密码等服务器知道的信息来确认用户身份进行身份验证,常见的身份验证方式有两种:
- 基于session的验证
- 基于token的验证
编写login.html
<html>
<head>
<meta charset="UTF-8"> <!-- 非常重要,没有这个,许多中文在页面上显示都是乱码 -->
<!-- 引入css样式 -->
<link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/global/css/component.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/>
<!-- 引入jquery的版本 -->
<script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script>
</head>
<body class="login">
<div class="content">
<h3 class="form-title">用户登录</h3>
<div class="form-group">
<label class="control-label">手机号</label>
<div>
<input class="form-control" type="text" placeholder="手机号" name="telphone" id="telphone"/>
</div>
</div>
<div class="form-group">
<label class="control-label">密码</label>
<div>
<input class="form-control" type="password" placeholder="密码" name="password" id="password"/>
</div>
</div>
<div class="form-actions">
<!-- 登录的button -->
<button class="btn btn-info" id="login" type="submit"> <!-- 这个id是我们后面需要绑定login的click(点击)事件 -->
登录
</button>
<!-- 注册的button -->
<button class="btn btn-success" id="register" type="submit"> <!-- 这个id是我们后面需要绑定registe的click(点击)事件 -->
注册
</button>
</div>
</div>
</body>
<!-- 用于解决一些动态请求的问题 -->
<script>
//所有 jQuery 函数位于一个 document ready 函数中
//所有的 jQuery 动态绑定元素必须在 jQuery 的一个document ready里面完成
jQuery(document).ready(function () {
$("#register").on("click",function () {
window.location.href="getotp.html"; //采用相对路径,跳转界面
});
//使用jQuery的id标签login,绑定login的click事件
$("#login").on("click",function () {
var telphone = $("#telphone").val();
var password = $("#password").val();
if (telphone == null || telphone == ""){
alert("手机号不能为空");
return false;
}
if (password == null || password == ""){
alert("密码不能为空");
return false;
}
//使用ajax方式发送异步请求完成业务,映射到后端@RequestMapping(value = "/register",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED})
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"http://localhost:8090/user/login", //至此完成了ajax请求头,接下来解决传参
//ajax内部的传参
data:{
"telphone":telphone,
"password":password,
},
//允许跨域授信请求,以使其session变成跨域可授信
xhrFields:{withCredentials:true},
//定义两个回调,分别是ajax请求成功和ajax请求失败
//只要被服务端正确的处理,必定会进success block
success:function (data) {
if (data.status == "success"){ //说明我们的业务逻辑请求处理成功了
alert("登录成功");
}else{
alert("登录失败,原因为" + data.data.errMsg);
}
},
//当http请求因为网络原因或者异常原因没有被正确处理,网络的请求会进入error的block
error:function (data) {
alert("登录失败,原因为" + data.responseText);
}
});
return false;
});
});
</script>
</html>
优化校验规则
在maven仓库中查询是否有可用的类库
pom.xml
<!-- 校验 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.3.6.Final</version>
</dependency>
对 validator 做一个简单的封装
在工程目录里面添加一个validator的目录,新建一个 ValidationResult 的类。
package org.example.validator;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
public class ValidationResult {
//校验结果是否有错
private boolean hasErrors = false;
//存放错误信息的map
private Map<String,String> errorMsgMap = new HashMap<>();
public boolean isHasErrors() {
return hasErrors;
}
public void setHasErrors(boolean hasErrors) {
this.hasErrors = hasErrors;
}
public Map<String, String> getErrorMsgMap() {
return errorMsgMap;
}
public void setErrorMsgMap(Map<String, String> errorMsgMap) {
this.errorMsgMap = errorMsgMap;
}
//实现通用的通过格式化字符串信息获取错误结果的msg方法
public String getErrMsg(){
return StringUtils.join(errorMsgMap.values().toArray(),",");
}
}
新建一个 ValidatorImpl 的类
package org.example.validator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
//(把普通pojo实例化到spring容器中,相当于配置文件中的 <bean id="" class=""/>),泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类。
@Component //spring的bean,学习bean的注入!!!!!
public class ValidatorImpl implements InitializingBean {
//Validator(javax.validation),是通过 javax 定义的一套接口实现的一个 validator 的工具。
private Validator validator;
//实现校验方法并返回校验结果
public ValidationResult validate(Object bean) {
ValidationResult result = new ValidationResult();
}
@Override
public void afterPropertiesSet() throws Exception {
//将hibernate validator通过工厂的初始化方式使其实例化
this.validator = Validation.buildDefaultValidatorFactory().getValidator();
}
}
给 hasErrors 和 errorMsgMap 赋初始值,避免程序内部出现空指针的错误。
package org.example.validator; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import java.util.Set; //(把普通pojo实例化到spring容器中,相当于配置文件中的 <bean id="" class=""/>),泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类。 @Component //spring的bean,学习bean的注入!!!!! public class ValidatorImpl implements InitializingBean { //Validator(javax.validation),是通过 javax 定义的一套接口实现的一个 validator 的工具。 private Validator validator; //实现校验方法并返回校验结果 public ValidationResult validate(Object bean) { final ValidationResult result = new ValidationResult(); Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean); if (constraintViolationSet.size() > 0) { //有错误 result.setHasErrors(true); //遍历constraintViolationSet constraintViolationSet.forEach(constraintViolation ->{ String errMsg = constraintViolation.getMessage(); String propertyName = constraintViolation.getPropertyPath().toString(); result.getErrorMsgMap().put(propertyName, errMsg); }); } return result; } @Override public void afterPropertiesSet() throws Exception { //将hibernate validator通过工厂的初始化方式使其实例化 this.validator = Validation.buildDefaultValidatorFactory().getValidator(); } }
使用 validator
在 UserModel.java 中添加注释 @NotBlank、@NotNull、@Min、@Max。
private Integer id;
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(message = "性别不能不填写")
private Byte gender;
@NotNull(message = "年龄不能不填写")
@Min(value = 0, message = "年龄必须大于0岁")
@Max(value = 150, message = "年龄必须小于150岁")
private Integer age;
@NotBlank(message = "手机号不能为空")
private String telphone;
private String registerMode;
private String thirdPartyId;
@NotBlank(message = "密码不能为空")
private String encrptPassword;
修改UserServiceImpl.java
//引入validator的bean
@Autowired
private ValidatorImpl validator;
@Override
@Transactional//声明事务
public void register(UserModel userModel) throws BusinessException {
if (userModel == null) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
/*if (org.apache.commons.lang3.StringUtils.isEmpty(userModel.getName())
|| userModel.getGender() == null
|| userModel.getAge() == null
|| StringUtils.isEmpty(userModel.getTelphone())) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR); //入参不合法
}*/
ValidationResult result = validator.validate(userModel);
if (result.isHasErrors()){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,result.getErrMsg());
}
UserDO userDO = convertFromModel(userModel);
try {
userDOMapper.insertSelective(userDO);
} catch (DuplicateKeyException ex) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "手机号已重复注册");
}
userModel.setId(userDO.getId());
UserPasswordDO userPasswordDO = convertPasswordFromModel(userModel);
userPasswordDOMapper.insertSelective(userPasswordDO);
}
解决方法:
![]()
修改成如下:
![]()
![]()
![]()
![]()