点评项目-4-隐藏敏感信息、使用 redis 优化登录业务

一、隐藏敏感信息

之前我们对 /user/me 路径,直接返回了登录的所有用户信息,其中的 passward 等敏感信息也会被返回到前端,这是很危险的,故我们需要选择性的返回用户信息,隐藏敏感用户信息

我们可以创建一个 UserDTO 类将 user 中可以返回的信息封装到其中后,将 UserDTO 返回

java 复制代码
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

然后我们将登录成功时,存入 user 的操作,改为存入 UserDTO ,这里使用的是 hutool 工具中 BeanUtil 来封装,将之前的 user 换成 UserDTO 后再存入 session

java 复制代码
        //保存用户登录信息
//        session.setAttribute("user",user);
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

在拦截器进行登录校验时,我们会拿出这个 user ,需要将拦截器的对应代码也修改

java 复制代码
    //前置拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //拿到 session 中的 user
        Object user = request.getSession().getAttribute("user");
        System.out.println(user+"进入前置拦截器");
        //若用户不存在,拦截
        if(user == null){
            response.setStatus(401);//响应 401 状态码,表示未授权
            return false;
        }
        //将用户保存在 ThreadLocal 中,调用 UserHolder 中的静态方法
        UserHolder.saveUser((UserDTO) user);
        //放行
        System.out.println("前置拦截放行");
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
java 复制代码
public class UserHolder {

    //user 对应的的线程池
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    //往线程池中存入用户
    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    //拿到当前线程的 user,一个线程只有一个 user
    public static UserDTO getUser(){
        return tl.get();
    }

    //移除当前线程的 user
    public static void removeUser(){
        tl.remove();
    }

}

修改完毕后,我们再次使用 postman 进行测试,完成发验证码,登录,等了校验三个请求

可以看到,这次返回的用户信息只含有 id,昵称,头像。保护了敏感信息。

二、解决集群的 session 共享问题

使用 session 进行登录信息的存储,当出现多台 tomcat 时,存储的信息会出现无法共享的情况,我们可以通过 Redis 来存储信息来解决

对于验证码,我们可以使用手机号作为 key 验证码存入 value中;

对于登录信息,我们可以使用哈希结构存储不同的信息,使用随机 token 生成随机且唯一的 key,在响应时,将 token 返回给浏览器,在之后需要用到 token 的请求,需要在请求中发送 token

在完成业务之前,我们先配置一下 redis 并测试是否可以正常访问

yml 配置文件:

java 复制代码
server:
  port: 8082

spring:
  application:
    name: mydp
  datasource:
    url: jdbc:mysql://localhost:3306/learnbase
    username: root
    password: 1234
  redis:
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        max-active: 8 # 最大连接
        max-idle: 8 # 最大空闲连接
        min-idle: 0
        max-wait: 100 # 最大等待时间,单位毫秒
    jackson:
      default-property-inclusion: non_null # JSON处理时忽略非空字段

测试 redis 是否连接正常

java 复制代码
@SpringBootTest
class MyDianpingApplicationTests {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void redisText() {
        stringRedisTemplate.opsForValue().set("dataRides","check");
        Object dataRides = stringRedisTemplate.opsForValue().get("dataRides");
        System.out.println(dataRides);
    }

}

若 redis 中写入了键值对 dataRides , check 则可以认为连接时畅通的

接下来我们便可以通过 Redis 来优化登录业务了

发送验证码

首先在成员变量位置注入 StringRedisTemplate

java 复制代码
    @Resource
    private StringRedisTemplate stringRedisTemplate;

Controller 层和 UserService 接口无需改动,只需修改 UserServiceImpl 的 sendCode 方法即可

java 复制代码
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //先用 hutool 工具校验手机号是否合法
        if (phone == null || !PhoneUtil.isPhone(phone)) {
            return Result.fail("请输入合法的手机号");//若不合法直接响应错误
        }
        //用 hutool 工具生成六位验证码
        String code = RandomUtil.randomNumbers(6);
        //将生成的验证码放入 session
        //TODO 优化:保存验证码到redis
        //TODO 后面两个参数时验证码的有效期,2分钟,设置后 redis 会在底层加上 set key value ex 120
        stringRedisTemplate.opsForValue().set("login:code:"+phone,code,2, TimeUnit.MINUTES);
        //给手机号发送验证码,这里模拟发送验证码的操作,而不是真正的发送
        System.out.println("手机收到了一条验证码短信:"+code);
        return Result.ok();
    }

登录逻辑

从 redis 中获取验证码并验证,验证通过后存入用户信息到 redis

生成 token 作为唯一的标识符,将用户信息封装为哈希表,将其挂在 token 下后存入 redis

最后返回 token ,在之后每次需要用到用户信息的请求中,我们都将 token 作为请求头发送请求

java 复制代码
    @Override
    public Result login(LoginFormDTO loginFormDTO, HttpSession session) {
        if(loginFormDTO == null){
            return Result.fail("无效操作");
        }
        //校验手机号
        String phone = loginFormDTO.getPhone();
        if (phone == null || !PhoneUtil.isPhone(phone)) {
            return Result.fail("请输入合法的手机号");//若不合法直接响应错误
        }
        //拿到 session 域中的验证码
        //TODO 优化:从 redis 中获取验证码
        String code1 = stringRedisTemplate.opsForValue().get("login:code:"+phone);
        String code = loginFormDTO.getCode();
        if(!code.equals(code1)){
           return Result.fail("验证码输入错误,请重新输入");
        }
        //验证码正确,判断是否存在用户
        User user = userMapper.selectByPhone(phone);
        if(user == null){
            //若不存在就创建一个用户并存入 mysql
            user = createUserByPhone(phone);
            userMapper.insert(user);
        }
        //保存用户登录信息
        //TODO 优化:生成 token ,使用哈希的方式保存用户信息
        //使用 hutool 的 UUID 来生成 token
        String token = UUID.randomUUID().toString();
        UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
        //使用 stringRedisTemplate,使用 hash 的方式存储存信息时必须保证所有 key value 都是 String 类型
        Map<String,String> userMap = new HashMap<>();
        userMap.put("id",Long.toString(userDTO.getId()));
        userMap.put("nickName",userDTO.getNickName());
        userMap.put("icon",userDTO.getIcon());
        stringRedisTemplate.opsForHash().putAll("login:token:"+token,userMap);
        //设置 token 有效日期,此处设定初始有效日期,可以通过通用拦截器更新有效日期
        stringRedisTemplate.expire("login:token:"+token,30,TimeUnit.MINUTES);
        //TODO 优化:返回生成的 token
        return Result.ok(token);
    }

拦截器更新 token 有效期,登录验证

为了做到当用户完成任何操作后,token 的有效期更新,以保证用户的使用体验,我们可以设置一个全局拦截器,将 token 的更新操作写在全局拦截器中,并在全局拦截器中将用户信息存入线程池中

再使用第二级拦截器判断用户是否登录,登录的用户一定会存在于线程池,我们可以通过此来判断用户是否登录

一级拦截
java 复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {

    //这个类没有被 Spring 管理,我们可以使用其配置类 MvcConfig 拿到后通过有参构造传递进来
    private StringRedisTemplate stringRedisTemplate;
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //前置拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //获取请求头中的 token
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
            //token 不存在,直接放行
            return true;
        }

        //基于 token 取用户信息
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:"+token);
        if(userMap.isEmpty()){
            //没有信息,直接放行
            return true;
        }
        //有用户信息,将 userMap 转为 UserDTO 后保存到 ThreadLocal 中
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//最后一个参数表示是否忽略转换过程中的错误
        UserHolder.saveUser(userDTO);
        //更新 token 的失效时间
        stringRedisTemplate.expire("login:token:"+token,30, TimeUnit.MINUTES);
        //放行
        return true;
    }

    //渲染后拦截
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //在渲染后将 user 从线程中移除
        UserHolder.removeUser();
    }
}
二级拦截
java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    //前置拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断线程池中是否有登录用户,若没有则拦截,返回 401 状态码
        if(UserHolder.getUser() == null){
            response.setStatus(401);
            return false;
        }
        return true;
    }

}

测试

相关推荐
JH30738 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
qq_124987075311 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_11 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_8187320611 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu14 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶15 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip16 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide16 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf17 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
zhangyi_viva17 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端