常见的登录逻辑漏洞及其修复方法
问题来源参考文献: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";
}
}
二、锁定账号
当用户的请求次数超过了限制,可以将该账号锁定一段时间,防止用户继续进行非法请求。主要思路如下:
- 在请求计数器类中,记录每个账号最后一次请求的时间戳。
- 在请求次数限制切面中添加判断,如果当前账号已经被锁定,则抛出异常提示用户账号已被锁定;否则,检查当前请求次数是否超过了限制,并更新最后一次请求时间戳。
- 在登录方法中判断当前账号是否已被锁定,如果已被锁定,则不允许用户登录。
下面是一个简单的实现示例:
在请求计数器类中添加记录最后一次请求时间的方法:
@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切面或者令牌桶
- 定义一个验证码请求计数器类 VerificationCodeRequestCounter,用于记录验证码请求次数和最后一次请求时间,可以采用Map<String, VerificationCodeRequest>类型的集合,其中Key为用户账号,Value为该账号的验证码请求信息。
- 利用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";
}
}
万能验证码
- 添加滑块验证码或人机验证等复杂验证码,增加破解的难度。
- 添加验证码有效期,验证码过期后需重新获取验证码。
- 限制验证码输入错误次数,超过一定次数后需要重新获取验证码。
下面是一个限制验证码输入错误次数的示例代码:
首先定义一个验证码错误计数器类 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、浏览器类型等,以增加凭证的复杂度和安全性。