part1
part2
part3
part4
part5 本页
6 移动端短信发送和手机验证码登入
6.1 短信发送
6.2 手机验证码登入
6.1 短信发送
6.1.1整体分析
2.
3. 注册登入阿里云账户。找到短信服务,设置短信签名(上面图片的阿里云、菜鸟裹裹、天猫…),模板等等
4. 设置AccessKey
5. 看帮助文档,导入对应的Util包
6.2 手机验证码登入
6.2.1 整体思路整理
- 需求分析
- 涉及表的操作(数据模型)
- 代码开发思路
想要判断当前手机号是否在表中,如果不在,说明是新用户,将改号码保存 - 使用浏览器的手机模式查看H5的页面
- 拦截器改造,现在加入移动端的登入(session中是否有user,user是成功登入后设置的),设置threadLocal等信息。
- 后端思路:短信发送的controller处理完之后,把发送的验证码存到session中。之后是登入的controller,把前端提交的短信验证码和session中的验证码进行比较,如果手机号和验证码都没错,那就放行(设置一个User的session,拦截器就会放行),同时查数据库,看看有没有对应的手机号,没有的话就插入这条手机号的数据
6.2.2 前端代码分析
- login.html显示到页面上的代码如下:
<div id="login" v-loading="loading">
<div class="divHead">登录</div>
<div class="divContainer">
<el-input placeholder=" 请输入手机号码" v-model="form.phone" maxlength='20'/></el-input>
<div class="divSplit"></div>
<el-input placeholder=" 请输入验证码" v-model="form.code" maxlength='20'/></el-input>
<span @click='getCode'>获取验证码</span>
</div>
<div class="divMsg" v-if="msgFlag">手机号输入不正确,请重新输入</div>
<el-button type="primary" :class="{btnSubmit:1===1,btnNoPhone:!form.phone,btnPhone:form.phone}" @click="btnLogin">登录</el-button>
</div>
- 获取验证码,注意发送请求的参数是sendMsgApi({phone:this.form.phone}),json的格式,后端使用注解@RequestParam接收,可以直接接收到一个对象中,属性含有phone字段
getCode(){
this.form.code = ''
//正则表达式验证手机号是否正确
const regex = /^(13[0-9]{9})|(15[0-9]{9})|(17[0-9]{9})|(18[0-9]{9})|(19[0-9]{9})$/;
if (regex.test(this.form.phone)) {
this.msgFlag = false
//sendMsgApi({phone:this.form.phone}) 一个json,key是phone,value是前端输入的手机号
this.form.code = (Math.random()*1000000).toFixed(0)
}else{
this.msgFlag = true
}
},
这里的msgFlag作用是对应之前的:<div class="divMsg" v-if="msgFlag">手机号输入不正确,请重新输入</div>
,即已经通过了验证,不需要弹出提示信息
正常情况下,通过sendMsgApi发送axios请求,请求到后端发送验证码
function sendMsgApi(data) {
return $axios({
'url': '/user/sendMsg',
'method': 'post',
data
})
}
- 登入模块,前端只需要给后端传loginApi(this.form),这个form:{phone:‘’“,code:‘’” } 是一个json,需要把phone和前端的code都传给后端,后端之前设置了session:session.setAttribute(phone,code); 进行一个验证就好了。
async btnLogin(){
if(this.form.phone && this.form.code){
this.loading = true
const res = await loginApi(this.form)
this.loading = false
if(res.code === 1){
sessionStorage.setItem("userPhone",this.form.phone)
window.requestAnimationFrame(()=>{
window.location.href= '/front/index.html'
})
}else{
this.$notify({ type:'warning', message:res.msg});
}
}else{
this.$notify({ type:'warning', message:'请输入手机号码'});
}
}
6.2.3 后端代码分析
整体思路:短信发送的controller处理完之后,把发送的验证码存到session中。之后是登入的controller,把前端提交的短信验证码和session中的验证码进行比较,如果手机号和验证码都没错,那就放行(设置一个User的session,拦截器就会放行),同时查数据库,看看有没有对应的手机号,没有的话就插入这条手机号的数据
- 发送短信controller: 前端发送请求的参数是sendMsgApi({phone:this.form.phone}),json的格式,后端使用注解@RequestParam接收,可以直接接收到一个对象中,属性含有phone字段
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
//获取手机号
String phone = user.getPhone();
if(StringUtils.isNotEmpty(phone)){
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code={}",code);
//调用阿里云提供的短信服务API完成发送短信
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);
//需要将生成的验证码保存到Session
session.setAttribute(phone,code);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败");
}
- 注意这里不能再使用User接收,主要是User中没有code验证码属性,解决方法一是使用Dto继承User,方法二是使用map接收。这里用方法二,因为code验证码只需要和session做验证就好了,没有存到数据库里的需求。
/**
* 登入验证
* @param map 前端传过来电话和验证码,User没有验证码的属性,接不住,所以用map,也可以用Dto
* @param session 验证成功了,就把user存入session,拦截器那边放行
* @return
*/
@PostMapping("/login")
public RetObj loginController(@RequestBody Map<String,String> map, HttpSession session){
String phone = map.get("phone");
String code = map.get("code");
String sessionCode = session.getAttribute(phone).toString();
if (StringUtils.isNotBlank(sessionCode) && code.equals(sessionCode)){
//用户名秘密验证成功!
//先查电话号码在不在数据库里,如果不在,就要保存
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(lambdaQueryWrapper);
if (user == null){
//存入数据库
User user1 = new User();
user1.setPhone(phone);
user1.setStatus(1);
userService.save(user1);
//session.setAttribute("user",user1.getId());
}else {
session.setAttribute(Contants.SESSION_USERID,user.getId());
}
return RetObj.success("登入成功!");
}else {
return RetObj.error("登入失败!");
}
- 写完之后配置拦截器,有两个类要配置
package cn.edu.uestc.ruijitakeout.common.interceptor;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
log.info("当前路径:{}", uri);
/**
* HandlerMethod=>Controller中标注@RequestMapping的方法
* 需要配置静态资源不拦截时,添加这块逻辑 => 前后端分离项目
*
*/
// 是我们的controller中的方法就拦截,如果不是的话,放行,给加载静态资源
if (!(handler instanceof HandlerMethod)) {
log.info("是静态资源或非controller中的方法,放行");
return true;
}
//1-通过session判断是否登入
if (request.getSession().getAttribute(Contants.SESSIONLOGIN) != null) {
log.info("用户已经登入,id={}", request.getSession().getAttribute(Contants.SESSIONLOGIN));
//进入拦截器后,给threadLocal绑定session,让后面需要的公共字段自动填充的时候,填充这个updateUser
//每次http请求,会分配一个新的线程来处理
BaseContext.setThreadLocal((Long) request.getSession().getAttribute(Contants.SESSIONLOGIN));
log.info("拦截器这里设置了ThreadLocal,值为:{}",(Long) request.getSession().getAttribute(Contants.SESSIONLOGIN));
log.info("当前线程id={}",Thread.currentThread().getId());
return true;
} else if (request.getSession().getAttribute(Contants.SESSION_USERID) != null) {
log.info("用户已经登入,id={}", request.getSession().getAttribute(Contants.SESSION_USERID));
//进入拦截器后,给threadLocal绑定session,让后面需要的公共字段自动填充的时候,填充这个updateUser
//每次http请求,会分配一个新的线程来处理
BaseContext.setThreadLocal((Long) request.getSession().getAttribute(Contants.SESSION_USERID));
log.info("拦截器这里设置了ThreadLocal,值为:{}",(Long) request.getSession().getAttribute(Contants.SESSION_USERID));
log.info("当前线程id={}",Thread.currentThread().getId());
return true;
}
//这里应该跳转到登入页面,如何做?
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
log.info("用户未登入,通过输出流方式向客户端页面响应数据,打回登入页面");
response.getWriter().write(JSON.toJSONString(RetObj.error("NOTLOGIN")));//与前端request.js中的代码呼应
return false;
}
package cn.edu.uestc.ruijitakeout.common.config;
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 拓展消息转换器
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("拓展消息转换器成功加载");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//重写方法,添加拦截器方法
registry.addInterceptor(loginInterceptor())
//拦截哪些路径
.addPathPatterns("/**")
//不拦截路径
.excludePathPatterns("/employee/backend/page/login/login.do",
//"/backend/**",
"/employee/backend/page/login/logout.do",
//"/front/**",
"/error",
"/user/**"
);
}
@Bean
public LoginInterceptor loginInterceptor(){
return new LoginInterceptor();
}
}