Vierte Verbesserungsrunde der Reddit-Anwendung

Vierte Runde der Verbesserungen an der Reddit-Anwendung

1. Überblick

In diesem Tutorial werden wir die einfache Reddit-Anwendung, die wir als Teil vonthis public case study erstellen, weiter verbessern.

2. Bessere Tabellen für Admin

Zunächst bringen wir die Tabellen auf den Admin-Seiten mit dem jQuery DataTable-Plugin auf die gleiche Ebene wie die Tabellen in der benutzerbezogenen Anwendung.

2.1. Benutzer paginieren lassen - Die Service-Schicht

Fügen wir die paginierungsfähige Operation in die Service-Schicht ein:

public List getUsersList(int page, int size, String sortDir, String sort) {
    PageRequest pageReq = new PageRequest(page, size, Sort.Direction.fromString(sortDir), sort);
    return userRepository.findAll(pageReq).getContent();
}
public PagingInfo generatePagingInfo(int page, int size) {
    return new PagingInfo(page, size, userRepository.count());
}

2.2. Ein Benutzer-DTO

Weiter - Stellen wir jetzt sicher, dass wir DTOs sauber und konsistent an den Client zurückgeben.

Wir benötigen ein Benutzer-DTO, da die API bisher die tatsächliche EntitätUseran den Client zurückgegeben hat:

public class UserDto {
    private Long id;

    private String username;

    private Set roles;

    private long scheduledPostsCount;
}

2.3. Benutzer paginieren lassen - im Controller

Lassen Sie uns diese einfache Operation nun auch in der Controller-Ebene implementieren:

public List getUsersList(
  @RequestParam(value = "page", required = false, defaultValue = "0") int page,
  @RequestParam(value = "size", required = false, defaultValue = "10") int size,
  @RequestParam(value = "sortDir", required = false, defaultValue = "asc") String sortDir,
  @RequestParam(value = "sort", required = false, defaultValue = "username") String sort,
  HttpServletResponse response) {
    response.addHeader("PAGING_INFO", userService.generatePagingInfo(page, size).toString());
    List users = userService.getUsersList(page, size, sortDir, sort);

    return users.stream().map(
      user -> convertUserEntityToDto(user)).collect(Collectors.toList());
}

Und hier ist die DTO-Konvertierungslogik:

private UserDto convertUserEntityToDto(User user) {
    UserDto dto = modelMapper.map(user, UserDto.class);
    dto.setScheduledPostsCount(scheduledPostService.countScheduledPostsByUser(user));
    return dto;
}

2.4. Vorderes Ende

Lassen Sie uns auf der Clientseite diesen neuen Vorgang verwenden und unsere Admin-Benutzerseite erneut implementieren:

UsernameScheduled Posts CountRolesActions

3. Deaktivieren Sie einen Benutzer

Als nächstes werden wir eine einfache Admin-Funktion entwickeln -the ability to disable a user.

Das erste, was wir brauchen, ist das Feldenabledin der EntitätUser:

private boolean enabled;

Dann können wir dies in unsererUserPrincipal-Implementierung verwenden, um festzustellen, ob der Principal aktiviert ist oder nicht:

public boolean isEnabled() {
    return user.isEnabled();
}

Hier die API-Operation, die sich mit dem Deaktivieren / Aktivieren von Benutzern befasst:

@PreAuthorize("hasRole('USER_WRITE_PRIVILEGE')")
@RequestMapping(value = "/users/{id}", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
public void setUserEnabled(@PathVariable("id") Long id,
  @RequestParam(value = "enabled") boolean enabled) {
    userService.setUserEnabled(id, enabled);
}

Und hier ist die einfache Implementierung der Service-Schicht:

public void setUserEnabled(Long userId, boolean enabled) {
    User user = userRepository.findOne(userId);
    user.setEnabled(enabled);
    userRepository.save(user);
}

4. Sitzungszeitlimit behandeln

Als nächstes konfigurieren wir die Appto handle a session timeout - wir fügen unserem Kontextto control session timeout ein einfachesSessionListener hinzu:

public class SessionListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        event.getSession().setMaxInactiveInterval(5 * 60);
    }
}

Und hier ist die Konfiguration von Spring Security:

protected void configure(HttpSecurity http) throws Exception {
    http
    ...
        .sessionManagement()
        .invalidSessionUrl("/?invalidSession=true")
        .sessionFixation().none();
}

Hinweis:

  • Wir haben das Sitzungszeitlimit auf 5 Minuten konfiguriert.

  • Nach Ablauf der Sitzung wird der Benutzer zur Anmeldeseite umgeleitet.

5. Registrierung verbessern

Als Nächstes verbessern wir den Registrierungsfluss, indem wir einige Funktionen hinzufügen, die zuvor fehlten.

Wir werden hier nur die wichtigsten Punkte veranschaulichen. Um tief in die Registrierung einzusteigen, überprüfen Sie dieRegistration series.

5.1. Registrierungsbestätigungs-E-Mail

Eine dieser Funktionen, die bei der Registrierung fehlten, war, dass Benutzer nicht zur Bestätigung ihrer E-Mail befördert wurden.

Wir lassen Benutzer jetzt zuerst ihre E-Mail-Adresse bestätigen, bevor sie im System aktiviert werden:

public void register(HttpServletRequest request,
  @RequestParam("username") String username,
  @RequestParam("email") String email,
  @RequestParam("password") String password) {
    String appUrl =
      "http://" + request.getServerName() + ":" +
       request.getServerPort() + request.getContextPath();
    userService.registerNewUser(username, email, password, appUrl);
}

Die Service-Schicht muss auch ein wenig arbeiten - im Grunde stellen Sie sicher, dass der Benutzer anfangs deaktiviert ist:

@Override
public void registerNewUser(String username, String email, String password, String appUrl) {
    ...
    user.setEnabled(false);
    userRepository.save(user);
    eventPublisher.publishEvent(new OnRegistrationCompleteEvent(user, appUrl));
}

Nun zur Bestätigung:

@RequestMapping(value = "/user/regitrationConfirm", method = RequestMethod.GET)
public String confirmRegistration(Model model, @RequestParam("token") String token) {
    String result = userService.confirmRegistration(token);
    if (result == null) {
        return "redirect:/?msg=registration confirmed successfully";
    }
    model.addAttribute("msg", result);
    return "submissionResponse";
}
public String confirmRegistration(String token) {
    VerificationToken verificationToken = tokenRepository.findByToken(token);
    if (verificationToken == null) {
        return "Invalid Token";
    }

    Calendar cal = Calendar.getInstance();
    if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
        return "Token Expired";
    }

    User user = verificationToken.getUser();
    user.setEnabled(true);
    userRepository.save(user);
    return null;
}

5.2. Auslösen eines Passwort-Resets

Lassen Sie uns nun sehen, wie Benutzer ihr eigenes Kennwort zurücksetzen können, falls sie es vergessen:

@RequestMapping(value = "/users/passwordReset", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void passwordReset(HttpServletRequest request, @RequestParam("email") String email) {
    String appUrl = "http://" + request.getServerName() + ":" +
      request.getServerPort() + request.getContextPath();
    userService.resetPassword(email, appUrl);
}

Jetzt sendet der Service-Layer einfach eine E-Mail an den Benutzer - mit dem Link, über den er sein Passwort zurücksetzen kann:

public void resetPassword(String userEmail, String appUrl) {
    Preference preference = preferenceRepository.findByEmail(userEmail);
    User user = userRepository.findByPreference(preference);
    if (user == null) {
        throw new UserNotFoundException("User not found");
    }

    String token = UUID.randomUUID().toString();
    PasswordResetToken myToken = new PasswordResetToken(token, user);
    passwordResetTokenRepository.save(myToken);
    SimpleMailMessage email = constructResetTokenEmail(appUrl, token, user);
    mailSender.send(email);
}

5.3. Passwort zurücksetzen

Sobald der Benutzer auf den Link in der E-Mail klickt, kann er tatsächlichperform the reset password operation:

@RequestMapping(value = "/users/resetPassword", method = RequestMethod.GET)
public String resetPassword(
  Model model,
  @RequestParam("id") long id,
  @RequestParam("token") String token) {
    String result = userService.checkPasswordResetToken(id, token);
    if (result == null) {
        return "updatePassword";
    }
    model.addAttribute("msg", result);
    return "submissionResponse";
}

Und die Serviceschicht:

public String checkPasswordResetToken(long userId, String token) {
    PasswordResetToken passToken = passwordResetTokenRepository.findByToken(token);
    if ((passToken == null) || (passToken.getUser().getId() != userId)) {
        return "Invalid Token";
    }

    Calendar cal = Calendar.getInstance();
    if ((passToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
        return "Token Expired";
    }

    UserPrincipal userPrincipal = new UserPrincipal(passToken.getUser());
    Authentication auth = new UsernamePasswordAuthenticationToken(
      userPrincipal, null, userPrincipal.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(auth);
    return null;
}

Zum Schluss noch die Implementierung des Update-Passworts:

@RequestMapping(value = "/users/updatePassword", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void changeUserPassword(@RequestParam("password") String password) {
    userService.changeUserPassword(userService.getCurrentUser(), password);
}

5.4. Passwort ändern

Als Nächstes implementieren wir eine ähnliche Funktionalität: Ändern Sie Ihr Passwort intern:

@RequestMapping(value = "/users/changePassword", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void changeUserPassword(@RequestParam("password") String password,
  @RequestParam("oldpassword") String oldPassword) {
    User user = userService.getCurrentUser();
    if (!userService.checkIfValidOldPassword(user, oldPassword)) {
        throw new InvalidOldPasswordException("Invalid old password");
    }
    userService.changeUserPassword(user, password);
}
public void changeUserPassword(User user, String password) {
    user.setPassword(passwordEncoder.encode(password));
    userRepository.save(user);
}

6. Starten Sie das Projekt

Als Nächstes konvertieren / aktualisieren wir das Projekt auf Spring Boot. Zuerst werden wir diepom.xml modifizieren:

...

    org.springframework.boot
    spring-boot-starter-parent
    1.2.5.RELEASE



    
        org.springframework.boot
        spring-boot-starter-web
    

    
       org.aspectj
       aspectjweaver
     
...

Und geben Sie aucha simple Boot application for startup an:

@SpringBootApplication
public class Application {

    @Bean
    public SessionListener sessionListener() {
        return new SessionListener();
    }

    @Bean
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }

    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

Beachten Sie, dassthe new base URL jetzthttp://localhost:8080 anstelle der altenhttp://localhost:8080/reddit-scheduler ist.

7. Eigenschaften externalisieren

Nachdem wir Boot in haben, können wir@ConfigurationProperties verwenden, um unsere Reddit-Eigenschaften zu externalisieren:

@ConfigurationProperties(prefix = "reddit")
@Component
public class RedditProperties {

    private String clientID;
    private String clientSecret;
    private String accessTokenUri;
    private String userAuthorizationUri;
    private String redirectUri;

    public String getClientID() {
        return clientID;
    }

    ...
}

Wir können diese Eigenschaften jetzt sauber und typsicher nutzen:

@Autowired
private RedditProperties redditProperties;

@Bean
public OAuth2ProtectedResourceDetails reddit() {
    AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
    details.setClientId(redditProperties.getClientID());
    details.setClientSecret(redditProperties.getClientSecret());
    details.setAccessTokenUri(redditProperties.getAccessTokenUri());
    details.setUserAuthorizationUri(redditProperties.getUserAuthorizationUri());
    details.setPreEstablishedRedirectUri(redditProperties.getRedirectUri());
    ...
    return details;
}

8. Fazit

Diese Runde der Verbesserungen war ein sehr guter Schritt vorwärts für die Anwendung.

Wir fügen keine weiteren wichtigen Funktionen hinzu, sodass Architekturverbesserungen der nächste logische Schritt sind. Darum geht es in diesem Artikel.