1. Обзор
В этом руководстве мы продолжим улучшать простое приложение Reddit, которое мы создаем в рамках этого общедоступного тематического исследования .
2. Улучшенные таблицы для администратора
Во-первых, мы приведем таблицы на страницах администрирования к тому же уровню, что и таблицы в пользовательском приложении — с помощью подключаемого модуля jQuery DataTable.
2.1. Разбивка пользователей на страницы — сервисный уровень
Давайте добавим операцию разбиения на страницы на сервисном уровне:
public List<User> 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 клиенту.
Нам понадобится User DTO, потому что до сих пор API возвращал фактическую сущность пользователя
обратно клиенту:
public class UserDto {
private Long id;
private String username;
private Set<Role> roles;
private long scheduledPostsCount;
}
2.3. Разбить пользователей на страницы — в контроллере
Теперь давайте реализуем эту простую операцию и на уровне контроллера:
public List<UserDto> 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<User> 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. Внешний интерфейс
Наконец, на стороне клиента давайте воспользуемся этой новой операцией и повторно реализуем нашу страницу пользователей-администраторов:
<table><thead><tr>
<th>Username</th><th>Scheduled Posts Count</th><th>Roles</th><th>Actions</th>
</tr></thead></table>
<script>
$(function(){
$('table').dataTable( {
"processing": true,
"searching":false,
"columnDefs": [
{ "name": "username", "targets": 0},
{ "name": "scheduledPostsCount", "targets": 1,"orderable": false},
{ "targets": 2, "data": "roles", "width":"20%", "orderable": false,
"render":
function ( data, type, full, meta ) { return extractRolesName(data); } },
{ "targets": 3, "data": "id", "render": function ( data, type, full, meta ) {
return '<a onclick="showEditModal('+data+',\'' +
extractRolesName(full.roles)+'\')">Modify User Roles</a>'; }}
],
"columns": [
{ "data": "username" },
{ "data": "scheduledPostsCount" }
],
"serverSide": true,
"ajax": function(data, callback, settings) {
$.get('admin/users', {
size: data.length,
page: (data.start/data.length),
sortDir: data.order[0].dir,
sort: data.columns[data.order[0].column].name
}, function(res,textStatus, request) {
var pagingInfo = request.getResponseHeader('PAGING_INFO');
var total = pagingInfo.split(",")[0].split("=")[1];
callback({
recordsTotal: total,recordsFiltered: total,data: res
});});
}
});});
</script>
3. Отключить пользователя
Далее мы создадим простую функцию администратора — возможность отключить пользователя .
Первое, что нам нужно, это поле enable
в сущности User :
private boolean enabled;
Затем мы можем использовать это в нашей реализации UserPrincipal
, чтобы определить, включен ли субъект или нет:
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);
}
}
А вот конфигурация Spring Security:
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
:
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
...
А также предоставьте простое загрузочное приложение для запуска :
@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. Экстернализация свойств
Теперь, когда у нас есть Boot, мы можем использовать @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. Заключение
Этот раунд улучшений стал очень хорошим шагом вперед для приложения.
Мы больше не добавляем каких-либо важных функций, что делает архитектурные улучшения следующим логическим шагом — именно об этом эта статья.