Рубрики
Без рубрики

Четвертый раунд улучшений приложения Reddit

Улучшения в процессе регистрации приложения reddit, преобразование проекта в загрузочный, обработка тайм-аута сеанса и отключение пользователя. Улучшений в изобилии.

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы продолжим совершенствовать простое приложение Reddit, которое мы создаем в рамках этого публичного тематического исследования .

2. Лучшие таблицы для администратора

Во – первых, мы приведем таблицы на страницах администратора на тот же уровень, что и таблицы в пользовательском приложении, с помощью плагина jQuery DataTable.

2.1. Разбиение пользователей на страницы – уровень сервиса

Давайте добавим операцию с включенной разбиением на страницы на уровне сервиса:

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. Пользователь DTO

Далее – давайте теперь убедимся, что мы последовательно возвращаем DTO клиенту.

Нам понадобится пользователь DTO, потому что – до сих пор – API возвращал фактическую сущность User обратно клиенту:

public class UserDto {
    private Long id;

    private String username;

    private Set roles;

    private long scheduledPostsCount;
}

2.3. Разбиение пользователей на страницы – в Контроллере

Теперь давайте реализуем эту простую операцию и на уровне контроллера:

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

А вот логика преобразования DTO:

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

2.4. Интерфейс

Наконец, на стороне клиента давайте используем эту новую операцию и повторно реализуем нашу страницу пользователей администратора:

UsernameScheduled Posts CountRolesActions

3. Отключите пользователя

Далее мы создадим простую функцию администратора – возможность отключить пользователя .

Первое, что нам нужно, – это поле enabled в сущности User :

private boolean enabled;

Затем мы можем использовать это в нашей реализации User Principal , чтобы определить, включен ли принципал или нет:

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

Здесь операция API, которая имеет дело с отключением/включением пользователей:

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

И вот простая реализация уровня обслуживания:

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

4. Время Ожидания сеанса обработки

Далее, давайте настроим приложение для обработки тайм – аута сеанса – мы добавим простой SessionListener в наш контекст для управления тайм-ау сеанса :

public class SessionListener implements HttpSessionListener {

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

А вот весенняя конфигурация безопасности:

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

Примечание:

  • Мы настроили тайм-аут сеанса на 5 минут.
  • По истечении сеанса пользователь будет перенаправлен на страницу входа в систему.

5. Улучшите Регистрацию

Затем мы улучшим процесс регистрации, добавив некоторые функции, которые ранее отсутствовали.

Мы собираемся проиллюстрировать здесь только основные моменты; чтобы углубиться в регистрацию – ознакомьтесь с серией Регистрация .

5.1. Электронное письмо С Подтверждением Регистрации

Одна из этих функций, отсутствующих при регистрации, заключалась в том, что пользователи не получали повышения для подтверждения своей электронной почты.

Теперь мы заставим пользователей сначала подтвердить свой адрес электронной почты, прежде чем они будут активированы в системе:

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

Уровень обслуживания также нуждается в небольшой работе – в основном для того, чтобы убедиться, что пользователь изначально отключен:

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

Теперь для подтверждения:

@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. Инициировать сброс пароля

Теперь давайте посмотрим, как разрешить пользователям сбросить свой собственный пароль, если они его забудут:

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

Теперь уровень обслуживания просто отправит пользователю электронное письмо со ссылкой, по которой он может сбросить свой пароль:

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. Сброс Пароля

Как только пользователь нажимает на ссылку в электронном письме, он может фактически выполнить операцию сброса пароля :

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

И уровень обслуживания:

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

Наконец, вот реализация пароля обновления:

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

5.4. Изменение пароля

Далее мы собираемся реализовать аналогичную функцию – изменить ваш пароль внутри:

@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. Уведомить Проект

Затем давайте преобразуем/обновим проект до Spring Boot; сначала мы изменим pom.xml :

...

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



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

А также предоставить простое загрузочное приложение для запуска :

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

Обратите внимание, что новый базовый URL теперь будет http://localhost:8080 вместо нагрузки http://localhost:8080/reddit-scheduler .

7. Экстернализация Свойств

Теперь, когда мы загрузились, мы можем использовать @ConfigurationProperties для экстернализации наших свойств 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;
    }
    
    ...
}

Теперь мы можем четко использовать эти свойства в типобезопасном режиме:

@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. Заключение

Этот раунд улучшений был очень хорошим шагом вперед для приложения.

Мы не добавляем больше никаких основных функций, что делает архитектурные улучшения следующим логическим шагом – вот о чем эта статья.