Vierte Runde der Verbesserungen an der Reddit-Anwendung
1. Überblick
In diesem Tutorial werden wir die einfache Reddit-Anwendung, die wir als Teil vonthis public case study erstellen, weiter verbessern.
2. Bessere Tabellen für Admin
Zunächst bringen wir die Tabellen auf den Admin-Seiten mit dem jQuery DataTable-Plugin auf die gleiche Ebene wie die Tabellen in der benutzerbezogenen Anwendung.
2.1. Benutzer paginieren lassen - Die Service-Schicht
Fügen wir die paginierungsfähige Operation in die Service-Schicht ein:
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. Ein Benutzer-DTO
Weiter - Stellen wir jetzt sicher, dass wir DTOs sauber und konsistent an den Client zurückgeben.
Wir benötigen ein Benutzer-DTO, da die API bisher die tatsächliche EntitätUseran den Client zurückgegeben hat:
public class UserDto {
private Long id;
private String username;
private Set roles;
private long scheduledPostsCount;
}
2.3. Benutzer paginieren lassen - im Controller
Lassen Sie uns diese einfache Operation nun auch in der Controller-Ebene implementieren:
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());
}
Und hier ist die DTO-Konvertierungslogik:
private UserDto convertUserEntityToDto(User user) {
UserDto dto = modelMapper.map(user, UserDto.class);
dto.setScheduledPostsCount(scheduledPostService.countScheduledPostsByUser(user));
return dto;
}
2.4. Vorderes Ende
Lassen Sie uns auf der Clientseite diesen neuen Vorgang verwenden und unsere Admin-Benutzerseite erneut implementieren:
Username Scheduled Posts Count Roles Actions
3. Deaktivieren Sie einen Benutzer
Als nächstes werden wir eine einfache Admin-Funktion entwickeln -the ability to disable a user.
Das erste, was wir brauchen, ist das Feldenabledin der EntitätUser:
private boolean enabled;
Dann können wir dies in unsererUserPrincipal-Implementierung verwenden, um festzustellen, ob der Principal aktiviert ist oder nicht:
public boolean isEnabled() {
return user.isEnabled();
}
Hier die API-Operation, die sich mit dem Deaktivieren / Aktivieren von Benutzern befasst:
@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);
}
Und hier ist die einfache Implementierung der Service-Schicht:
public void setUserEnabled(Long userId, boolean enabled) {
User user = userRepository.findOne(userId);
user.setEnabled(enabled);
userRepository.save(user);
}
4. Sitzungszeitlimit behandeln
Als nächstes konfigurieren wir die Appto handle a session timeout - wir fügen unserem Kontextto control session timeout ein einfachesSessionListener hinzu:
public class SessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent event) {
event.getSession().setMaxInactiveInterval(5 * 60);
}
}
Und hier ist die Konfiguration von Spring Security:
protected void configure(HttpSecurity http) throws Exception {
http
...
.sessionManagement()
.invalidSessionUrl("/?invalidSession=true")
.sessionFixation().none();
}
Hinweis:
-
Wir haben das Sitzungszeitlimit auf 5 Minuten konfiguriert.
-
Nach Ablauf der Sitzung wird der Benutzer zur Anmeldeseite umgeleitet.
5. Registrierung verbessern
Als Nächstes verbessern wir den Registrierungsfluss, indem wir einige Funktionen hinzufügen, die zuvor fehlten.
Wir werden hier nur die wichtigsten Punkte veranschaulichen. Um tief in die Registrierung einzusteigen, überprüfen Sie dieRegistration series.
5.1. Registrierungsbestätigungs-E-Mail
Eine dieser Funktionen, die bei der Registrierung fehlten, war, dass Benutzer nicht zur Bestätigung ihrer E-Mail befördert wurden.
Wir lassen Benutzer jetzt zuerst ihre E-Mail-Adresse bestätigen, bevor sie im System aktiviert werden:
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);
}
Die Service-Schicht muss auch ein wenig arbeiten - im Grunde stellen Sie sicher, dass der Benutzer anfangs deaktiviert ist:
@Override
public void registerNewUser(String username, String email, String password, String appUrl) {
...
user.setEnabled(false);
userRepository.save(user);
eventPublisher.publishEvent(new OnRegistrationCompleteEvent(user, appUrl));
}
Nun zur Bestätigung:
@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. Auslösen eines Passwort-Resets
Lassen Sie uns nun sehen, wie Benutzer ihr eigenes Kennwort zurücksetzen können, falls sie es vergessen:
@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);
}
Jetzt sendet der Service-Layer einfach eine E-Mail an den Benutzer - mit dem Link, über den er sein Passwort zurücksetzen kann:
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. Passwort zurücksetzen
Sobald der Benutzer auf den Link in der E-Mail klickt, kann er tatsächlichperform 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";
}
Und die Serviceschicht:
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;
}
Zum Schluss noch die Implementierung des Update-Passworts:
@RequestMapping(value = "/users/updatePassword", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void changeUserPassword(@RequestParam("password") String password) {
userService.changeUserPassword(userService.getCurrentUser(), password);
}
5.4. Passwort ändern
Als Nächstes implementieren wir eine ähnliche Funktionalität: Ändern Sie Ihr Passwort intern:
@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. Starten Sie das Projekt
Als Nächstes konvertieren / aktualisieren wir das Projekt auf Spring Boot. Zuerst werden wir diepom.xml modifizieren:
...
org.springframework.boot
spring-boot-starter-parent
1.2.5.RELEASE
org.springframework.boot
spring-boot-starter-web
org.aspectj
aspectjweaver
...
Und geben Sie aucha simple Boot application for startup an:
@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);
}
}
Beachten Sie, dassthe new base URL jetzthttp://localhost:8080 anstelle der altenhttp://localhost:8080/reddit-scheduler ist.
7. Eigenschaften externalisieren
Nachdem wir Boot in haben, können wir@ConfigurationProperties verwenden, um unsere Reddit-Eigenschaften zu externalisieren:
@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;
}
...
}
Wir können diese Eigenschaften jetzt sauber und typsicher nutzen:
@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. Fazit
Diese Runde der Verbesserungen war ein sehr guter Schritt vorwärts für die Anwendung.
Wir fügen keine weiteren wichtigen Funktionen hinzu, sodass Architekturverbesserungen der nächste logische Schritt sind. Darum geht es in diesem Artikel.