IT WORKS almost
This commit is contained in:
parent
7f7ea6da9f
commit
cadc8e499d
8 changed files with 251 additions and 38 deletions
|
@ -7,12 +7,22 @@ import org.springframework.context.annotation.Configuration;
|
||||||
public class AppConfig {
|
public class AppConfig {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Bean(name = "rateLimit")
|
||||||
|
public boolean rateLimit() {
|
||||||
|
String appName = System.getProperty("rateLimit");
|
||||||
|
if (appName == null)
|
||||||
|
appName = System.getenv("rateLimit");
|
||||||
|
System.out.println("rateLimit=" + appName);
|
||||||
|
return (appName != null) ? Boolean.valueOf(appName) : false;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean(name = "loginEnabled")
|
@Bean(name = "loginEnabled")
|
||||||
public boolean loginEnabled() {
|
public boolean loginEnabled() {
|
||||||
String appName = System.getProperty("login.enabled");
|
String appName = System.getProperty("login.enabled");
|
||||||
if (appName == null)
|
if (appName == null)
|
||||||
appName = System.getenv("login.enabled");
|
appName = System.getenv("login.enabled");
|
||||||
|
System.out.println("loginEnabled=" + appName);
|
||||||
return (appName != null) ? Boolean.valueOf(appName) : false;
|
return (appName != null) ? Boolean.valueOf(appName) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
@ -30,11 +31,21 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserDetailsService userDetailsService;
|
private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("rateLimit")
|
||||||
|
public boolean rateLimit;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
if (!rateLimit) {
|
||||||
|
// If rateLimit is not enabled, just pass all requests without rate limiting
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String method = request.getMethod();
|
String method = request.getMethod();
|
||||||
|
|
||||||
if (!"POST".equalsIgnoreCase(method)) {
|
if (!"POST".equalsIgnoreCase(method)) {
|
||||||
|
@ -43,14 +54,23 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String identifier;
|
String identifier = null;
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
|
|
||||||
|
// Check for API key in the request headers
|
||||||
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
|
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||||
|
} else {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||||
identifier = userDetails.getUsername();
|
identifier = userDetails.getUsername();
|
||||||
} else {
|
}
|
||||||
identifier = request.getRemoteAddr(); // Use IP as identifier if not authenticated
|
}
|
||||||
|
|
||||||
|
// If neither API key nor an authenticated user is present, use IP address
|
||||||
|
if (identifier == null) {
|
||||||
|
identifier = request.getRemoteAddr();
|
||||||
}
|
}
|
||||||
|
|
||||||
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket());
|
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket());
|
||||||
|
@ -66,13 +86,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||||
response.getWriter().write("Rate limit exceeded for POST requests.");
|
response.getWriter().write("Rate limit exceeded for POST requests.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bucket createUserBucket() {
|
private Bucket createUserBucket() {
|
||||||
Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1)));
|
Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1)));
|
||||||
return Bucket.builder().addLimit(limit).build();
|
return Bucket.builder().addLimit(limit).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ public class CustomUserDetailsService implements UserDetailsService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
@ -14,7 +15,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
@ -25,14 +26,23 @@ public class SecurityConfiguration {
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
public boolean loginEnabledValue;
|
public boolean loginEnabledValue;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
if(loginEnabledValue) {
|
if(loginEnabledValue) {
|
||||||
|
|
||||||
http.csrf().disable();
|
http.csrf().disable();
|
||||||
http
|
http
|
||||||
.formLogin(formLogin -> formLogin
|
.formLogin(formLogin -> formLogin
|
||||||
|
@ -74,5 +84,7 @@ public class SecurityConfiguration {
|
||||||
return authProvider;
|
return authProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.util.HashMap;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
@ -88,6 +89,21 @@ public class UserService {
|
||||||
return userRepository.findByApiKey(apiKey);
|
return userRepository.findByApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserDetails loadUserByApiKey(String apiKey) {
|
||||||
|
User userOptional = userRepository.findByApiKey(apiKey);
|
||||||
|
if (userOptional != null) {
|
||||||
|
User user = userOptional;
|
||||||
|
// Convert your User entity to a UserDetails object with authorities
|
||||||
|
return new org.springframework.security.core.userdetails.User(
|
||||||
|
user.getUsername(),
|
||||||
|
user.getPassword(), // you might not need this for API key auth
|
||||||
|
getAuthorities(user)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null; // or throw an exception
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean validateApiKeyForUser(String username, String apiKey) {
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||||
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
|
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
|
||||||
|
|
|
@ -6,6 +6,8 @@ import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
@ -69,5 +72,32 @@ public class UserController {
|
||||||
return "redirect:/addUsers";
|
return "redirect:/addUsers";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/get-api-key")
|
||||||
|
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||||
|
if (principal == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||||
|
}
|
||||||
|
String username = principal.getName();
|
||||||
|
String apiKey = userService.getApiKeyForUser(username);
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/update-api-key")
|
||||||
|
public ResponseEntity<String> updateApiKey(Principal principal) {
|
||||||
|
if (principal == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||||
|
}
|
||||||
|
String username = principal.getName();
|
||||||
|
User user = userService.refreshApiKeyForUser(username);
|
||||||
|
String apiKey = user.getApiKey();
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package stirling.software.SPDF.model;
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
|
private final Object principal;
|
||||||
|
private Object credentials;
|
||||||
|
|
||||||
|
public ApiKeyAuthenticationToken(String apiKey) {
|
||||||
|
super(null);
|
||||||
|
this.principal = null;
|
||||||
|
this.credentials = apiKey;
|
||||||
|
setAuthenticated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
super(authorities);
|
||||||
|
this.principal = principal; // principal can be a UserDetails object
|
||||||
|
this.credentials = apiKey;
|
||||||
|
super.setAuthenticated(true); // this authentication is trusted
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead.");
|
||||||
|
}
|
||||||
|
super.setAuthenticated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eraseCredentials() {
|
||||||
|
super.eraseCredentials();
|
||||||
|
credentials = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@
|
||||||
<br> <br>
|
<br> <br>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-9">
|
||||||
|
|
||||||
<!-- User Settings Title -->
|
<!-- User Settings Title -->
|
||||||
<h2 class="text-center" th:text="#{settings.accountSettings}">User Settings</h2>
|
<h2 class="text-center" th:text="#{settings.accountSettings}">User Settings</h2>
|
||||||
|
@ -58,6 +58,81 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Your API Key
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="password" class="form-control" id="apiKey" placeholder="Your API Key" readonly>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" id="showBtn" type="button" onclick="showApiKey()">👁️ Show</button>
|
||||||
|
<button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">🔄 Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function showApiKey() {
|
||||||
|
const apiKeyElement = document.getElementById("apiKey");
|
||||||
|
if (apiKeyElement.type === "password") {
|
||||||
|
apiKeyElement.type = "text";
|
||||||
|
} else {
|
||||||
|
apiKeyElement.type = "password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async function() {
|
||||||
|
try {
|
||||||
|
let response = await fetch('/get-api-key', { method: 'POST' });
|
||||||
|
if (response.status === 200) {
|
||||||
|
let apiKey = await response.text();
|
||||||
|
manageUIState(apiKey);
|
||||||
|
} else {
|
||||||
|
manageUIState(null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('There was an error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshApiKey() {
|
||||||
|
try {
|
||||||
|
let response = await fetch('/update-api-key', { method: 'POST' });
|
||||||
|
if (response.status === 200) {
|
||||||
|
let apiKey = await response.text();
|
||||||
|
manageUIState(apiKey);
|
||||||
|
document.getElementById("apiKey").type = 'text';
|
||||||
|
} else {
|
||||||
|
alert('Error refreshing API key.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('There was an error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function manageUIState(apiKey) {
|
||||||
|
const apiKeyElement = document.getElementById("apiKey");
|
||||||
|
const showBtn = document.getElementById("showBtn");
|
||||||
|
|
||||||
|
if (apiKey && apiKey.trim().length > 0) {
|
||||||
|
apiKeyElement.value = apiKey;
|
||||||
|
showBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
apiKeyElement.value = "";
|
||||||
|
showBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<hr> <!-- Separator Line -->
|
<hr> <!-- Separator Line -->
|
||||||
|
|
||||||
<h4>Sync browser settings with Account</h4>
|
<h4>Sync browser settings with Account</h4>
|
||||||
|
|
Loading…
Reference in a new issue