CodeGym /Kurse /Spring Security /Grenze zwischen UserAccount und UserProfile

Grenze zwischen UserAccount und UserProfile

Spring Security
Level 14, Lektion 2
Verfügbar

1. Das Problem „User mit allem auf einmal“ in der Sicherheit

Wenn wir anfangen, Benutzer in der Datenbank zu speichern, hat der Anfänger einen sehr natürlichen Wunsch: „Lass uns eine einzige Klasse User machen, alles dort hineinlegen – Konto, Profil, Avatar, Rollen und überhaupt alles, was man im Internet zum Wort 'User' finden kann.“ Dieses Gefühl ähnelt dem Wunsch, alle Werkzeuge in eine einzige große Kiste ohne Trennwände zu packen: ja, formal ist alles „an einem Ort“, aber jedes Mal holst du nicht das Richtige heraus, und früher oder später schneidest du dich.

Das Hauptproblem des „universellen User“ in einem Security-Projekt liegt nicht einmal in der Schönheit des Codes. Das Problem ist, dass Sicherheitsdaten fast immer sensitiv sind, während Profildaten fast immer bequem über die API ausgeliefert und aktualisiert werden sollen. Und wenn du sie in einer einzigen Entität vermischst, wirst du entweder anfangen, versehentlich zu viel preiszugeben, oder endlose @JsonIgnore einbauen, oder (der häufigste Fall) – eines Tages gibt jemand „schnell die Entity aus dem Controller zurück“ und du bekommst… eine Überraschung.

Schauen wir uns ein typisches Anti-Beispiel an. Es ist nicht schlecht, weil 'Clean Code es verbietet', sondern weil es direkte Sicherheitslücken provoziert (Datenlecks und unbefugte Änderungen).

package com.example.securecontent.user;

import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;

    // Diese Felder dürfen fast nie über die API nach außen dringen:
    private String passwordHash; // oh: selbst der Hash des Passworts sind sensitive Daten
    private boolean enabled;     // oh: der Client sollte nicht den Account-Status steuern
    private Set<String> roles;   // oh: Rollen sind Teil des Security-Kerns

    // Und das sind schon die „Schaufenster“-Felder des Profils:
    private String displayName;  // Profil: das, was wir Leuten zeigen
    private String bio;          // Profil: Beschreibung/Über mich
    private String avatarPath;   // Profil: Pfad zur Avatardatei (kein Security-Teil)
}

Und jetzt fügen wir einen „bequemen“ Controller hinzu. Er sieht nett aus, bis du dich erinnerst, dass „bequem“ und „sicher“ nicht immer Freunde sind.

package com.example.securecontent.user;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MeController {

    @GetMapping("/api/me")
    public User me() {
        // Gefährliche Angewohnheit: „Entity/Modell so zurückgeben“.
        // Sobald hier ein echter Benutzer aus der DB auftaucht, gibst du überschüssige Felder preis.
        return new User(); // Platzhalter, aber in der Realität würde jemand die Entity zurückgeben
    }
}

Selbst wenn jetzt User leer ist (weil es ein Skelett ist), lädt die Architektur bereits zu Fehlern ein. In einer Woche schließt du JPA an, fängst an, den „echten Benutzer“ zurückzugeben, und plötzlich erhält der Client Felder, die er überhaupt nicht sehen sollte. Und ja, manchmal „hast du Glück“ und die Serialisierung schließt das Feld nicht ein, aber bei Sicherheit darf man solche Wetten nicht eingehen: „Glück haben“ ist keine Strategie.

Der zweite Schmerzpunkt ist das Aktualisieren. Wenn du User in einem PATCH /api/me/profile akzeptierst, sagst du dem Client im Grunde: „Hier ist ein Objekt, du kannst versuchen, mir beliebige Felder zurückzuschicken, mal sehen, was passiert.“ Im besten Fall wirst du endlos filtern, im schlimmsten Fall wird eines Tages etwas aus dem Sicherheitskern aktualisiert (z. B. enabled), und du bemerkst es erst in der Produktion. Der dritte Schmerz ist die Wartung: Eine solche Klasse wird zu einem „Magneten“ für neue Felder, und nach einem Monat enthält sie alles, einschließlich der Träume und der Schmerzen des Entwicklers.

2. UserAccount und UserProfile: Zwei Entitäten

Um das Projekt nicht in ein Chaos zu verwandeln und die Sicherheit nicht fragil zu machen, führen wir vorab eine einfache Grenze ein: Benutzerkonto (UserAccount) und Profil (UserProfile) sind unterschiedliche Entitäten, auch wenn die API sie manchmal zusammen ausliefert. Wenn du ein Bild brauchst: UserAccount ist der „Pass für den Systemzugang“, und UserProfile ist die „Visitenkarte / das Benutzerformular“, das der Benutzer für andere (oder für sich selbst) ausfüllt.

Diese Grenze löst gleich mehrere praktische Aufgaben. Erstens reduziert sie das Risiko von Datenlecks aus dem Sicherheitsbereich über die API. Zweitens macht sie die Aktualisierungsabläufe sicherer: Das Profil kann geändert werden, ohne das Passwort / die Rollen / den Kontostatus zu berühren. Drittens hilft sie bei der Projektstruktur: Der Sicherheitsteil bleibt kompakt, und das Profil entwickelt sich separat, ohne das Authentifizierungs-/Autorisierungsmodell zu überlasten.

Es ist praktisch, die Unterschiede in einer kleinen Tabelle festzuhalten. Wichtig ist hier nicht, „wie im Lehrbuch“ auswendig zu lernen, sondern die Intuition zu erfassen: Es sind unterschiedliche Fragen des Systems.

Frage Beantwortet UserAccount Beantwortet UserProfile
„Wer bist du?“ (Identifikation) Ja (username, email) Nein
„Kannst du hereingelassen werden?“ Ja (enabled, accountNonLocked) Nein
„Was darfst du?“ Ja (roles) Nein
„Wie zeige ich dich in der UI?“ Teilweise (manchmal username) Ja (displayName, bio, avatarPath)
„Was ändert der Benutzer selbst in den Einstellungen?“ Selten (normalerweise nur das Passwort, und auch das getrennt) Ja (fast alles)
„Welche Daten dürfen nicht versehentlich über die API preisgegeben werden?“ Sehr viele (Hash, Status, Rollen) Fast keine (aber wir filtern trotzdem)

Nun schauen wir uns die minimalen Skelette an. UserAccount hatten wir bereits in der vorherigen Lektion, aber hier geht es besonders um den Fokus: Es ist der Sicherheitskern.

package com.example.securecontent.security.account;

import java.util.Set;

public class UserAccount {
    private Long id;

    // Identifikation des Benutzers (das, was er im System „ist“)
    private String username;
    private String email;

    // Sicherheitskern: Diese Felder dürfen nicht über normale Profil-APIs ausgegeben/entgegengenommen werden
    private String passwordHash;
    private boolean enabled;
    private boolean accountNonLocked;

    // Zugriffsrechte: Das ist kein „Profil“, sondern Teil der Autorisierung
    private Set<Role> roles; // Details zur Rollenspeicherung sind ein separates Thema
}

Und das Profil – das sind die anwendungsbezogenen Felder. Es kann leer sein, es kann erweitert werden, aber es darf weder Passwort noch Rollen noch „Login-Sperre“ enthalten.

package com.example.securecontent.profile;

public class UserProfile {
    // Verbindung zum Konto: Das Profil wird über die ID auf UserAccount „aufgesetzt“
    private Long userId;       // Referenz auf das Konto

    // Das, was wir in der UI zeigen/bearbeiten
    private String displayName;
    private String bio;
    private String avatarPath; // Pfad zur Avatardatei
}

Ich betone einen wichtigen Gedanken für den Anfänger: „Was, wenn ich /api/me brauche, das sowohl username als auch displayName enthält?“ – Das ist ein legitimes Bedürfnis, und es wird auf der Ebene des DTO gelöst, nicht durch Vermischung der Speichermodelle.

3. Minimales UserProfile: Was speichern

Wenn wir UserProfile zum ersten Mal abgrenzen, möchte man sofort „das Produkt verbessern“: Geburtsdatum, Links zu sozialen Netzwerken, Datenschutzeinstellungen, Lieblingshintergrundfarbe und eine Liste der Lieblings-Memes hinzufügen. Das ist emotional verständlich, aber für den aktuellen Kurs gefährlich: Wir sind im Security-Track und brauchen das Profil genau so weit, wie es hilft, geschützte Endpunkte, Szenarien für Nur-Besitzer und Datei-Upload (Avatar) zu demonstrieren. Deshalb halten wir UserProfile minimal und langweilig – langweilige Modelle gehen normalerweise seltener kaputt.

Das minimale Profil in unserem Projekt ist das, was der Benutzer tatsächlich sieht und als „seine Daten“ ändert, und das bequem in /api/me/profile angezeigt wird. Das sind bei uns displayName, bio und avatarPath. Der entscheidende Punkt ist userId, der das Profil mit dem Konto verbindet. Dabei ist userId nicht „noch eine ID“, sondern eine Brücke zwischen zwei Welten: der Welt des Security-Kontos und der Welt des Anwendungsprofils.

Das Skelett kann maximal kurz belassen werden, und das ist in Ordnung:

package com.example.securecontent.profile;

public class UserProfile {
    // ID des Kontos, zu dem das Profil gehört
    private Long userId;

    // Öffentliche/halböffentliche Profilfelder
    private String displayName;
    private String bio;

    // Wir speichern genau den „Link/Pfad“, nicht die binären Dateidaten
    private String avatarPath;
}

Die Invariante hier ist einfach (und sehr nützlich für den Kopf): Das Profil gehört zu genau einem Konto. Auf der Denkmodellebene: Wenn es ein UserAccount mit id = 10 gibt, dann existiert das Profil entweder (userId = 10), oder es wurde noch nicht erstellt, aber es kann nicht „Profil für ein anderes Konto“ sein. Das scheint offensichtlich, aber solche Offensichtlichkeiten retten später vor seltsamen Fehlern.

Es gibt noch einen praktischen Vorteil der Trennung: Du kannst das Profil beliebig oft aktualisieren (und sogar neue Felder hinzufügen), ohne deinen Sicherheitskern zu verändern. Das bedeutet, die Sicherheitsschicht denkt weiterhin in Kategorien: „ID, Login, Passwort, Rollen, enabled/locked“, während das Profil als normale Anwendungsfunktion wächst.

4. DTO/VIEW: Wie man die Antwort /api/me zusammenstellt

Eine sehr häufige Verwirrung bei Anfängern klingt so: „Da /api/me sowohl username als auch displayName ausliefert, müssen wir sie in einem Objekt speichern.“ Das ist die Logik „Wie zeichne ich den Bildschirm“ statt der Logik „Wie speichere und ändere ich Daten sicher“. Im Backend sollten diese beiden Logiken an der Grenze der Darstellung aufeinandertreffen: im DTO, View-Modell, Assembler, Mapper – nenne es, wie du willst, der Kern ist derselbe: Wir bauen die Antwort aus verschiedenen Quellen zusammen, vermischen aber nicht die Quellen.

Das Schema der Antwort /api/me könnte so aussehen: Wir nehmen einen Teil der Daten aus dem Konto, einen Teil aus dem Profil und geben dann dem Client genau das, was er braucht. Kein passwordHash, kein enabled, keine Rollen (wenn du sie nicht zeigen willst). Für Anfänger ist das eine hervorragende Gewohnheit: Gib keine Entity nach außen, sondern erstelle einen klaren Vertrag.

Zum Beispiel ein DTO „das, was der Client in der Zone /api/me sieht“:

package com.example.securecontent.profile.api;

public class MeView {
    // Das, was dem Client zur Identifikation in der UI gezeigt werden kann
    private String username;
    private String email;

    // Profilteil: die Schaufenster des Benutzers
    private String displayName;
    private String bio;
    private String avatarPath;
}

Und jetzt ein kleiner Mapper. Ja, man möchte ihn „nicht schreiben, weil es ja klar ist“. Aber in einem echten Projekt sind es genau solche Klassen, die vor Datenlecks und Chaos in den Controllern schützen.

package com.example.securecontent.profile.api;

import com.example.securecontent.profile.UserProfile;
import com.example.securecontent.security.account.UserAccount;

public class MeViewMapper {

    public MeView toView(UserAccount account, UserProfile profile) {
        // Wichtig: Hier wählen wir bewusst aus, welche Felder in die API-Antwort einfließen.
        // Alles, was wir nicht manuell setzen, wird nicht „versehentlich“ nach außen gelangen.
        MeView v = new MeView();

        // Nur die sicheren und notwendigen Daten aus dem Konto übernehmen
        v.setUsername(account.getUsername());
        v.setEmail(account.getEmail());

        // Profilfelder aus dem Profil übernehmen
        v.setDisplayName(profile.getDisplayName());
        v.setBio(profile.getBio());
        v.setAvatarPath(profile.getAvatarPath());

        return v;
    }
}

Beachte, dass der Mapper überhaupt keinen Zugriff auf passwordHash hat. Selbst wenn UserAccount ihn enthält, verwendet der Mapper dieses Feld einfach nicht. Psychologisch ist das eine wichtige Barriere: „Um etwas preiszugeben, müsste man es explizit wollen.“

Nun der Service, der MeView erstellt. Hier nehme ich bewusst eine Methode mit dem Parameter username, obwohl wir in der Realität den aktuellen Benutzer aus dem Security-Kontext bekommen. Die Feinheit „wie hole ich den aktuellen Benutzer“ ist ein separates Thema, in das wir jetzt nicht einsteigen müssen.

package com.example.securecontent.profile;

import com.example.securecontent.profile.api.MeView;
import com.example.securecontent.profile.api.MeViewMapper;
import com.example.securecontent.security.account.UserAccount;

public class MeService {

    private final UserAccountRepository accountRepository;
    private final UserProfileRepository profileRepository;
    private final MeViewMapper mapper;

    public MeView getMe(String username) {
        // 1) Zuerst das Konto anhand des „Logins“ finden (vereinfacht)
        UserAccount acc = accountRepository.getByUsername(username);

        // 2) Dann das Profil anhand der Konto-ID finden
        UserProfile prof = profileRepository.getByUserId(acc.getId());

        // 3) Erst danach das DTO für die äußere Antwort zusammenstellen
        return mapper.toView(acc, prof);
    }
}

Und schließlich der Controller. Er bleibt „dünn“: Er baut das Objekt nicht manuell zusammen, verteilt die Logik zum Profilabruf nicht über den Code, denkt nicht darüber nach, was wo gespeichert ist.

package com.example.securecontent.profile.api;

import com.example.securecontent.profile.MeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MeController {

    private final MeService meService;

    public MeController(MeService meService) {
        this.meService = meService;
    }

    @GetMapping("/api/me")
    public MeView me() {
        // Hier sollte der aktuelle Benutzer aus dem Security-Kontext stehen.
        // In der Vorlesung lassen wir einen Platzhalter, um die Themen nicht zu vermischen.
        return meService.getMe("user"); // Platzhalter: aktueller username
    }
}

Das wirkt etwas „wortreicher“ als „Entity zurückgeben“. Aber dieser Wortreichtum erkauft dir Sicherheit und Vorhersagbarkeit. Und ehrlich gesagt ist das im Security-Kurs einer der besten Deals.

Um das Bild im Kopf zu festigen, hier eine kleine Grafik, wie /api/me aus zwei Teilen zusammengesetzt wird:

flowchart TD
  Client["Client: GET /api/me"] --> Controller["MeController"]
  Controller --> Service["MeService"]
  Service --> AccRepo["UserAccountRepository"]
  Service --> ProfRepo["UserProfileRepository"]
  AccRepo --> Account["UserAccount (Sicherheitskern)"]
  ProfRepo --> Profile["UserProfile (Anwendungsprofil)"]
  Account --> Mapper["MeViewMapper"]
  Profile --> Mapper
  Mapper --> DTO["MeView (DTO für die Antwort)"]
  DTO --> Client

Wenn du denkst, „das sind zu viele Schichten für ein Übungsprojekt“, dann denk daran: Wir bauen keinen CRUD-Kurs, wir bauen einen Kurs, in dem Sicherheit verständlich und stabil bleiben soll. Die Trennung von Account/Profil ist einer der günstigsten Wege, das zu erreichen.

5. Aktualisierung des Profils: Request-DTO statt Entity

Die nächste typische Falle sieht so aus: „Da das Profil die Daten des Benutzers sind, soll er das Objekt UserProfile in PATCH /api/me/profile senden.“ Klingt logisch, aber es gibt einen Haken: Sobald du anfängst, neue Felder in UserProfile hinzuzufügen, erweiterst du versehentlich den API-Vertrag. Und falls du irrtümlich UserAccount oder einen „universellen User“ akzeptierst, öffnest du die Tür zur Aktualisierung von Rollen/Status.

Daher lautet der sichere Ansatz: Für jeden Aktualisierungsvorgang ein eigenes Request-DTO, das nur das enthält, was bei dieser Operation geändert werden darf. Das klingt „formal“, ist aber in der Praxis eines der zuverlässigsten Muster.

Zum Beispiel ein DTO für die Profilaktualisierung:

package com.example.securecontent.profile.api;

public class UpdateMyProfileRequest {
    // Nur das, was der Benutzer bei diesem Vorgang bearbeiten darf
    private String displayName;
    private String bio;
}

Der Controller akzeptiert genau dieses DTO. Er kennt und soll keine Rollen, kein enabled, keinen passwordHash kennen. Er bekommt einfach das, was der Benutzer ändern darf.

package com.example.securecontent.profile.api;

import com.example.securecontent.profile.ProfileService;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyProfileController {

    private final ProfileService profileService;

    public MyProfileController(ProfileService profileService) {
        this.profileService = profileService;
    }

    @PatchMapping("/api/me/profile")
    public void update(@RequestBody UpdateMyProfileRequest req) {
        profileService.updateProfile(10L, req); // Platzhalter: ID des aktuellen Users
    }
}

Und der Service, der das Profil tatsächlich ändert. Beachte, dass diese Änderung UserProfile betrifft, nicht UserAccount. Das ist unsere im Code umgesetzte Grenze: Die Profilaktualisierung darf nicht über das Kontoobjekt laufen.

package com.example.securecontent.profile;

import com.example.securecontent.profile.api.UpdateMyProfileRequest;

public class ProfileService {

    private final UserProfileRepository profileRepository;

    public void updateProfile(Long userId, UpdateMyProfileRequest req) {
        UserProfile profile = profileRepository.getByUserId(userId);
        profile.setDisplayName(req.getDisplayName());
        profile.setBio(req.getBio());
        profileRepository.save(profile);
    }
}

Ja, hier gibt es „Platzhalter“ (wie man userId erhält), und wir lassen das bewusst außen vor. Heute ist unsere Aufgabe die Datengrenze, nicht die Anbindung des Security-Kontextes. Aber selbst in dieser Form sieht man das Wesentliche: Das Profil wird in seiner eigenen Zone aktualisiert, der Sicherheitskern bleibt unberührt.

6. Beziehung in der DB: id des Kontos und userId des Profils

Wenn du zur Datenbank übergehst, muss die Beziehung zwischen Account und Profil irgendwie gespeichert werden. Der einfachste und verständlichste Gedanke: UserAccount hat eine id, UserProfile hat ein userId, und das ist derselbe numerische Identifikator. Das Profil ist sozusagen ein „Aufsatz“ auf das Konto. Das ist ein sehr einfaches Modell: Ein Konto – ein Profil (oder das Profil kann später erscheinen), und es ist ideal für einen Kurs, in dem wir nicht in ORM-Akrobatik ertrinken wollen.

Ich zeige die Idee an vereinfachten JPA-Entities. Ich werde hier nicht auf die Feinheiten von @OneToOne, @MapsId und so weiter eingehen – nicht weil es „im Leben nicht nötig“ ist, sondern weil in dieser Lektion die semantische Grenze wichtiger ist als die Schönheit des Mappings. In einem Übungsprojekt ist es eine normale Strategie, einfach und transparent zu beginnen.

UserAccountEntity (vereinfacht):

package com.example.securecontent.security.account;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class UserAccountEntity {

    @Id
    private Long id;

    private String username;
    private String email;
}

UserProfileEntity:

package com.example.securecontent.profile;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class UserProfileEntity {

    @Id
    private Long userId;

    private String displayName;
    private String avatarPath;
}

Ja, das sieht „zu minimal“ aus. Und das ist wunderbar. Denn ein minimales Modell hält die Grenzen besser. Hauptsache verstehen, dass userId im Profil auf das Konto verweist und nicht ein Eigenleben führt. Auf Datenbankebene wird das normalerweise mit einem Fremdschlüssel umgesetzt, aber selbst ohne das hast du bereits ein korrektes mentales Modell aufgebaut.

Ein weiterer Vorteil einer solchen Beziehung ist, dass du das Profil in Ruhe als separate Funktion weiterentwickeln kannst. Du möchtest bio hinzufügen – fügst es in UserProfile ein. Du möchtest createdAt im Konto – fügst es in UserAccount ein. Und das führt nicht zu endlosen Konflikten „wer und wann die gleiche Tabelle aktualisieren soll“.

7. Typische Fehler bei der Trennung von Account/Profil

Fehler Nr. 1: Fehlen von DTO an der API-Grenze.
Sehr häufige Situation: Du hast UserAccount und UserProfile erstellt, gut, aber dann hast du im Controller UserAccount zurückgegeben, „weil es schneller geht“. Formal sind die Klassen verschieden, aber das Problem ist nicht gelöst: Nach außen können trotzdem passwordHash, Rollen oder Statusflags gelangen. Die Behandlung ist einfach: Verwende an der API-Grenze DTO (MeView, ProfileView) und einen Mapper.

Fehler Nr. 2: Aktualisierung des Profils über das Kontoobjekt.
Manchmal macht der Entwickler PATCH /api/me und akzeptiert UserAccount, weil „es ja meine Daten sind“. Dann beginnt er bedingt „nur displayName zu ändern“, aber das Objekt enthält Rollen/Status/Passwort – und dann ist es nur eine Frage der Technik: Jemand vergisst den Filter oder macht BeanUtils.copyProperties, und du erhältst eine Aktualisierung der Sicherheitsfelder basierend auf den Eingabedaten des Clients. Aus Sicherheitssicht ist das eine sehr böse Überraschung. Der richtige Weg ist ein separates Request-DTO für das Profil-Update.

Fehler Nr. 3: Vermischte Verantwortlichkeiten in Namen und Paketen.
Selbst wenn du die Entitäten richtig getrennt hast, aber alles in ein einziges Paket wie com.example.securecontent.user legst, fängt das Gehirn an zu verwirren: Wo ist Sicherheit, wo ist Profil, wo ist API? Nach ein paar Wochen wird jede neue Klasse „versehentlich“ von allem abhängen. Es ist viel einfacher, die Grenze zu wahren, wenn UserAccount näher an den Security-/Auth-/Admin-Zonen lebt und UserProfile in der Profil-Feature. Das ist kein Dogma, aber eine gute Lehrschiene.

Fehler Nr. 4: Das Profil wird zur Müllhalde der Sicherheitslogik.
Manchmal tauchen in UserProfile Felder wie lockedReason, failedLoginAttempts, rolesText und andere Dinge auf, die eigentlich zur Sicherheit gehören und nicht zum Profil. Dann verwischst du die Grenze wieder, nur unter einem anderen Namen. Ein guter Test für den gesunden Menschenverstand: Wenn ein Feld Einfluss darauf hat, „ob man sich anmelden darf“ oder „was man tun darf“, dann gehört es fast sicher zu UserAccount, nicht zu UserProfile.

Fehler Nr. 5: Datenverdopplung in Konto und Profil.
Zum Beispiel liegt email sowohl im Konto als auch im Profil „für alle Fälle“. Zuerst scheint es bequem, aber dann erwischst du eine Desynchronisation: Wo ist die Wahrheit? Welches Feld aktualisieren? Was in /api/me ausgeben? Versuche, dass jedes Feld einen einzigen „richtigen Ort“ hat. Für unseren Kurs: email und username leben in UserAccount, und die menschenorientierten Felder in UserProfile.

1
Aufgabe
Spring Security, Level 14, Lektion 2
Gesperrt
Sichere Antwort `/api/me`
Sichere Antwort `/api/me`
1
Aufgabe
Spring Security, Level 14, Lektion 2
Gesperrt
Profilaktualisierung ohne Kontos�nderung
Profilaktualisierung ohne Kontos�nderung
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION