1. Обзор
В этом уроке мы продолжим совершенствовать простое приложение Reddit, которое мы создаем в рамках этого публичного тематического исследования .
2. Лучшие таблицы для администратора
Во – первых, мы приведем таблицы на страницах администратора на тот же уровень, что и таблицы в пользовательском приложении, с помощью плагина jQuery DataTable.
2.1. Разбиение пользователей на страницы – уровень сервиса
Давайте добавим операцию с включенной разбиением на страницы на уровне сервиса:
public ListgetUsersList(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 Setroles; private long scheduledPostsCount; }
2.3. Разбиение пользователей на страницы – в Контроллере
Теперь давайте реализуем эту простую операцию и на уровне контроллера:
public ListgetUsersList( @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. Интерфейс
Наконец, на стороне клиента давайте используем эту новую операцию и повторно реализуем нашу страницу пользователей администратора:
Username | Scheduled Posts Count | Roles | Actions |
---|
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. Заключение
Этот раунд улучшений был очень хорошим шагом вперед для приложения.
Мы не добавляем больше никаких основных функций, что делает архитектурные улучшения следующим логическим шагом – вот о чем эта статья.