流程图如下:
1、实现UserController下的sendCode方法:
/**
* 发送手机验证码
*/
@PostMapping("/code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
2、在userService下添加sendCode方法,根据流程图实现每一步的功能,校验手机号以及生成验证码的功能都是直接封装好的,在这里仅仅是利用后端模拟了发送验证码的功能:
public Result sendCode(String phone, HttpSession session) {
//1、校验手机号
if(isPhoneInvalid(phone)){
//2、手机号无效,返回错误信息
return Result.fail("手机号无效!");
}
//3、手机号有效,生成验证码
String code = RandomUtil.randomNumbers(6);
//4、将验证码保存在session中
session.setAttribute("code", code);
//5、模拟发送验证码
log.debug("发送验证码成功:验证码:{}", code);
//返回ok
return Result.ok();
}
流程图如下:
1、实现UserController的下的login方法:
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
2、在UserService下添加login方法,同样根据流程图实现功能,在这里隐藏了用户的敏感信息,如密码等,仅在session中存入电话,头像以及昵称信息,通过BeanUtil.copyProperties()实现属性复制,将User对象转化为UserDto:
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1、校验手机号
String phone = loginForm.getPhone();
if(isPhoneInvalid(phone)){
//2、手机号无效,返回错误信息
return Result.fail("手机号无效!");
}
//手机号有效
//4、取出session中的验证码
Object cacheCode = session.getAttribute("code");
//5、取出登录时输入的验证码
String loginCode = loginForm.getCode();
//6、校验验证码,如果验证码过期或者未获取到,以及验证码不正确
if(cacheCode == null || !cacheCode.toString().equals(loginCode)){
//7、返回错误信息
return Result.fail("验证码错误");
}
//8、查询用户是否存在
User user = query().eq("phone", phone).one();
//9、不存在,创建新用户
if(user == null) {
user = createUserWithPhone(phone);
}
//10、将用户信息存入session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));//隐藏用户的敏感信息,仅返回用户手机号,id,头像
return Result.ok();
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
//保存用户
save(user);
return user;
}
在第2步中,我们已经实现了短信验证码登录注册的功能,但是没有做登录校验。
上图中的请求是携带了 cookie 的,cookie 里是包含了 JSESSIONID 的。
服务端可以基于 JSESSIONID 来获得 session ,再从 session 里取出用户,进而来判断该用户是否存在。
但是这个流程里有一个问题,我们需要在每一个 controller 里来写这些业务逻辑。
我们可以通过拦截器(由 SpringMVC 提供)来统一拦截判断,最后决定是否放行。
此外,如果要做分布式 session,会存在系统负担和性能以及安全问题。考虑到系统负担和安全,我们可以在拦截器拦截到之后,将 session 中的用户信息保存到 ThreadLocal 中。每一个进入 Tomcat 的请求都是一个独立的线程,那么将来 ThreadLocal 就会在线程内开启一个独立的空间去保存这些请求(请求中携带了对应的用户信息)。这样一来,不同的用户访问 controller,都是一个独立的线程,每一个线程都有自己的用户信息,相互独立不干扰,controller 从 ThreadLocal 中取出用户信息。)
1、我们新建一个拦截器loginIntercepter:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取session
HttpSession session = request.getSession();
//2、获取session中的用户
Object user = session.getAttribute("user");
//3、判断用户是否存在
if(user == null) {
//4、不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//5、存在,保存信息到ThreadLocal
UserHolder.saveUser((UserDTO) user);
//6、放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//7、移除用户
UserHolder.removeUser();
}
}
2、接下来我们配置这个拦截器,新建一个MvcConfig 实现 WebMvcConfigurer接口,将拦截器添加,并且排除掉不需要拦截的路径(发送短信验证码,短信验证码登录等):
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
所以如果在多台Tomcat的情况下,就会存在session的共享问题,在后续通过Redis来解决这个问题,Redis同样满足内存存储以及键值对的结构,最为重要的一点就是可以实现数据共享。