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:
Username Scheduled Posts Count Roles Actions
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.