常见的登录逻辑漏洞及其修复方法

常见的登录逻辑漏洞及其修复方法

问题来源参考文献:https://blog.csdn.net/qq_63217130/article/details/130187929?spm=1001.2014.3001.5502

密码可爆破的情况

一、限制请求次数

可以利用Spring AOP实现请求次数的限制,主要思路如下:
定义一个请求计数器类 RequestCounter,用于记录请求次数,可以采用Map<String, Integer>类型的集合,其中Key为用户账号,Value为当前账号请求次数。
利用Spring AOP,在处理用户请求的方法上添加切面,判断当前请求账号的请求次数是否超过了限制,如果超过了限制,则抛出异常提示用户请求次数过多。
下面是一个简单的实现示例:
定义请求计数器类:

@Component
public class RequestCounter {
    private Map<String, Integer> counterMap = new ConcurrentHashMap<>();
    public void increment(String account) {
        Integer count = counterMap.get(account);
        if (count == null) {
            count = 0;
        }
        counterMap.put(account, count + 1);
    }
    public int getCount(String account) {
        Integer count = counterMap.get(account);
        return count == null ? 0 : count;
    }
}

定义请求次数限制切面:

@Aspect
@Component
public class RequestLimitAspect {
    @Autowired
    private RequestCounter requestCounter;
    @Pointcut("@annotation(com.example.demo.annotation.RequestLimit)")
    public void requestLimit() {}
    @Before("requestLimit()")
    public void before(JoinPoint joinPoint) throws RequestLimitException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String account = request.getParameter("account");
        int count = requestCounter.getCount(account);
        if (count >= 5) {
            throw new RequestLimitException("请求次数过多,已被限制访问!");
        }
        requestCounter.increment(account);
    }
}

在需要进行请求次数限制的方法上添加@RequestLimit注解:

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping("/login")
    @RequestLimit
    public String login(String account, String password) {
        userService.login(account, password);
        return "success";
    }
}

二、锁定账号

当用户的请求次数超过了限制,可以将该账号锁定一段时间,防止用户继续进行非法请求。主要思路如下:

  1. 在请求计数器类中,记录每个账号最后一次请求的时间戳。
  2. 在请求次数限制切面中添加判断,如果当前账号已经被锁定,则抛出异常提示用户账号已被锁定;否则,检查当前请求次数是否超过了限制,并更新最后一次请求时间戳。
  3. 在登录方法中判断当前账号是否已被锁定,如果已被锁定,则不允许用户登录。

下面是一个简单的实现示例:
在请求计数器类中添加记录最后一次请求时间的方法:

@Component
public class RequestCounter {
    private Map<String, Integer> counterMap = new ConcurrentHashMap<>();
    private Map<String, Long> lastRequestTimeMap = new ConcurrentHashMap<>();
    public void increment(String account) {
        Integer count = counterMap.get(account);
        if (count == null) {
            count = 0;
        }
        counterMap.put(account, count + 1);
        lastRequestTimeMap.put(account, System.currentTimeMillis());
    }
    public int getCount(String account) {
        Integer count = counterMap.get(account);
        return count == null ? 0 : count;
    }
    public long getLastRequestTime(String account) {
        Long time = lastRequestTimeMap.get(account);
        return time == null ? 0L : time;
    }
}

修改请求次数限制切面:

@Aspect
@Component
public class RequestLimitAspect {
    @Autowired
    private RequestCounter requestCounter;
    @Pointcut("@annotation(com.example.demo.annotation.RequestLimit)")
    public void requestLimit() {}
    @Before("requestLimit()")
    public void before(JoinPoint joinPoint) throws RequestLimitException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String account = request.getParameter("account");
        long lastRequestTime = requestCounter.getLastRequestTime(account);
        if (lastRequestTime > 0 && System.currentTimeMillis() - lastRequestTime < 60000) {
            throw new RequestLimitException("请求过于频繁,请稍后再试!");
        }
        int count = requestCounter.getCount(account);
        if (count >= 5) {
            requestCounter.lastRequestTimeMap.put(account, System.currentTimeMillis());
            throw new RequestLimitException("请求次数过多,已被限制访问!");
        }
        requestCounter.increment(account);
    }
}

修改登录方法:

@Service
public class UserService {
    @Autowired
    private RequestCounter requestCounter;
    private Map<String, Long> lockMap = new ConcurrentHashMap<>();
    public void login(String account, String password) throws AccountLockedException {
        long lastRequestTime = requestCounter.getLastRequestTime(account);
        if (lastRequestTime > 0 && System.currentTimeMillis() - lastRequestTime < 60000) {
            throw new AccountLockedException("账号已被锁定,请稍后再试!");
        }
        // TODO: 验证账号密码
        // ...
        // 登录成功,清除请求次数和最后一次请求时间
        requestCounter.counterMap.remove(account);
        requestCounter.lastRequestTimeMap.remove(account);
        lockMap.remove(account);
    }
}

短信轰炸 aop切面或者令牌桶

  1. 定义一个验证码请求计数器类 VerificationCodeRequestCounter,用于记录验证码请求次数和最后一次请求时间,可以采用Map<String, VerificationCodeRequest>类型的集合,其中Key为用户账号,Value为该账号的验证码请求信息。
  2. 利用Spring AOP,在发送验证码的方法上添加切面,判断当前账号的验证码请求时间是否超过了限制,如果超过了限制,则抛出异常提示用户请求过于频繁。

下面是一个简单的实现示例:
定义验证码请求计数器类:

@Component
public class VerificationCodeRequestCounter {
    private Map<String, VerificationCodeRequest> requestMap = new ConcurrentHashMap<>();
    public void increment(String account) {
        VerificationCodeRequest request = requestMap.get(account);
        if (request == null) {
            request = new VerificationCodeRequest();
        }
        request.increment();
        requestMap.put(account, request);
    }
    public int getCount(String account) {
        VerificationCodeRequest request = requestMap.get(account);
        return request == null ? 0 : request.getCount();
    }
    public long getLastRequestTime(String account) {
        VerificationCodeRequest request = requestMap.get(account);
        return request == null ? 0L : request.getLastRequestTime();
    }
}
class VerificationCodeRequest {
    private int count;
    private long lastRequestTime;
    public VerificationCodeRequest() {
        this.count = 0;
        this.lastRequestTime = 0L;
    }
    public void increment() {
        count++;
        lastRequestTime = System.currentTimeMillis();
    }
    public int getCount() {
        return count;
    }
    public long getLastRequestTime() {
        return lastRequestTime;
    }
}

定义验证码请求次数限制切面:

@Aspect
@Component
public class VerificationCodeRequestLimitAspect {
    @Autowired
    private VerificationCodeRequestCounter requestCounter;
    @Pointcut("@annotation(com.example.demo.annotation.VerificationCodeRequestLimit)")
    public void verificationCodeRequestLimit() {}
    @Before("verificationCodeRequestLimit()")
    public void before(JoinPoint joinPoint) throws VerificationCodeRequestLimitException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String account = request.getParameter("account");
        long lastRequestTime = requestCounter.getLastRequestTime(account);
        if (lastRequestTime > 0 && System.currentTimeMillis() - lastRequestTime < 60000) {
            throw new VerificationCodeRequestLimitException("验证码请求过于频繁,请稍后再试!");
        }
        int count = requestCounter.getCount(account);
        if (count >= 3) {
            throw new VerificationCodeRequestLimitException("验证码请求次数过多,已被限制!");
        }
        requestCounter.increment(account);
    }
}

在需要进行验证码请求限制的方法上添加@VerificationCodeRequestLimit注解:

@RestController
public class UserController {
    @Autowired
    private VerificationCodeService verificationCodeService;
    @RequestMapping("/sendVerificationCode")
    @VerificationCodeRequestLimit
    public String sendVerificationCode(String account) {
        verificationCodeService.sendVerificationCode(account);
        return "success";
    }
}

万能验证码

  1. 添加滑块验证码或人机验证等复杂验证码,增加破解的难度。
  2. 添加验证码有效期,验证码过期后需重新获取验证码。
  3. 限制验证码输入错误次数,超过一定次数后需要重新获取验证码。

下面是一个限制验证码输入错误次数的示例代码:
首先定义一个验证码错误计数器类 VerificationCodeErrorCounter,用于记录验证码输入错误次数,可以采用Map<String, Integer>类型的集合,其中Key为验证码ID,Value为该验证码的错误输入次数。

@Component
public class VerificationCodeErrorCounter {
    private Map<String, Integer> errorMap = new ConcurrentHashMap<>();
    public void increment(String id) {
        Integer count = errorMap.get(id);
        if (count == null) {
            count = 0;
        }
        count++;
        errorMap.put(id, count);
    }
    public int getCount(String id) {
        Integer count = errorMap.get(id);
        return count == null ? 0 : count;
    }
    public void clear(String id) {
        errorMap.remove(id);
    }
}

然后在验证码校验的方法中,判断验证码输入错误次数是否超过了限制,如果超过了限制,则抛出异常提示用户验证码输入错误次数过多。

@RestController
public class VerificationCodeController {
    @Autowired
    private VerificationCodeService verificationCodeService;
    @Autowired
    private VerificationCodeErrorCounter errorCounter;
    @PostMapping("/verifyCode")
    public String verifyCode(String id, String code) {
        boolean result = verificationCodeService.verifyCode(id, code);
        if (!result) {
            errorCounter.increment(id);
            int count = errorCounter.getCount(id);
            if (count >= 3) {
                errorCounter.clear(id);
                throw new VerificationCodeErrorLimitException("验证码输入错误次数过多,请重新获取验证码!");
            }
            throw new VerificationCodeErrorException("验证码输入错误,请重新输入!");
        }
        errorCounter.clear(id);
        return "success";
    }
}

用户批量注册

后端对 IP 进行注册次数限制
1 定义一个 IP 计数器类 IpCounter,用于记录每个 IP 的注册次数。可以采用 Map<String, Integer> 类型的集合,其中 Key 为 IP 地址,Value 为该 IP 的注册次数。

@Component
public class IpCounter {
    private Map<String, Integer> countMap = new ConcurrentHashMap<>();
    public void increment(String ip) {
        Integer count = countMap.get(ip);
        if (count == null) {
            count = 0;
        }
        count++;
        countMap.put(ip, count);
    }
    public int getCount(String ip) {
        Integer count = countMap.get(ip);
        return count == null ? 0 : count;
    }
    public void clear(String ip) {
        countMap.remove(ip);
    }
}

2 在注册接口中,获取客户端的 IP 地址,并判断该 IP 的注册次数是否超过了限制。如果超过了限制,则抛出异常提示用户注册次数过多。

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private IpCounter ipCounter;
    @PostMapping("/register")
    public String register(String username, String password, HttpServletRequest request) {
        String ip = request.getRemoteAddr();
        int count = ipCounter.getCount(ip);
        if (count >= 3) {
            ipCounter.clear(ip);
            throw new IpRegisterLimitException("该 IP 注册次数过多,请稍后再试!");
        }
        ipCounter.increment(ip);
        userService.register(username, password);
        return "success";
    }
}

3 对于同一个 IP,需要在一定时间内进行注册次数限制。可以使用 Guava 的 Cache 工具类来实现,将 IP 地址作为 Key,注册时间作为 Value,设置过期时间为一定时间后,再次进行注册时,判断当前时间与注册时间的差是否超过了限制。

@Component
public class IpRegisterCache {
    private static final long EXPIRE_TIME = 1L; // 过期时间,单位为分钟
    private Cache<String, Long> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(EXPIRE_TIME, TimeUnit.MINUTES)
            .build();
    public boolean isOverLimit(String ip) {
        Long registerTime = cache.getIfPresent(ip);
        if (registerTime == null) {
            return false;
        }
        long currentTime = System.currentTimeMillis();
        return (currentTime - registerTime) < (EXPIRE_TIME * 60 * 1000);
    }
    public void put(String ip) {
        long currentTime = System.currentTimeMillis();
        cache.put(ip, currentTime);
    }
}
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private IpCounter ipCounter;
    @Autowired
    private IpRegisterCache ipRegisterCache;
    @PostMapping("/register")
    public String register(String username, String password, HttpServletRequest request) {
        String ip = request.getRemoteAddr();
        if (ipRegisterCache.isOverLimit(ip)) {
            throw new IpRegisterLimitException("该 IP 注册次数过多,请稍后再试!");
        }
        int count = ipCounter.getCount(ip);
        if (count >= 3) {
            ipCounter.clear(ip);
            ipRegisterCache.put(ip);
            throw new IpRegisterLimitException("该 IP 注册次数过多,请稍后再试!");
        }
        ipCounter.increment(ip);
        userService.register(username, password);
        return "success";
    }
}

销毁登录成功的凭证防止复用

1 在生成登录成功凭证时,设置凭证的有效期,并将凭证存储到 Redis 或其他缓存中。

public String generateToken() {
    String token = UUID.randomUUID().toString();
    stringRedisTemplate.opsForValue().set(token, "user_id", Duration.ofMinutes(30));
    return token;
}

2 在需要验证凭证的接口中,从请求头或请求参数中获取凭证,并从 Redis 中取出对应的值。如果存在,则说明凭证有效,可以继续执行操作;否则说明凭证无效,需要重新登录。

public void checkToken(String token) {
    String userId = stringRedisTemplate.opsForValue().get(token);
    if (userId == null) {
        throw new RuntimeException("Token invalid");
    } else {
        // ...执行其他操作...
    }
}

3 在用户注销或退出登录时,从 Redis 中删除对应的凭证,以销毁凭证并防止复用。

public void logout(String token) {
    stringRedisTemplate.delete(token);
}

通过这种方式,可以有效防止登录成功凭证被复用。当用户注销或退出登录时,系统会删除对应的凭证,即使攻击者获取了凭证,也无法再次使用该凭证进行操作。同时,为了增加凭证的安全性,可以在生成凭证时,加入一些额外的信息,如客户端 IP、浏览器类型等,以增加凭证的复杂度和安全性。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值