基于Session实现登录
1.发送验证码
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到session,每一个session都对应着一个sessionid保存在浏览器的cookie中
session.setAttribute("code", code);
//5.发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
return Result.ok();
}
2.校验验证码
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号 TODO 没有指定唯一key,有漏洞
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误!");
}
//2.校验验证码
Object sessionCode = session.getAttribute("code");
String submitCode = loginForm.getCode();
if (sessionCode == null || !sessionCode.toString().equals(submitCode)) {
//3.不一致,报错
return Result.fail("验证码错误");
}
//4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if (user == null) {
//6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
//7.保存用户信息到session中
session.setAttribute("user", user); // TODO 没有指定唯一key,有漏洞
return Result.ok();
}
3. 登录状态校验
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.不存在,拦截
response.setStatus(401);
return false;
}
//5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser((User) user);
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//afterCompletion:请求处理完之后被执行
//移除用户
UserHolder.removeUser();
}
基于session的ThreadLocal,用户的每次请求还是要先判断session,再把session中的用户信息保存到ThreadLocal,从而实现用户的每一次请求都有用户信息。
为什么不直接用session进行登录校验呢? 因为如果这样的话每一次请求(Controller)都要携带session,再从session中读取数据,麻烦;而用了ThreadLocal后,可以直接在拦截器中把用户信息放在ThreadLocal中,在同一次请求的别的地方读取数据更为方便)。
缺点
在多台Tomcat中session并不共享,当请求切换到不同tomcat服务时会导致数据丢失的问题。
Redis代替session实现登录
1.发送验证码
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到redis 相当于set key value ex 120
stringRedisTemplate.opsForValue().set("login:code:" + phone, code, 2, TimeUnit.MINUTES);
//5.发生验证码
log.debug("发送短信验证码成功,验证码:{}", code);
return Result.ok();
2.校验验证码
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
// 2.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
String submitCode = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(submitCode)) {
//3.不一致,报错
return Result.fail("验证码错误");
}
//4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if (user == null) {
//6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
//7.保存用户信息到redis中
//7.1. 随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2. 将User对象转换为Hash存储,User对象的id是long,需要转为String
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userDTOMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3存储
stringRedisTemplate.opsForHash().putAll("login:token:" + token, userDTOMap);
//7.4 设置token有效期
stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
3.登录状态校验
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
//不存在,拦截
response.setStatus(401);
return false;
}
//2.基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
//3.判断用户是否存在
if(userMap.isEmpty()){
//不存在,拦截
response.setStatus(401);
return false;
}
//4.存在,将查询到的Hash数据转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//5.保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//6.刷新token有效期
stringRedisTemplate.expire("login:token:" + token, 999999 , TimeUnit.DAYS);
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//afterCompletion:请求处理完之后被执行
//移除用户
UserHolder.removeUser();
}
服务端返回token给客户端,前端每次请求都会携带token。
为什么不用手机号作为key呢?
因为用手机号作为key的话,要校验的时候每次请求都携带手机号,放在浏览器中不安全,所以把token放在前端。
改进登录状态刷新问题
上面的登录状态校验只有在规定路径才会刷新token有效期,而其他路径并不会刷新有效期,可能导致登录用户状态异常。
解决:新增一个拦截器,在第一个拦截器中拦截所有的路径,同时刷新令牌,第二个拦截器只需要判断ThreadLocal中的userDTO对象是否存在。
第一个拦截器
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
//2.基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
//3.判断用户是否存在
if(userMap.isEmpty()){
return true;
}
//4.存在,将查询到的Hash数据转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//5.保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//6.刷新token有效期
stringRedisTemplate.expire("login:token:" + token, 999999 , TimeUnit.DAYS);
return true;
}
第二个拦截器
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.判断是否需要拦截,依据:ThreadLocal中是否有用户
if (UserHolder.getUser() == null){
//没有,需要拦截,设置状态码
response.setStatus(401);
return false;
}
//有用户,放行
return true;
}