Ich habe fast einen Monat an meiner persönlichen Website gearbeitet und bin leider beim Deployment-Bereich gescheitert. Dabei wurde ich zunehmend frustrierter, da die Zeit verging und alles perfekt auf dem localhost lief, aber ich konnte es nicht mit der Welt teilen. Die Domain, der Speicherplatz und der vServer sind nicht kostenlos, und letzteres habe ich mittlerweile auch gekündigt. Anfangs empfand ich es als persönliches Versagen, aber tatsächlich habe ich viel daraus gelernt.
Ich hatte ursprünglich eine Website erstellen wollen, die sowohl als persönlicher Blog als auch als beruflicher Lebenslauf fungieren sollte. Ich wollte unbedingt Rezepte und Beiträge zum Thema Gartenarbeit veröffentlichen, bei denen der Benutzer je nach ausgewählten Pflanzen und Zutaten saisonale Ergebnisse erhält. Da der Fokus jedoch auf dem Blog lag, war es wichtig, dass Benutzer sich registrieren und Kommentare hinterlassen, ihr Konto verwalten sowie Lieblingsrezepte und Beiträge hinzufügen können, abhängig von den Berechtigungen mit individuellem Benutzerinterface. Ich konnte alles, was ich wollte, erfolgreich integrieren. Dies geschah sicher, wobei die Spring Security einen sehr guten Dienst geleistet hat
Die Probleme begannen, als ich das fertige Projekt veröffentlichen wollte. Zunächst versuchte ich gemäß des folgenden Tutorials eine WAR-Datei auf meinen FTP-Speicher hochzuladen:
https://www.piotrnowicki.com/2012/10/creating-maven-repository-on-shared-hosting/
Das hat leider nicht funktioniert, denn wie sich später herausstellte, war es bei der Hosting-Firma nicht möglich. Schließlich habe ich einen vServer gemietet. Das ist eigentlich die perfekte Lösung, wenn man sich mit Servern auskennt. Es gelang mir, nach einigem Herumprobieren Apache Tomcat zu installieren, aber leider konnte ich die WAR-Datei trotzdem nicht ausführen:
Später lief mein containerisiertes Backend bereits, aber ich konnte das große Ganze nicht lösen (der Endpunkt war nicht von außen erreichbar), und dabei machte ich mir ständig Sorgen darüber, dass meine mangelnde Erfahrung mit Servern die Website sowieso Sicherheitsrisiken aussetzen würde.
Ein vServer ist wirklich tiefes Wasser, eine eigene Wissenschaft. Es ist wirklich ein so komplexes Thema, dass ich im Nachhinein nicht verstehe, was ich mit ungefähr einem Jahr Programmiererfahrung gedacht habe. Zusammenfassend gesagt: Ich habe die Latte hochgelegt und es nicht geschafft, darüber zu springen.
Tech Stack
Backend: Java, Maven, Spring Boot, REST API, Spring Security, Hibernate, Lombok, Java Mail Sender.
Datenbank: H2, später MariaDB – HeidiSQL
Frontend: Vue 3 Composition API, Vuetify, Pinia, Axios, Quill.
Sonstiges: GitLab, Docker
Mehr Details zum Backend
Natürlich werde ich hier nicht den gesamten Code teilen, denn ich möchte kein Buch schreiben, aber ich werde einige interessante Abschnitte herausgreifen.
In der UserEntity-Klasse habe ich alle Variablen definiert, die für einen Benutzer relevant sein könnten. Es wäre eigentlich eleganter, die für die Registrierung/Anmeldung erforderlichen Dinge in einer separaten Tabelle zu speichern, aber ich habe es alles in einem gelöst.
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "Users")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "userId")
public class UserEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false, updatable = false, unique = true)
private UUID userId;
@Column(nullable = false, unique = true)
private String username;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserRole userRole;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private LocalDate registrationDate;
private LocalDate lastLoginDate;
@Column
boolean isEnabled;
@Column
private String confirmationToken;
@Column
private LocalDate tokenExpirationDate;
@Column
private String avatarUrl = "https://valami.com/assets/alapertelmezettprofilkep.jpg";
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private Set<ArticleEntity> articles = new HashSet<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private Set<RecipeEntity> recipes = new HashSet<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private Set<FavouriteRecipeEntity> favouriteRecipes = new HashSet<>();
@Override
public Collection<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorityList = new ArrayList<>();
authorityList.add(new SimpleGrantedAuthority(userRole.toString()));
return authorityList;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.isEnabled;
}
}
Der Token wird so aktualisiert:
@Service
public class TokenRefreshService {
private final TokenService tokenService;
public TokenRefreshService(TokenService tokenService) {
this.tokenService = tokenService;
}
public String refreshTokenIfNeeded(String token) {
if (tokenService.isTokenExpired(token)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserEntity userEntity = (UserEntity) authentication.getPrincipal();
return tokenService.generateTokenWithClaims(userEntity);
}
return token;
}
}
Es ist wichtig zu beachten, dass ich zwei Arten von Tokens verwende: das JWT-Webtoken wird im Local Storage des Browsers gespeichert und enthält nach der Authentifizierung Informationen wie z.B. die Berechtigungsstufe usw. mit einer Gültigkeitsdauer von 24 Stunden. Das andere Token wird verwendet, um bei der Registrierung eine E-Mail an den Benutzer zu senden, der durch Klicken auf den Link die Registrierung bestätigen musste. Nach erfolgreicher Registrierung wurde der Wert von isEnabled auf true geändert, und der Benutzer konnte sich in sein Konto einloggen.

Die UserService-Klasse sieht folgendermaßen aus:
@Service
public class UserService {
@Autowired
UserCrudRepository userCrudRepository;
@Autowired
ConversionService conversionService;
@Autowired
TokenService tokenService;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
EmailService emailService;
@Autowired
PasswordEncoder passwordEncoder;
public AuthDTO login(LoginDTO loginDTO) {
UserEntity userEntity = getUserByUsernameOrEmail(loginDTO);
String username = userEntity.getUsername();
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, loginDTO.getPassword()));
String jwt = tokenService.generateTokenWithClaims(userEntity);
return new AuthDTO(
userEntity.getUserId(),
userEntity.getUsername(),
userEntity.getEmail(),
userEntity.getUserRole().getLabel(),
jwt);
}
public AuthDTO register(RegisterDTO registerDTO) throws UserAlreadyExistsException {
String confirmationToken = UUID.randomUUID().toString();
LocalDate tokenExpirationDate = LocalDate.now().plusDays(1); // Például 1 napos lejárati idő
UserEntity userEntity = UserEntity.builder()
.userId(UUID.randomUUID())
.username(registerDTO.getUsername())
.userRole(registerDTO.getUserRole())
.password(passwordEncoder.encode(registerDTO.getPassword()))
.email(registerDTO.getEmail())
.registrationDate(LocalDate.now())
.lastLoginDate(LocalDate.now())
.confirmationToken(confirmationToken)
.tokenExpirationDate(tokenExpirationDate)
.build();
userCrudRepository.save(userEntity);
emailService.sendSimpleEmail(
userEntity.getEmail(),
"Bestätigung der Registrierung",
"Bitte klicken Sie auf den folgenden Link, um Ihre Registrierung zu bestätigen: " +
"http://localhost:8080/api/auth/confirm?token=" + confirmationToken
);
String jwt = tokenService.generateTokenWithClaims(userEntity);
return new AuthDTO(
userEntity.getUserId(),
userEntity.getUsername(),
userEntity.getEmail(),
userEntity.getUserRole().getLabel(),
jwt
);
}
public UserEntity getUserByUsernameOrEmail(LoginDTO loginDTO) {
return getUserFromOptional(
userCrudRepository.findByUsernameOrEmail(loginDTO.getUsername(), loginDTO.getUsername())
);
}
public void confirmRegistration(String token) throws Exception {
Optional<UserEntity> userOptional = userCrudRepository.findByConfirmationToken(token);
if (userOptional.isPresent()) {
UserEntity user = userOptional.get();
// Es überprüft, ob der Benutzer bereits aktiviert wurde.
if (user.isEnabled()) {
throw new Exception("Benutzer ist schon aktiviert.");
}
// Es überprüft, ob das Token noch nicht abgelaufen ist.
if (user.getTokenExpirationDate().isBefore(LocalDate.now())) {
throw new Exception("Token ist abgelaufen.");
}
user.setEnabled(true);
user.setConfirmationToken(null); // Törli a megerősítő tokent
user.setTokenExpirationDate(null); // Törli a token lejárati dátumát
userCrudRepository.save(user);
} else {
throw new Exception("Token ist ungültig.");
}
}
public UserEntity getUserFromOptional(Optional<UserEntity> userEntityOptional) {
UserEntity userEntity;
try {
userEntity = conversionService.getEntityFromOptional(userEntityOptional);
} catch (EmptyOptionalException e) {
throw new UsernameNotFoundException("Kein entsprechender User in der Datenbank gefunden!");
}
return userEntity;
}
public ResponseUserDTO createUser(UserDTO userDTO) {
// Es wandelt das UserDTO in UserEntity um.
UserEntity userEntity = convertUserDTOToEntity(userDTO);
// Es speichert den Benutzer in der UserEntity.
UserEntity savedUser = userCrudRepository.save(userEntity);
// Es wandelt die gespeicherte UserEntity in ResponseUserDTO um.
return convertUserEntityToResponseUserDTO(savedUser);
}
public ResponseUserDTO getUserById(UUID userId) {
UserEntity userEntity = userCrudRepository.findByUserId(userId)
.orElseThrow(() -> new NotFoundException("Benutzer nicht gefunden."));
// Es wandelt die UserEntity in ResponseUserDTO um.
return convertUserEntityToResponseUserDTO(userEntity);
}
public UserEntity getUserByUsername(String username) {
return userCrudRepository.findUserByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Benutzer nicht gefunden mit Benutzernamen: " + username));
}
public List<ResponseUserDTO> getAllUsers() {
// Alle UserEntities abrufen.
List<UserEntity> userEntities = (List<UserEntity>) userCrudRepository.findAll();
// Die List<UserEntity> in eine List<ResponseUserDTO> umwandeln.
return userEntities.stream()
.map(this::convertUserEntityToResponseUserDTO)
.collect(Collectors.toList());
}
public ResponseUserDTO editUserById(UUID userId, UserDTO userDTO) {
UserEntity existingUser = userCrudRepository.findByUserId(userId)
.orElseThrow(() -> new NotFoundException("Benutzer nicht gefunden"));
// Aktualisiert den vorhandenen Benutzer - existingUser - mit dem DTO.
updateUserEntity(existingUser, userDTO);
// Speichert die aktualisierten Benutzerdaten.
UserEntity updatedUser = userCrudRepository.save(existingUser);
// Es wandelt die aktualisierte UserEntity in ResponseUserDTO um.
return convertUserEntityToResponseUserDTO(updatedUser);
}
@Transactional
public void deleteUserById(UUID userId) {
// Überprüft, ob der Benutzer existiert.
if (!userCrudRepository.existsByUserId(userId)) {
throw new NotFoundException("Benutzer nicht gefunden.");
}
// Löscht den Benutzer anhand der erhaltenen Benutzer-ID.
userCrudRepository.deleteByUserId(userId);
}
private UserEntity convertUserDTOToEntity(UserDTO userDTO) {
return UserEntity.builder()
.username(userDTO.getUsername())
.userRole(userDTO.getUserRole())
.password(passwordEncoder.encode(userDTO.getPassword()))
.email(userDTO.getEmail())
.registrationDate(userDTO.getRegistrationDate())
.lastLoginDate(userDTO.getLastLoginDate())
.build();
}
private ResponseUserDTO convertUserEntityToResponseUserDTO(UserEntity userEntity) {
return ResponseUserDTO.builder()
.userId(userEntity.getUserId())
.username(userEntity.getUsername())
.userRole(userEntity.getUserRole())
.email(userEntity.getEmail())
.registrationDate(userEntity.getRegistrationDate())
.lastLoginDate(userEntity.getLastLoginDate())
.build();
}
private void updateUserEntity(UserEntity existingUser, UserDTO userDTO) {
existingUser.setUsername(userDTO.getUsername());
existingUser.setUserRole(userDTO.getUserRole());
existingUser.setEmail(userDTO.getEmail());
existingUser.setRegistrationDate(userDTO.getRegistrationDate());
existingUser.setLastLoginDate(userDTO.getLastLoginDate());
// Es aktualisiert das Passwort, aber nur, wenn es im UserDTO angegeben ist.
if (userDTO.getPassword() != null) {
existingUser.setPassword(passwordEncoder.encode(userDTO.getPassword()));
}
}
}
Mehr Details zur Frontend
Ich mag Vue sehr, weil es mir ermöglicht, Komponenten immer wiederzuverwenden, wodurch ich viel Zeit und Energie bei der Entwicklung gespart habe. Außerdem ist es eines der leicht zu erlernenden Frameworks.
Grundsätzlich liebe ich es, mit CSS zu jonglieren. Ich nenne es einfach „Pixelbastlerei“, aber Vuetify bietet spektakuläre, geschmackvolle, klare und moderne Lösungen. Ähnlich wie bei Bootstrap ist es einfach zu verwenden und verfügt auch über eine gute Dokumentation.

Die Abfrage von Endpunkten erfolgt mit Hilfe der Axios HTTP-Client-Bibliothek. Die Installation von Axios ist übrigens ein Kinderspiel:
npm install axios
Die Antworten im JSON-Format werden mithilfe der Pinia-Statusverwaltungsbibliothek in sogenannten Stores thematisch gespeichert (AuthStore, UserStore, ArticleStore, RecipeStore usw.), ebenso wie die unmittelbar zugehörige Frontend-Logik. Dies erleichtert die einfache, aber dennoch effektive Statusverwaltung der Anwendung erheblich. Es lohnt sich, Pinia bereits am Anfang des Projekts hinzuzufügen, da Vue es standardmäßig sofort nach der Installation als Option anbietet.
Manchmal war es eine Herausforderung, sicherzustellen, dass bestimmte Komponenten nicht immer korrekt aktualisiert wurden, aber die Verwendung von Watchern, berechneten Eigenschaften und bestimmten Hooks hat schließlich alle meine Reaktivitätsprobleme gelöst.
Alles, was ich ursprünglich vorgestellt hatte, funktionierte auch auf der Frontend-Seite perfekt. Wie bereits erwähnt, lag das Problem beim Deployment. Ich habe eine Weile hart daran gearbeitet, aber schließlich entschied ich mich dafür, den vServer aufzugeben und stattdessen schnell das aktuelle WordPress auf meinem Hosting zu installieren.
So habe ich mich schließlich für WordPress entschieden
Ich möchte betonen, dass ich kein übermäßig großer Fan von WordPress bin, aber man muss zugeben, dass es unglaublich bequem ist, mit einer Vielzahl nützlicher Plugins, modernen und geschmackvollen Vorlagen, von denen die meisten auch noch kostenlos erhältlich sind. Bis zu einem gewissen Punkt ist es so einfach zu konfigurieren, dass man nicht einmal programmieren können muss, um fantastische Dinge damit zu erreichen, sondern lediglich über grundlegende Textverarbeitungsfähigkeiten verfügen muss.
Aber was als Vorteil gilt, hat auch seine Nachteile. Ich mag es, die Dinge sehr genau nach meinem Geschmack zu formen, und dafür ist WordPress oft nicht geeignet. Es lässt sich recht gut mit Kenntnissen in HTML, CSS und PHP anpassen, aber trotzdem hat es seine Grenzen. Leider fehlt die Freiheit, die ein selbst geschriebenes Backend/Frontend bietet, aber dafür ist es ein etabliertes, seit langem bewährtes, relativ stressfreies Content Management System.
Schreibe einen Kommentar