Implement enhanced login functionality with JWT, role-based permissions, and frontend integration
Added JWT-based login dialog with form validation and secure token handling on the frontend. Updated backend with role-based access control, privilege management, and refined security configurations. Extended database schema for user privileges and permissions.
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class InMemoryRateLimiterService {
|
||||
|
||||
private final Map<String, RequestCounter> limiters = new ConcurrentHashMap<>();
|
||||
|
||||
public boolean tryAcquire(String key, int limit, Duration duration) {
|
||||
RequestCounter counter = limiters.computeIfAbsent(key, k -> new RequestCounter());
|
||||
return counter.tryAcquire(limit, duration);
|
||||
}
|
||||
|
||||
public Duration getNextResetTime(String key, Duration duration) {
|
||||
RequestCounter counter = limiters.get(key);
|
||||
if (counter == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
return counter.getNextResetTime(duration);
|
||||
}
|
||||
|
||||
public int getRemainingTokens(String key, int limit, Duration duration) {
|
||||
RequestCounter counter = limiters.get(key);
|
||||
if (counter == null) {
|
||||
return limit;
|
||||
}
|
||||
return Math.max(0, limit - counter.getCurrentCount(duration));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RateLimit {
|
||||
int limit() default 20;
|
||||
long timeValue() default 60;
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
String key() default "";
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class RateLimitAspect {
|
||||
|
||||
private final InMemoryRateLimiterService rateLimiterService;
|
||||
|
||||
@Around("""
|
||||
@annotation(com.alttd.altitudeweb.services.limits.RateLimit)
|
||||
|| @within(com.alttd.altitudeweb.services.limits.RateLimit)""")
|
||||
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes == null) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
HttpServletResponse response = requestAttributes.getResponse();
|
||||
|
||||
String clientIp = request.getRemoteAddr();
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
|
||||
if (rateLimit == null) {
|
||||
rateLimit = method.getDeclaringClass().getAnnotation(RateLimit.class);
|
||||
}
|
||||
|
||||
if (rateLimit == null) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
int limit = rateLimit.limit();
|
||||
Duration duration = Duration.ofSeconds(rateLimit.timeUnit().toSeconds(rateLimit.timeValue()));
|
||||
String customKey = rateLimit.key();
|
||||
|
||||
String key = clientIp + "-" + (customKey.isEmpty() ? method.getName() : customKey);
|
||||
|
||||
boolean allowed = rateLimiterService.tryAcquire(key, limit, duration);
|
||||
|
||||
if (allowed) {
|
||||
if (response != null) {
|
||||
response.setHeader("X-Rate-Limit-Limit", String.valueOf(limit));
|
||||
response.setHeader("X-Rate-Limit-Remaining",
|
||||
String.valueOf(rateLimiterService.getRemainingTokens(key, limit, duration)));
|
||||
}
|
||||
|
||||
return joinPoint.proceed();
|
||||
} else {
|
||||
log.warn("Rate limit exceeded for IP: {}, endpoint: {}", clientIp, request.getRequestURI());
|
||||
|
||||
Duration nextResetTime = rateLimiterService.getNextResetTime(key, duration);
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||
.header("X-Rate-Limit-Limit", String.valueOf(limit))
|
||||
.header("X-Rate-Limit-Remaining", "0")
|
||||
.header("Retry-After", String.valueOf(nextResetTime.getSeconds()))
|
||||
.header("Access-Control-Expose-Headers", "Retry-After, X-Rate-Limit-Limit, X-Rate-Limit-Remaining")
|
||||
.body(String.format("Rate limit exceeded. Try again in %d seconds.", nextResetTime.getSeconds()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class RequestCounter {
|
||||
private final Queue<Instant> requestTimestamps = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/**
|
||||
* Attempts to acquire a permit for a request within the specified rate limit.
|
||||
* This method checks if the number of requests made within the given duration
|
||||
* is less than the specified limit. If the limit has not been exceeded, the
|
||||
* current request is recorded and the method will return true. Otherwise, it
|
||||
* will return false.
|
||||
*
|
||||
* @param limit the maximum number of requests allowed within the specified duration
|
||||
* @param duration the time window during which requests are counted against the limit
|
||||
* @return true if the request is successfully acquired without exceeding the rate limit, false otherwise
|
||||
*/
|
||||
public synchronized boolean tryAcquire(int limit, Duration duration) {
|
||||
Instant currentTime = Instant.now();
|
||||
Instant cutoffTime = currentTime.minus(duration);
|
||||
|
||||
while (!requestTimestamps.isEmpty() && requestTimestamps.peek().isBefore(cutoffTime)) {
|
||||
requestTimestamps.poll();
|
||||
}
|
||||
|
||||
if (requestTimestamps.size() < limit) {
|
||||
requestTimestamps.add(currentTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getCurrentCount(Duration duration) {
|
||||
Instant cutoffTime = Instant.now().minus(duration);
|
||||
|
||||
return (int) requestTimestamps.stream()
|
||||
.filter(timestamp -> !timestamp.isBefore(cutoffTime))
|
||||
.count();
|
||||
}
|
||||
|
||||
public Duration getNextResetTime(Duration duration) {
|
||||
if (requestTimestamps.isEmpty()) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
Instant oldestTimestamp = requestTimestamps.peek();
|
||||
Instant resetTime = oldestTimestamp.plus(duration);
|
||||
Duration timeUntilReset = Duration.between(Instant.now(), resetTime);
|
||||
|
||||
return timeUntilReset.isNegative() ? Duration.ZERO : timeUntilReset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alttd.altitudeweb.services.user;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException {
|
||||
try {
|
||||
//Validate uuid
|
||||
UUID.fromString(uuid);
|
||||
return new User(uuid, "", Collections.emptyList());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new UsernameNotFoundException("Invalid UUID format: " + uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user