Profilbild-Upload (413 + Auto-Skalierung) und CSRF aktivieren

- application.properties: multipart max-file/request-size auf 10MB
  (nginx client_max_body_size muss serverseitig passend gesetzt sein)
- ProfileController: hochgeladene Profilbilder werden serverseitig auf
  max. 400px herunterskaliert und als JPEG normalisiert gespeichert
  (statt der Originaldatei roh); nicht lesbare Bilder werden ignoriert
- SecurityConfig: CSRF aktiviert; Spiele-JSON-APIs (/api/games,
  /api/techtacular, /api/mindmatch) ausgenommen; GET-Logout erlaubt,
  da die Logout-Links in den Templates GET sind

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AliceDarkest
2026-06-26 15:22:25 +02:00
parent 7cb6621d1b
commit fc98e5aad1
3 changed files with 55 additions and 19 deletions
@@ -10,6 +10,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.*; import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration @Configuration
public class SecurityConfig { public class SecurityConfig {
@@ -31,10 +32,16 @@ public class SecurityConfig {
.permitAll() .permitAll()
) )
.logout(logout -> logout .logout(logout -> logout
// Logout-Links in den Templates sind GET -> GET-Logout erlauben, sonst 404/Fehler unter CSRF
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
.logoutSuccessUrl("/?logout") .logoutSuccessUrl("/?logout")
.permitAll() .permitAll()
) )
.csrf(csrf -> csrf.disable()); // optional deaktivieren bei Custom-Logik/API-Nutzung .csrf(csrf -> csrf
// Spiele-APIs werden per fetch (application/json) ohne Token aufgerufen -> ausnehmen.
// Reine JSON-Endpunkte sind nicht über klassische HTML-Formulare CSRF-ausnutzbar.
.ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**")
);
return http.build(); return http.build();
} }
@@ -14,6 +14,11 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -99,26 +104,42 @@ public class ProfileController {
if (file != null && !file.isEmpty()) { if (file != null && !file.isEmpty()) {
try { try {
BufferedImage original = ImageIO.read(file.getInputStream());
if (original == null) {
// Keine lesbare Bilddatei -> Upload ignorieren, restliches Profil trotzdem speichern.
System.out.println("Profilbild-Upload ignoriert: keine lesbare Bilddatei.");
} else {
// Auf max. Kantenlänge herunterskalieren (kein Hochskalieren).
final int MAX_DIM = 400;
int w = original.getWidth();
int h = original.getHeight();
double scale = Math.min(1.0, (double) MAX_DIM / Math.max(w, h));
int nw = Math.max(1, (int) Math.round(w * scale));
int nh = Math.max(1, (int) Math.round(h * scale));
BufferedImage scaled = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setColor(Color.WHITE); // JPEG kennt kein Alpha -> Transparenz auf weißem Grund
g.fillRect(0, 0, nw, nh);
g.drawImage(original, 0, 0, nw, nh, null);
g.dispose();
// Altes Profilbild löschen (falls vorhanden) // Altes Profilbild löschen (falls vorhanden)
if (existingUser.getProfilePicture() != null && !existingUser.getProfilePicture().isEmpty()) { if (existingUser.getProfilePicture() != null && !existingUser.getProfilePicture().isEmpty()) {
Path oldFilePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", existingUser.getProfilePicture()); Path oldFilePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", existingUser.getProfilePicture());
if (Files.exists(oldFilePath)) { Files.deleteIfExists(oldFilePath);
Files.delete(oldFilePath);
}
} }
// Neues Profilbild speichern // Normalisiert als JPEG speichern
String fileExtension = getFileExtension(file.getOriginalFilename()); String fileName = UUID.randomUUID() + ".jpg";
String fileName = UUID.randomUUID() + fileExtension;
Path uploadPath = Paths.get(UPLOAD_DIR); Path uploadPath = Paths.get(UPLOAD_DIR);
Files.createDirectories(uploadPath); Files.createDirectories(uploadPath);
ImageIO.write(scaled, "jpg", uploadPath.resolve(fileName).toFile());
Path filePath = uploadPath.resolve(fileName);
file.transferTo(filePath.toFile());
existingUser.setProfilePicture(fileName); existingUser.setProfilePicture(fileName);
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -41,3 +41,11 @@ spring.output.ansi.enabled=ALWAYS
# Öffentliche Basis-URL, z. B. für künftig erzeugte absolute Links. # Öffentliche Basis-URL, z. B. für künftig erzeugte absolute Links.
mindflux.public-url=${MINDFLUX_PUBLIC_URL:http://localhost:8282} mindflux.public-url=${MINDFLUX_PUBLIC_URL:http://localhost:8282}
# =======================
# Datei-Uploads (Profilbilder)
# =======================
# Hinweis: Der nginx-Reverse-Proxy muss client_max_body_size passend gesetzt haben,
# sonst greift schon dort ein 413, bevor die App den Upload sieht.
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB