知識庫 / Spring RSS 訂閱

Reddit應用第四次改進

REST,Spring
HongKong
4
03:59 AM · Dec 06 ,2025

1. 概述

在本教程中,我們將繼續改進我們正在構建的簡單 Reddit 應用,該應用是作為本公共案例研究的一部分構建的。

2. 管理頁面更優秀的表格

首先,我們將管理頁面中的表格與用户界面應用程序中的表格統一到同一水平——通過使用 jQuery DataTables 插件。

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 返回給客户端。

我們需要一個用户 DTO,因為 – 之前,API 會將實際的 User 實體返回給客户端:

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. 禁用用户

接下來,我們將構建一個簡單的管理功能——禁用用户

首先我們需要在 用户實體中添加 已啓用字段:

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>
...

同時,還提供一個簡單的 Boot 應用用於啓動:

@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. 結論

本輪改進對應用程序來説是一個重要的進步。

我們不會再添加任何主要功能,這意味着架構改進是下一步的自然發展方向——而本文正是圍繞這一點的。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.