Quarta Rodada de Melhorias no Aplicativo Reddit
1. Visão geral
Neste tutorial, continuaremos aprimorando o aplicativo Reddit simples que estamos construindo como parte dethis public case study.
2. Melhores tabelas para administrador
Primeiro, vamos trazer as tabelas nas páginas Admin para o mesmo nível que as tabelas no aplicativo voltado para o usuário - usando o plugin jQuery DataTable.
2.1. Faça com que os usuários sejam paginados - a camada de serviço
Vamos adicionar a operação ativada de paginação na camada de serviço:
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. Um usuário DTO
A seguir - agora vamos nos certificar de que estamos retornando DTOs de forma limpa para o cliente de forma consistente.
Vamos precisar de um DTO de usuário porque - até agora - a API estava retornando a entidadeUser real de volta para o cliente:
public class UserDto {
private Long id;
private String username;
private Set roles;
private long scheduledPostsCount;
}
2.3. Obtenha a paginação dos usuários - no controlador
Agora, vamos implementar esta operação simples na camada do controlador também:
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());
}
E aqui está a lógica de conversão DTO:
private UserDto convertUserEntityToDto(User user) {
UserDto dto = modelMapper.map(user, UserDto.class);
dto.setScheduledPostsCount(scheduledPostService.countScheduledPostsByUser(user));
return dto;
}
2.4. A parte dianteira
Por fim, do lado do cliente, vamos usar essa nova operação e reimplementar nossa página de usuários administradores:
Username Scheduled Posts Count Roles Actions
3. Desabilitar um usuário
Em seguida, vamos construir um recurso de administração simples -the ability to disable a user.
A primeira coisa de que precisamos é o campoenabled na entidadeUser:
private boolean enabled;
Então, podemos usar isso em nossa implementaçãoUserPrincipal para determinar se o principal está habilitado ou não:
public boolean isEnabled() {
return user.isEnabled();
}
Aqui a operação da API que lida com a desativação / ativação de usuários:
@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);
}
E aqui está a implementação simples da camada de serviço:
public void setUserEnabled(Long userId, boolean enabled) {
User user = userRepository.findOne(userId);
user.setEnabled(enabled);
userRepository.save(user);
}
4. Lidar com o tempo limite da sessão
A seguir, vamos configurar o aplicativoto handle a session timeout - adicionaremos umSessionListener simples ao nosso contextoto control session timeout:
public class SessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent event) {
event.getSession().setMaxInactiveInterval(5 * 60);
}
}
E aqui está a configuração do Spring Security:
protected void configure(HttpSecurity http) throws Exception {
http
...
.sessionManagement()
.invalidSessionUrl("/?invalidSession=true")
.sessionFixation().none();
}
Nota:
-
Configuramos o tempo limite da nossa sessão para 5 minutos.
-
Quando a sessão expirar, o usuário será redirecionado para a página de login.
5. Melhorar o registro
A seguir, vamos aprimorar o fluxo de registro adicionando algumas funcionalidades que antes estavam faltando.
Vamos apenas ilustrar os pontos principais aqui; para se aprofundar no registro - verifique oRegistration series.
5.1. Email de confirmação de registro
Um desses recursos ausentes no registro era que os usuários não eram promovidos para confirmar seu e-mail.
Agora, faremos os usuários confirmarem seus endereços de e-mail antes de serem ativados no sistema:
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);
}
A camada de serviço também precisa de um pouco de trabalho - basicamente, certificando-se de que o usuário seja desativado inicialmente:
@Override
public void registerNewUser(String username, String email, String password, String appUrl) {
...
user.setEnabled(false);
userRepository.save(user);
eventPublisher.publishEvent(new OnRegistrationCompleteEvent(user, appUrl));
}
Agora para a confirmação:
@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. Acionar uma redefinição de senha
Agora, vamos ver como permitir que os usuários redefinam suas próprias senhas caso a esqueçam:
@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);
}
Agora, a camada de serviço simplesmente envia um email ao usuário - com o link onde ele pode redefinir sua senha:
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. Redefinir senha
Depois que o usuário clica no link do e-mail, ele podeperform 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";
}
E a camada de serviço:
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;
}
Finalmente, aqui está a implementação de atualização de senha:
@RequestMapping(value = "/users/updatePassword", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void changeUserPassword(@RequestParam("password") String password) {
userService.changeUserPassword(userService.getCurrentUser(), password);
}
5.4. Mudar senha
Em seguida, vamos implementar uma funcionalidade semelhante - alterar sua senha internamente:
@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. Bootify the Project
Em seguida, vamos converter / atualizar o projeto para Spring Boot; primeiro, vamos modificar opom.xml:
...
org.springframework.boot
spring-boot-starter-parent
1.2.5.RELEASE
org.springframework.boot
spring-boot-starter-web
org.aspectj
aspectjweaver
...
E também forneçaa 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);
}
}
Observe quethe new base URL agora seráhttp://localhost:8080 em vez do antigohttp://localhost:8080/reddit-scheduler.
7. Exteriorizar propriedades
Agora que temos a inicialização, podemos usar@ConfigurationProperties para externalizar nossas propriedades do 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;
}
...
}
Agora, podemos usar essas propriedades de maneira limpa e de maneira segura:
@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. Conclusão
Essa rodada de melhorias foi um passo muito bom para o aplicativo.
Não estamos adicionando mais nenhum recurso importante, o que torna as melhorias arquitetônicas a próxima etapa lógica - é disso que trata este artigo.