mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-10-25 11:17:28 +02:00 
			
		
		
		
	IT WORKS almost
This commit is contained in:
		
							parent
							
								
									7f7ea6da9f
								
							
						
					
					
						commit
						cadc8e499d
					
				| @ -7,12 +7,22 @@ import org.springframework.context.annotation.Configuration; | ||||
| 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") | ||||
|     public boolean loginEnabled() { | ||||
|         String appName = System.getProperty("login.enabled"); | ||||
|         if (appName == null)  | ||||
|             appName = System.getenv("login.enabled"); | ||||
|          | ||||
|         System.out.println("loginEnabled=" + appName); | ||||
|         return (appName != null) ? Boolean.valueOf(appName) : false; | ||||
|     } | ||||
| 	 | ||||
|  | ||||
| @ -6,6 +6,7 @@ import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import org.springframework.security.core.userdetails.UserDetails; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.beans.factory.annotation.Qualifier; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| @ -30,49 +31,68 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { | ||||
|     @Autowired | ||||
|     private UserDetailsService userDetailsService; | ||||
| 
 | ||||
|     @Autowired | ||||
|     @Qualifier("rateLimit") | ||||
|     public boolean rateLimit; | ||||
| 
 | ||||
|     @Override | ||||
|     protected void doFilterInternal(HttpServletRequest request, | ||||
|                                     HttpServletResponse response, | ||||
|                                     FilterChain filterChain) throws ServletException, IOException { | ||||
| 
 | ||||
|     String method = request.getMethod(); | ||||
|      | ||||
|     if (!"POST".equalsIgnoreCase(method)) { | ||||
|         // If the request is not a POST, just pass it through without rate limiting | ||||
|         filterChain.doFilter(request, response); | ||||
|         return; | ||||
|         if (!rateLimit) { | ||||
|             // If rateLimit is not enabled, just pass all requests without rate limiting | ||||
|             filterChain.doFilter(request, response); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         String method = request.getMethod(); | ||||
| 
 | ||||
|         if (!"POST".equalsIgnoreCase(method)) { | ||||
|             // If the request is not a POST, just pass it through without rate limiting | ||||
|             filterChain.doFilter(request, response); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         String identifier = null; | ||||
| 
 | ||||
|         // 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()) { | ||||
|                 UserDetails userDetails = (UserDetails) authentication.getPrincipal(); | ||||
|                 identifier = userDetails.getUsername(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // 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()); | ||||
|         ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); | ||||
| 
 | ||||
|         if (probe.isConsumed()) { | ||||
|             response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens())); | ||||
|             filterChain.doFilter(request, response); | ||||
|         } else { | ||||
|             long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; | ||||
|             response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); | ||||
|             response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); | ||||
|             response.getWriter().write("Rate limit exceeded for POST requests."); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     String identifier; | ||||
|     Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); | ||||
|      | ||||
|     if (authentication != null && authentication.isAuthenticated()) { | ||||
|         UserDetails userDetails = (UserDetails) authentication.getPrincipal(); | ||||
|         identifier = userDetails.getUsername(); | ||||
|     } else { | ||||
|         identifier = request.getRemoteAddr(); // Use IP as identifier if not authenticated | ||||
|     } | ||||
| 
 | ||||
|     Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket()); | ||||
|     ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); | ||||
|      | ||||
|     if (probe.isConsumed()) { | ||||
|         response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens())); | ||||
|         filterChain.doFilter(request, response); | ||||
|     } else { | ||||
|         long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; | ||||
|         response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); | ||||
|         response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); | ||||
|         response.getWriter().write("Rate limit exceeded for POST requests."); | ||||
|         return; | ||||
|     private Bucket createUserBucket() { | ||||
|         Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1))); | ||||
|         return Bucket.builder().addLimit(limit).build(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private Bucket createUserBucket() { | ||||
|     Bandwidth limit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofDays(1))); | ||||
|     return Bucket.builder().addLimit(limit).build(); | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -23,6 +23,7 @@ public class CustomUserDetailsService implements UserDetailsService { | ||||
|     @Autowired | ||||
|     private UserRepository userRepository; | ||||
| 
 | ||||
|     | ||||
|     @Override | ||||
|     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | ||||
|         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.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.context.annotation.Lazy; | ||||
| import org.springframework.security.authentication.dao.DaoAuthenticationProvider; | ||||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||||
| 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.web.SecurityFilterChain; | ||||
| import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | ||||
| 
 | ||||
| import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||||
| @Configuration | ||||
| public class SecurityConfiguration { | ||||
| 
 | ||||
| @ -25,14 +26,23 @@ public class SecurityConfiguration { | ||||
|     public PasswordEncoder passwordEncoder() { | ||||
|         return new BCryptPasswordEncoder(); | ||||
|     } | ||||
| 
 | ||||
|     @Autowired | ||||
|     @Lazy | ||||
|     private UserService userService; | ||||
|      | ||||
|     @Autowired | ||||
|     @Qualifier("loginEnabled") | ||||
|     public boolean loginEnabledValue; | ||||
|      | ||||
|     @Autowired | ||||
|     private UserAuthenticationFilter userAuthenticationFilter; | ||||
|      | ||||
|     @Bean | ||||
|     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception  { | ||||
|     	http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); | ||||
|          | ||||
|     	if(loginEnabledValue) { | ||||
|     		 | ||||
| 	    	http.csrf().disable(); | ||||
| 	        http | ||||
| 	            .formLogin(formLogin -> formLogin | ||||
| @ -74,5 +84,7 @@ public class SecurityConfiguration { | ||||
|         return authProvider; | ||||
|     } | ||||
|      | ||||
|      | ||||
|      | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -14,6 +14,7 @@ import java.util.HashMap; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.userdetails.UserDetails; | ||||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.stereotype.Service; | ||||
| @ -87,6 +88,21 @@ public class UserService { | ||||
|     public User getUserByApiKey(String 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) { | ||||
|         Optional<User> userOpt = userRepository.findByUsername(username); | ||||
|  | ||||
| @ -6,6 +6,8 @@ import java.util.Map; | ||||
| 
 | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| 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.stereotype.Controller; | ||||
| import org.springframework.ui.Model; | ||||
| @ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestParam; | ||||
| 
 | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import stirling.software.SPDF.config.security.UserService; | ||||
| import stirling.software.SPDF.model.User; | ||||
| 
 | ||||
| @Controller | ||||
| public class UserController { | ||||
| @ -68,6 +71,33 @@ public class UserController { | ||||
|     	userService.deleteUser(username);  | ||||
|         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> | ||||
|             <div class="container"> | ||||
|                 <div class="row justify-content-center"> | ||||
|                     <div class="col-md-8"> | ||||
|                     <div class="col-md-9"> | ||||
| 
 | ||||
|                         <!-- User Settings Title --> | ||||
|                         <h2 class="text-center" th:text="#{settings.accountSettings}">User Settings</h2> | ||||
| @ -58,6 +58,81 @@ | ||||
|                             </div> | ||||
|                         </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 --> | ||||
|                          | ||||
|                         <h4>Sync browser settings with Account</h4> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user