Quatrième cycle d’améliorations de l’application Reddit

Quatrième série d'améliorations à l'application Reddit

1. Vue d'ensemble

Dans ce didacticiel, nous continuerons à améliorer l'application Reddit simple que nous développons dans le cadre dethis public case study.

2. Meilleures tables pour l'administrateur

Tout d'abord, nous allons amener les tableaux des pages d'administration au même niveau que les tableaux de l'application destinée aux utilisateurs - en utilisant le plug-in jQuery DataTable.

2.1. Faire paginer les utilisateurs - La couche de service

Ajoutons l'opération activée de pagination dans la couche de service:

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. Un DTO utilisateur

Ensuite, assurons-nous maintenant que nous retournons proprement les DTO au client de manière cohérente.

Nous allons avoir besoin d'un DTO utilisateur car, jusqu'à présent, l'API renvoyait l'entitéUser réelle au client:

public class UserDto {
    private Long id;

    private String username;

    private Set roles;

    private long scheduledPostsCount;
}

2.3. Faire paginer les utilisateurs - dans le contrôleur

Maintenant, implémentons également cette opération simple dans la couche contrôleur:

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());
}

Et voici la logique de conversion DTO:

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

2.4. L'extrémité avant

Enfin, côté client, utilisons cette nouvelle opération et ré-implémentons notre page d’administrateur:

UsernameScheduled Posts CountRolesActions

3. Désactiver un utilisateur

Ensuite, nous allons créer une fonctionnalité d'administration simple -the ability to disable a user.

La première chose dont nous avons besoin est le champenabled dans l'entitéUser:

private boolean enabled;

Ensuite, nous pouvons utiliser cela dans notre implémentationUserPrincipal pour déterminer si le principal est activé ou non:

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

Voici l'opération de l'API qui concerne la désactivation / activation des utilisateurs:

@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);
}

Et voici la mise en œuvre simple de la couche de service:

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

4. Gérer le délai d'expiration de la session

Ensuite, configurons l'applicationto handle a session timeout - nous allons ajouter un simpleSessionListener à notre contexteto control session timeout:

public class SessionListener implements HttpSessionListener {

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

Et voici la configuration de Spring Security:

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

Remarque:

  • Nous avons configuré notre délai d'expiration de session sur 5 minutes.

  • Lorsque la session expire, l'utilisateur sera redirigé vers la page de connexion.

5. Améliorer l'inscription

Ensuite, nous améliorerons le processus d'inscription en ajoutant des fonctionnalités qui manquaient auparavant.

Nous allons seulement illustrer les points principaux ici; pour approfondir l'enregistrement - consultez lesRegistration series.

5.1. Email de confirmation d'inscription

L'une de ces fonctionnalités manquantes lors de l'inscription était que les utilisateurs n'étaient pas promus pour confirmer leur adresse e-mail.

Nous allons maintenant demander aux utilisateurs de confirmer leur adresse e-mail avant de les activer dans le système:

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);
}

La couche de service nécessite également un peu de travail - en s'assurant essentiellement que l'utilisateur est désactivé initialement:

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

Maintenant pour la confirmation:

@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. Déclencher une réinitialisation du mot de passe

Voyons maintenant comment autoriser les utilisateurs à réinitialiser leur propre mot de passe au cas où ils l'oublieraient:

@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);
}

Désormais, la couche service enverra simplement un courrier électronique à l'utilisateur - avec le lien lui permettant de réinitialiser son mot de passe:

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. réinitialiser le mot de passe

Une fois que l'utilisateur clique sur le lien dans l'e-mail, il peut en faitperform 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";
}

Et la couche de service:

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;
}

Enfin, voici l'implémentation du mot de passe de mise à jour:

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

5.4. Changer le mot de passe

Ensuite, nous allons mettre en œuvre une fonctionnalité similaire: changer votre mot de passe en interne:

@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. Démarrez le projet

Ensuite, convertissons / mettons à niveau le projet vers Spring Boot; tout d'abord, nous allons modifier lespom.xml:

...

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



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

    
       org.aspectj
       aspectjweaver
     
...

Et fournissez égalementa simple Boot application for startup:

@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);
    }
}

Notez quethe new base URL sera désormaishttp://localhost:8080 au lieu des ancienshttp://localhost:8080/reddit-scheduler.

7. Externaliser les propriétés

Maintenant que nous avons Boot in, nous pouvons utiliser@ConfigurationProperties pour externaliser nos propriétés Reddit:

@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;
    }

    ...
}

Nous pouvons maintenant utiliser proprement ces propriétés de manière sécurisée:

@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. Conclusion

Cette série d’améliorations a été un très bon pas en avant pour l’application.

Nous n’ajoutons plus de fonctionnalités majeures, ce qui fait des améliorations architecturales la prochaine étape logique - c’est le sujet de cet article.