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

一、隐藏敏感信息

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

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

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

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

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

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

    //前置拦截
    @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);
    }
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 配置文件:

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 是否连接正常

@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

    @Resource
    private StringRedisTemplate stringRedisTemplate;

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

    @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 作为请求头发送请求

    @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 的更新操作写在全局拦截器中,并在全局拦截器中将用户信息存入线程池中

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

一级拦截
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();
    }
}
二级拦截
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;
    }

}

测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值