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

Шестой раунд улучшений приложения Reddit

Завершение Reddit приложение с быстрым раундом улучшений.

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

1. Обзор

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

2. Командная безопасность API

Во-первых, мы собираемся сделать некоторую работу по обеспечению безопасности команды API, чтобы предотвратить манипулирование ресурсами пользователями, кроме владельца.

2.1. Конфигурация

Начнем с того, что мы будем использовать @Preauthorize в конфигурации:

@EnableGlobalMethodSecurity(prePostEnabled = true)

2.2. Авторизовать команды

Далее, давайте авторизовать наши команды в слое контроллера с помощью некоторых выражений весенней безопасности:

@PreAuthorize("@resourceSecurityService.isPostOwner(#postDto.id)")
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
public void updatePost(@RequestBody ScheduledPostUpdateCommandDto postDto) {
    ...
}

@PreAuthorize("@resourceSecurityService.isPostOwner(#id)")
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deletePost(@PathVariable("id") Long id) {
    ...
}
@PreAuthorize("@resourceSecurityService.isRssFeedOwner(#feedDto.id)")
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
public void updateFeed(@RequestBody FeedUpdateCommandDto feedDto) {
    ..
}

@PreAuthorize("@resourceSecurityService.isRssFeedOwner(#id)")
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteFeed(@PathVariable("id") Long id) {
    ...
}

Обратите внимание, что:

  • Мы используем “К” для доступа к аргументу метода – как мы это делали в #id
  • Мы используем “Я” для доступа к фасоли – как мы это делали в @resourceSecurityService

2.3. Служба безопасности ресурсов

Вот как выглядит служба, ответственная за проверку собственности:

@Service
public class ResourceSecurityService {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private MyFeedRepository feedRepository;

    public boolean isPostOwner(Long postId) {
        UserPrincipal userPrincipal = (UserPrincipal) 
          SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user = userPrincipal.getUser();
        Post post = postRepository.findOne(postId);
        return post.getUser().getId() == user.getId();
    }

    public boolean isRssFeedOwner(Long feedId) {
        UserPrincipal userPrincipal = (UserPrincipal) 
          SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user = userPrincipal.getUser();
        MyFeed feed = feedRepository.findOne(feedId);
        return feed.getUser().getId() == user.getId();
    }
}

Обратите внимание, что:

  • isPostOwner () : проверьте, является ли текущий пользователь владельцем Почтовые с данной postId
  • isRssFeedВладелец () : проверьте, является ли текущий пользователь владельцем MyFeed с данной feedId

2.4. Обработка исключений

Далее мы просто разберемся с AccessDeniedException – следующим образом:

@ExceptionHandler({ AuthenticationCredentialsNotFoundException.class, AccessDeniedException.class })
public ResponseEntity handleAccessDeniedException(final Exception ex, final WebRequest request) {
    logger.error("403 Status Code", ex);
    ApiError apiError = new ApiError(HttpStatus.FORBIDDEN, ex);
    return new ResponseEntity(apiError, new HttpHeaders(), HttpStatus.FORBIDDEN);
}

2.5. Тест авторизации

Наконец, мы будем тестировать нашу команду авторизации:

public class CommandAuthorizationLiveTest extends ScheduledPostLiveTest {

    @Test
    public void givenPostOwner_whenUpdatingScheduledPost_thenUpdated() throws ParseException, IOException {
        ScheduledPostDto post = newDto();
        post.setTitle("new title");
        Response response = withRequestBody(givenAuth(), post).put(urlPrefix + "/api/scheduledPosts/" + post.getId());

        assertEquals(200, response.statusCode());
    }

    @Test
    public void givenUserOtherThanOwner_whenUpdatingScheduledPost_thenForbidden() throws ParseException, IOException {
        ScheduledPostDto post = newDto();
        post.setTitle("new title");
        Response response = withRequestBody(givenAnotherUserAuth(), post).put(urlPrefix + "/api/scheduledPosts/" + post.getId());

        assertEquals(403, response.statusCode());
    }

    private RequestSpecification givenAnotherUserAuth() {
        FormAuthConfig formConfig = new FormAuthConfig(
          urlPrefix + "/j_spring_security_check", "username", "password");
        return RestAssured.given().auth().form("test", "test", formConfig);
    }
}

Обратите внимание, как givenAuth () реализация использует пользователя “джон”, в то время как даноAnotherUserAuth() использует пользователь “тест” – так что мы можем проверить эти сложные сценарии с участием двух разных пользователей.

3. Больше вариантов повторного висята

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

Начнем с изменения запланированных вариантов повторного размещения и разделим времяИнтервал . Раньше это было две отдельные обязанности; Это было:

  • время между представлением поста и время проверки оценки и
  • время между проверкой баллов и следующим временем представления

Мы не будем отделять эти две обязанности: checkAfterInterval и представитьПосежденоинтервал .

3.1. Почтовое образование

Мы изменим как сущности Почты, так и Предпочтения, удалив:

private int timeInterval;

И добавив:

private int checkAfterInterval;

private int submitAfterInterval;

Обратите внимание, что мы сделаем то же самое для связанных DTOs.

3.2. Планировщик

Далее мы изменим наш планировщик, чтобы использовать новые интервалы времени – следующим образом:

private void checkAndReSubmitInternal(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getCheckAfterInterval())) {
        PostScores postScores = getPostScores(post);
        ...
}

private void checkAndDeleteInternal(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getCheckAfterInterval())) {
        PostScores postScores = getPostScores(post);
        ...
}

private void resetPost(Post post, String failReason) {
    long time = new Date().getTime();
    time += TimeUnit.MILLISECONDS.convert(post.getSubmitAfterInterval(), TimeUnit.MINUTES);
    post.setSubmissionDate(new Date(time))
    ...
}

Обратите внимание, что для запланированного поста с представлениеDate T и checkAfterInterval t1 и представитьПосежденоинтервал t2 и количество попыток > 1, мы будем иметь:

  1. Почта представлена в первый раз на T
  2. Планировщик проверяет почтовый балл в ТЗТ1
  3. Предполагая, что должность не достигла цели оценка, должность представлена во второй раз на Тзт1’т2

4. Дополнительные проверки токена доступа OAuth2

Далее мы добавим некоторые дополнительные проверки, работающие с токеном доступа.

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

4.1. Контроллер Reddit

Вот простая проверка уровня контроллера – isAccessTokenValid () :

@RequestMapping(value = "/isAccessTokenValid")
@ResponseBody
public boolean isAccessTokenValid() {
    return redditService.isCurrentUserAccessTokenValid();
}

4.2. Сервис Reddit

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

@Override
public boolean isCurrentUserAccessTokenValid() {
    UserPrincipal userPrincipal = (UserPrincipal) 
      SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    User currentUser = userPrincipal.getUser();
    if (currentUser.getAccessToken() == null) {
        return false;
    }
    try {
        redditTemplate.needsCaptcha();
    } catch (Exception e) {
        redditTemplate.setAccessToken(null);
        currentUser.setAccessToken(null);
        currentUser.setRefreshToken(null);
        currentUser.setTokenExpiration(null);
        userRepository.save(currentUser);
        return false;
    }
    return true;
}

То, что здесь происходит, довольно просто. Если у пользователя уже есть токен доступа, мы постараемся связаться с API Reddit с помощью простого нуждаетсяCaptcha звать.

Если вызов не удается, то текущий маркер является недействительным – так что мы сбросим его. И, конечно, это приводит к тому, что пользователю будет предложено подключить свою учетную запись к Reddit.

4.3. Фронт-энд

Наконец, мы покажем это на главной странице:



Обратите внимание, как, если токен доступа недействителен, пользователю будет показана ссылка “Подключение к Reddit”.

5. Разделение на несколько модулей

Далее мы разделим приложение на модули. Мы пойдем с 4 модулями: Reddit-общий , Reddit-отдых , Reddit-ui и Reddit-веб- .

5.1. Родитель

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

Родительский модуль Reddit-планировщик содержит подмодули и простую пом.xml – следующим образом:


    4.0.0
    org.baeldung
    reddit-scheduler
    0.2.0-SNAPSHOT
    reddit-scheduler
    pom

    
        org.springframework.boot
        spring-boot-starter-parent
        1.2.7.RELEASE
    
        
    
        reddit-common
        reddit-rest
        reddit-ui
        reddit-web
    

    
        
    

Все свойства и версии зависимостей будут объявлены здесь, в родительском пом.xml – для использования всеми подмодуми.

5.2. Общий модуль

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

Классы конфигурации, включенные в этот модуль, ОбщиеКонфиг , PersistenceJpaConfig, RedditConfig , СервисКонфиг , WebGeneralConfig .

Вот простая пом.xml :


    4.0.0
    reddit-common
    reddit-common
    jar

    
        org.baeldung
        reddit-scheduler
        0.2.0-SNAPSHOT
    

5.3. Модуль REST

Наша Reddit-отдых модуль содержит контроллеры REST и DTOs.

Единственным классом конфигурации в этом модуле является WebApiConfig .

Вот пом.xml :


    4.0.0
    reddit-rest
    reddit-rest
    jar

    
        org.baeldung
        reddit-scheduler
        0.2.0-SNAPSHOT
    

    
        
            org.baeldung
            reddit-common
            0.2.0-SNAPSHOT
        
    
    ...

Этот модуль также содержит логику обработки исключений.

5.4. Модуль пользовательского интерфейса

Reddit-ui модуль содержит передние и MVC контроллеры.

Включенные классы конфигурации WebFrontendConfig и ТимелифКонфиг .

Нам нужно изменить конфигурацию Thymeleaf, чтобы загрузить шаблоны из классов ресурсов вместо контекста сервера:

@Bean
public TemplateResolver templateResolver() {
    SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
    templateResolver.setPrefix("classpath:/");
    templateResolver.setSuffix(".html");
    templateResolver.setCacheable(false);
    return templateResolver;
}

Вот простая пом.xml :


    4.0.0
    reddit-ui
    reddit-ui
    jar

    
        org.baeldung
        reddit-scheduler
        0.2.0-SNAPSHOT
    

    
        
            org.baeldung
            reddit-common
            0.2.0-SNAPSHOT
        
    
...

Теперь у нас есть более простой обработчик исключений здесь также, для обработки передних исключений:

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler implements Serializable {

    private static final long serialVersionUID = -3365045939814599316L;

    @ExceptionHandler({ UserApprovalRequiredException.class, UserRedirectRequiredException.class })
    public String handleRedirect(RuntimeException ex, WebRequest request) {
        logger.info(ex.getLocalizedMessage());
        throw ex;
    }

    @ExceptionHandler({ Exception.class })
    public String handleInternal(RuntimeException ex, WebRequest request) {
        logger.error(ex);
        String response = "Error Occurred: " + ex.getMessage();
        return "redirect:/submissionResponse?msg=" + response;
    }
}

5.5. Веб-модуль

Наконец, вот наш Reddit-веб-модуль.

Этот модуль содержит ресурсы, конфигурацию безопасности и СпрингБутАппликация конфигурация — следующим образом:

@SpringBootApplication
public class Application extends SpringBootServletInitializer {
    @Bean
    public ServletRegistrationBean frontendServlet() {
        AnnotationConfigWebApplicationContext dispatcherContext = 
          new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(WebFrontendConfig.class, ThymeleafConfig.class);
        ServletRegistrationBean registration = new ServletRegistrationBean(
          new DispatcherServlet(dispatcherContext), "/*");
        registration.setName("FrontendServlet");
        registration.setLoadOnStartup(1);
        return registration;
    }

    @Bean
    public ServletRegistrationBean apiServlet() {
        AnnotationConfigWebApplicationContext dispatcherContext = 
          new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(WebApiConfig.class);
        ServletRegistrationBean registration = new ServletRegistrationBean(
          new DispatcherServlet(dispatcherContext), "/api/*");
        registration.setName("ApiServlet");
        registration.setLoadOnStartup(2);
        return registration;
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        application.sources(Application.class, CommonConfig.class, 
          PersistenceJpaConfig.class, RedditConfig.class, 
          ServiceConfig.class, WebGeneralConfig.class);
        return application;
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        servletContext.addListener(new SessionListener());
        servletContext.addListener(new RequestContextListener());
        servletContext.addListener(new HttpSessionEventPublisher());
    }

    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

Вот пом.xml :


    4.0.0
    reddit-web
    reddit-web
    war

    
        org.baeldung
        reddit-scheduler
        0.2.0-SNAPSHOT
    

    
	
            org.baeldung
            reddit-common
            0.2.0-SNAPSHOT
        
        
        
            org.baeldung
            reddit-rest
            0.2.0-SNAPSHOT
        
        
        
            org.baeldung
            reddit-ui
            0.2.0-SNAPSHOT
        
...

Обратите внимание, что это единственная война, развертываемый модуль – так что приложение хорошо модульный сейчас, но по-прежнему развернуты в качестве монолита.

6. Заключение

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