Вступление
Добро пожаловать в Часть 9 создания клона Reddit с использованием Spring Boot и React.
Что мы строим в этой части?
- Поддержка разбивки на страницы
- Мы обновим наш серверный сервер для поддержки разбивки на страницы, это сократит время загрузки клиента по мере того, как база данных начнет масштабироваться
- Признание JWT недействительным
- JWT Освежающий
В части 8 мы добавили конечные точки СОЗДАНИЯ и ЧТЕНИЯ для создания и чтения комментариев!!
Важные ссылки
- Исходный код: https://github.com/MaxiCB/vox-nobis/tree/master/backend
- Источник интерфейса: https://github.com/MaxiCB/vox-nobis/tree/master/client
- Прямой URL-АДРЕС: в процессе
Часть 1: Обновление репозиториев 🗄
Давайте рассмотрим обновление всех наших репозиториев для реализации поддержки разбивки на страницы и сортировки. Внутри com.your-name.backend.repository мы обновим следующие классы.
- CommentRespository: Мы преобразуем вашу существующую логику, а также добавим метод findAllByPost, который по-прежнему возвращает список, поскольку мы полагаемся на него для отправки обратно общего количества комментариев в нашем PostService
package com.maxicb.backend.repository; import com.maxicb.backend.model.Comment; import com.maxicb.backend.model.Post; import com.maxicb.backend.model.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.PagingAndSortingRepository; import java.util.List; public interface CommentRepository extends PagingAndSortingRepository{ Page findByPost(Post post, Pageable pageable); List findAllByPost(Post post); Page findAllByUser(User user, Pageable pageable); }
- Пострепозиционный:
package com.maxicb.backend.repository; import com.maxicb.backend.model.Post; import com.maxicb.backend.model.Subreddit; import com.maxicb.backend.model.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import java.util.List; public interface PostRepository extends PagingAndSortingRepository{ Page findAllBySubreddit(Subreddit subreddit, Pageable pageable); Page findByUser(User user, Pageable pageable); }
- SubredditRepository:
package com.maxicb.backend.repository; import com.maxicb.backend.model.Subreddit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.PagingAndSortingRepository; import java.util.Optional; public interface SubredditRepository extends PagingAndSortingRepository{ Optional findByName(String subredditName); Optional > findByNameLike(String subredditName, Pageable pageable); }
Часть 2: Обновление служб 🌎
Теперь, когда мы обновили наши репозитории, нам нужно будет обновить наши серверы, чтобы отразить эти изменения. Внутри com.your-name.backend.service мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.
- CommentService: Мы обновим getCommentsForPost && get Comments Для пользовательских методов, чтобы правильно обрабатывать разбивку на страницы
public PagegetCommentsForPost(Long id, Integer page) { Post post = postRepository.findById(id) .orElseThrow(() -> new PostNotFoundException("Post not found with id: " + id)); return commentRepository.findByPost(post, PageRequest.of(page, 100)).map(this::mapToResponse); } public Page getCommentsForUser(Long id, Integer page) { User user = userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id)); return commentRepository.findAllByUser(user, PageRequest.of(page, 100)).map(this::mapToResponse); }
- Почтовое обслуживание: Мы обновим карту До методов Response && get All Posts && getPostsBySubreddit && getPostsByUsername для реализации разбивки на страницы, а также сохраним существующую логику сопоставления с DTO
private PostResponse mapToResponse(Post post) { return PostResponse.builder() .postId(post.getPostId()) .postTitle(post.getPostTitle()) .url(post.getUrl()) .description(post.getDescription()) .userName(post.getUser().getUsername()) .subredditName(post.getSubreddit().getName()) .voteCount(post.getVoteCount()) .commentCount(commentRepository.findAllByPost(post).size()) .duration(TimeAgo.using(post.getCreationDate().toEpochMilli())) .upVote(checkVoteType(post, VoteType.UPVOTE)) .downVote(checkVoteType(post, VoteType.DOWNVOTE)) .build(); } public PagegetAllPost(Integer page) { return postRepository.findAll(PageRequest.of(page, 100)).map(this::mapToResponse); } public Page getPostsBySubreddit(Integer page, Long id) { Subreddit subreddit = subredditRepository.findById(id) .orElseThrow(() -> new SubredditNotFoundException("Subreddit not found with id: " + id)); return postRepository .findAllBySubreddit(subreddit, PageRequest.of(page, 100)) .map(this::mapToResponse); } public Page getPostsByUsername(String username, Integer page) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UserNotFoundException("User not found with username: " + username)); return postRepository .findByUser(user, PageRequest.of(page, 100)) .map(this::mapToResponse); }
- SubredditService: Мы обновим метод GetAll
@Transactional(readOnly = true) public PagegetAll(Integer page) { return subredditRepository.findAll(PageRequest.of(page, 100)) .map(this::mapToDTO); }
Часть 3: Обновление контроллеров
Теперь, когда мы обновили наши сервисы и репозитории, нам нужно будет обновить наши контроллеры, чтобы позволить клиенту использовать разбивку на страницы. Внутри com.your-name.backend.controller мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.
- CommentController: Мы обновим методы get Comments By Post && getCommentsByUser для правильной обработки разбивки на страницы
@GetMapping("/post/{id}") public ResponseEntity> getCommentsByPost(@PathVariable("id") Long id, @RequestParam Optional page) { return new ResponseEntity<>(commentService.getCommentsForPost(id, page.orElse(0)), HttpStatus.OK); } @GetMapping("/user/{id}") public ResponseEntity > getCommentsByUser(@PathVariable("id") Long id,@RequestParam Optional page) { return new ResponseEntity<>(commentService.getCommentsForUser(id, page.orElse(0)), HttpStatus.OK); }
- PostController: Сначала мы обновим метод add Post, чтобы отправить созданную запись обратно клиенту при успешном создании, методы getAllPost && getPostsBySubreddit && getPostsByUsername для реализации разбивки на страницы
@PostMapping public ResponseEntityaddPost(@RequestBody PostRequest postRequest) { return new ResponseEntity<>(postService.save(postRequest), HttpStatus.CREATED); } @GetMapping public ResponseEntity > getAllPost(@RequestParam Optional page) { return new ResponseEntity<>(postService.getAllPost(page.orElse(0)), HttpStatus.OK); } @GetMapping("/sub/{id}") public ResponseEntity > getPostsBySubreddit(@PathVariable Long id, @RequestParam Optional page) { return new ResponseEntity<>(postService.getPostsBySubreddit(page.orElse(0), id), HttpStatus.OK); } @GetMapping("/user/{name}") public ResponseEntity > getPostsByUsername(@PathVariable("name") String username, @RequestParam Optional page) { return new ResponseEntity<>(postService.getPostsByUsername(username, page.orElse(0)), HttpStatus.OK); }
- SubredditController: Мы обновим все методы для реализации отправки ResponseEntity, а также поддержки разбивки на страницы
@GetMapping("/{page}") public ResponseEntity> getAllSubreddits (@PathVariable("page") Integer page) { return new ResponseEntity<>(subredditService.getAll(page), HttpStatus.OK); } @GetMapping("/sub/{id}") public ResponseEntity getSubreddit(@PathVariable("id") Long id) { return new ResponseEntity<>(subredditService.getSubreddit(id), HttpStatus.OK); } @PostMapping public ResponseEntity addSubreddit(@RequestBody @Valid SubredditDTO subredditDTO) throws Exception{ try { return new ResponseEntity<>(subredditService.save(subredditDTO), HttpStatus.OK); } catch (Exception e) { throw new Exception("Error Creating Subreddit"); } }
Теперь наше приложение полностью поддерживает разбивку на страницы для всех ресурсов, которые могут увеличиваться и вызывать медленную загрузку нашего интерфейсного приложения!
Часть 5: Обновить класс токена ⏳
Теперь нам нужно создать ваш класс токенов обновления, этот класс будет иметь идентификатор, токен и связанную с ним дату создания, чтобы разрешить аннулирование токенов по истечении заданного промежутка времени.
- refreshToken:
package com.maxicb.backend.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import java.time.Instant; @Data @Entity @AllArgsConstructor @NoArgsConstructor public class RefreshToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String token; private Instant creationDate; }
Часть 5: Обновление службы токенов и DTO 🌎
Теперь, когда у нас есть наш refreshToken, мы подготовим все необходимое, чтобы начать обновление нашей системы аутентификации. Внутри проекта мы добавим и обновим следующие классы.
- RefreshTokenRepository:
package com.maxicb.backend.repository; import com.maxicb.backend.model.RefreshToken; import org.springframework.data.repository.PagingAndSortingRepository; import java.util.Optional; public interface RefreshTokenRepository extends PagingAndSortingRepository{ Optional findByToken(String token); void deleteByToken(String token); }
- RefreshTokenService: Эта служба позволит нам генерировать токены, проверять токены и удалять токены.
package com.maxicb.backend.service; import com.maxicb.backend.exception.VoxNobisException; import com.maxicb.backend.model.RefreshToken; import com.maxicb.backend.repository.RefreshTokenRepository; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.UUID; @Service @AllArgsConstructor @Transactional public class RefreshTokenService { private RefreshTokenRepository refreshTokenRepository; RefreshToken generateRefreshToken () { RefreshToken refreshToken = new RefreshToken(); refreshToken.setToken(UUID.randomUUID().toString()); refreshToken.setCreationDate(Instant.now()); return refreshTokenRepository.save(refreshToken); } void validateToken(String token) { refreshTokenRepository.findByToken(token) .orElseThrow(() -> new VoxNobisException("Invalid Refresh Token")); } public void deleteRefreshToken(String token) { refreshTokenRepository.deleteByToken(token); } }
- Обновленный authResponse: Мы обновим authResponse, чтобы включить наш недавно сгенерированный токен.
import lombok.AllArgsConstructor; import lombok.Data; import java.time.Instant; @Data @AllArgsConstructor public class AuthResponse { private String authenticationToken; private String refreshToken; private Instant expiresAt; private String username; }
- RefreshTokenRequest: Этот DTO будет обрабатывать запросы от клиента на обновление их токена до истечения срока его действия в системе
package com.maxicb.backend.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; @Data @AllArgsConstructor @NoArgsConstructor public class RefreshTokenRequest { @NotBlank private String refreshToken; private String username; }
Часть 6: Обновление поставщика JWT 🔏
Теперь, когда у нас все готово, мы начнем обновлять нашу систему JWT. Внутри com.your-name.backend.service мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.
- JWTProvider: Мы обновим нашу СОБСТВЕННУЮ реализацию, включив дату выпуска, а также установим дату истечения срока действия при создании нового токена.
@Service public class JWTProvider { private KeyStore keystore; @Value("${jwt.expiration.time}") private Long jwtExpirationMillis; ... .... public String generateToken(Authentication authentication) { org.springframework.security.core.userdetails.User princ = (User) authentication.getPrincipal(); return Jwts.builder() .setSubject(princ.getUsername()) .setIssuedAt(from(Instant.now())) .signWith(getPrivKey()) .setExpiration(from(Instant.now().plusMillis(jwtExpirationMillis))) .compact(); } public String generateTokenWithUsername(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(from(Instant.now())) .signWith(getPrivKey()) .setExpiration(from(Instant.now().plusMillis(jwtExpirationMillis))) .compact(); } .... ... public Long getJwtExpirationMillis() { return jwtExpirationMillis; }
Часть 7: Обновленная аутентификация 💂 ♀ ️
Теперь, когда мы внедрили разбивку на страницы, мы начнем обновлять нашу систему аутентификации. Внутри нашего проекта мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.
- AuthService: Мы обновим наш AuthService для обработки отправки токенов обновления и добавим логику для обновления существующих токенов.
public AuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest) { refreshTokenService.validateToken(refreshTokenRequest.getRefreshToken()); String token = jwtProvider.generateTokenWithUsername(refreshTokenRequest.getUsername()); return new AuthResponse(token, refreshTokenService.generateRefreshToken().getToken(), Instant.now().plusMillis(jwtProvider.getJwtExpirationMillis()), refreshTokenRequest.getUsername()); } public AuthResponse login (LoginRequest loginRequest) { Authentication authenticate = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authenticate); String authToken = jwtProvider.generateToken(authenticate); String refreshToken = refreshTokenService.generateRefreshToken().getToken(); return new AuthResponse(authToken, refreshToken, Instant.now().plusMillis(jwtProvider.getJwtExpirationMillis()), loginRequest.getUsername()); }
- AuthController: Теперь мы внедрим новые конечные точки, чтобы позволить клиенту использовать недавно добавленную логику.
@PostMapping("/refresh/token") public AuthResponse refreshToken(@Valid @RequestBody RefreshTokenRequest refreshTokenRequest) { return authService.refreshToken(refreshTokenRequest); } @PostMapping("/logout") public ResponseEntitylogout(@Valid @RequestBody RefreshTokenRequest refreshTokenRequest) { refreshTokenService.deleteRefreshToken(refreshTokenRequest.getRefreshToken()); return ResponseEntity.status(HttpStatus.OK).body("Refresh Token Deleted"); }
Часть 8: Пользовательское исключение 🚫
- VoxNobisException: Мы создадим настраиваемое исключение общего назначения, которое можно использовать повторно во всем нашем приложении по мере его расширения.
package com.maxicb.backend.exception; public class VoxNobisException extends RuntimeException { public VoxNobisException(String message) {super(message);} }
Часть 9: Обновлено приложение.свойства
Нам нужно будет добавить время истечения срока действия, которое мы хотели бы, чтобы наше приложение использовало, когда дело доходит до генерации токенов, и соответствующим образом установить даты их истечения. Я решил установить его на 15 минут, но в будущем увеличу продолжительность.
# JWT Properties jwt.expiration.time=900000
Часть 10: Реализация пользовательского интерфейса Swagger 📃
Теперь, когда мы подошли к концу нашего бэкэнда MVP, мы добавим Swagger UI. Если вы никогда раньше не использовали Swagger, это отличный способ автоматически создавать документацию для вашего API. Вы можете узнать больше здесь!
- pom.xml: Нам нужно будет включить зависимости swagger в ваш проект pom.xml файл.
io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2
- SwaggerConfig: Внутри com.your-name.backend.config мы создадим следующий класс.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket voxNobisAPI() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build() .apiInfo(getAPIInfo()); } private ApiInfo getAPIInfo(){ return new ApiInfoBuilder() .title("Vox-Nobis API") .version("1.0") .description("API for Vox-Nobis reddit clone") .build(); } }
- Серверное приложение: Внутри com.your-name.backend мы введем нашу конфигурацию Swagger.
@SpringBootApplication @EnableAsync @Import(SwaggerConfig.class) public class BackendApplication { ... }
- Безопасность: Если вы запустите приложение сейчас и попытаетесь перейти к http://localhost:8080/swagger-ui.html#/ , вы , скорее всего, получите ошибку 403 forbidden. Внутри com.your-name.backend.config нам нужно будет обновить нашу конфигурацию безопасности, чтобы разрешить доступ без авторизации, добавив следующие сопоставления под нашим существующим.
.antMatchers(HttpMethod.GET, "/api/subreddit") .permitAll() .antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/security", "/swagger-ui.html", "/webjars/**") .permitAll()
Вывод 🔍
- Чтобы убедиться, что все настроено правильно, вы можете запустить приложение и убедиться, что в консоли нет ошибок. В нижней части консоли вы должны увидеть вывод, аналогичный приведенному ниже
Если в консоли нет ошибок, вы можете протестировать новую логику, отправив post-запрос на http://localhost:8080/api/auth/login с правильными данными, после успешного входа в систему вы должны получить refreshToken и имя пользователя обратно прямо сейчас!
Вы также можете перейти к http://localhost:8080/swagger-ui.html #/ , и просмотрите документацию для всех созданных нами конечных точек, а также необходимую им информацию и вернитесь.
В этой статье мы добавили разбивку на страницы и время истечения срока действия токена.
Следующий
Следите за новостями, чтобы получить информацию о выходе десятой части, где мы начнем работать над интерфейсом приложения!
Оригинал: “https://dev.to/maxicb/full-stack-reddit-clone-spring-boot-react-electron-app-part-9-3pj5”