Skip to content

Commit

Permalink
Merge pull request #66 from yooonwodyd/weekly
Browse files Browse the repository at this point in the history
[yooonwodyd-> weekly] 카카오 로그인 및 유저 컨트롤러 리팩토링
  • Loading branch information
yooonwodyd authored Nov 2, 2024
2 parents 78b904f + 3e7b766 commit b619c3c
Show file tree
Hide file tree
Showing 45 changed files with 934 additions and 156 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-client'

// Lombok
compileOnly 'org.projectlombok:lombok'
Expand Down Expand Up @@ -70,7 +71,14 @@ dependencies {

//S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

//Test
testImplementation 'org.testcontainers:testcontainers:1.20.2'
testImplementation 'org.testcontainers:junit-jupiter:1.20.2'
testImplementation 'org.testcontainers:mysql:1.20.2'

}

tasks.named('test') {
useJUnitPlatform()
}
2 changes: 2 additions & 0 deletions src/main/java/com/helpmeCookies/Step3Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
public class Step3Application {
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.helpmeCookies.global.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseTimeEntity {

@CreatedDate
private LocalDateTime createdDate;

@LastModifiedDate
private LocalDateTime modifiedDate;
}
29 changes: 12 additions & 17 deletions src/main/java/com/helpmeCookies/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,16 @@ public JwtToken createToken(JwtUser jwtUser) {
.build();
}

// 유요한 토큰인지 확인
/*
토큰 검증시 rawToken을 Claims로 변환하고, 해당 토큰이 accessToken이면서 만료되어있지 않다면 True를 반환한다.
*/

public boolean validateToken(String rawToken, boolean isAccessToken) {
try {
// 엑세스 토큰인지 확인
Claims claims = extractClaims(rawToken);
if (claims.get(IS_ACCESS_TOKEN, Boolean.class) != isAccessToken) {
return false;
}
// 만료시간 확인
return !claims.getExpiration().before(new Date());
} catch (Exception e) {
return false;
Expand All @@ -62,22 +63,24 @@ public boolean validateToken(String rawToken, boolean isAccessToken) {
/**
* refreshToken을 통해, accessToken을 재발급하는 메서드.
* refreshToken의 유효성을 검사하고, isAccessToken이 true일때만 accessToken을 재발급한다.
* TODO: refreshToken을 저장하고, 저장된 refreshToken과 비교하는 로직 필요
* TODO: refreshToken을 저장하고, 저장된 refreshToken과 비교하는 로직 필요 redis 추가 후 구현
*/
public String reissueAccessToken(String refreshToken) {
Claims claims = extractClaims(refreshToken);
if (claims.get(IS_ACCESS_TOKEN, Boolean.class)) {
throw new IllegalArgumentException("리프레시 토큰이 아닙니다.");
}

Date expiration = claims.getExpiration();
if (expiration.before(new Date())) {
throw new IllegalArgumentException("리프레시 토큰이 만료되었습니다.");
}

JwtUser jwtUser = claimsToJwtUser(claims);
return generateToken(jwtUser, true);
}

/**
* [validateToken] 이후 호출하는 메서드.
* rawToken을 통해 JwtUser를 추출한다.
* [jwtUser]는 userId와 role을 가지고 있다. 즉 JWT에 저장된 정보를 추출한다.
*/

public JwtUser getJwtUser(String rawToken) {
Claims claims = extractClaims(rawToken);
return claimsToJwtUser(claims);
Expand All @@ -88,10 +91,6 @@ private JwtUser claimsToJwtUser(Claims claims) {
return JwtUser.of(Long.parseLong(userId));
}

/**
* Jwt 토큰생성
* accessToken과 refreshToken의 다른점은 만료시간과, isAccessToken이다.
*/
private String generateToken(JwtUser jwtUser, boolean isAccessToken) {
long expireTime = isAccessToken ? accessTokenExpireTime : refreshTokenExpireTime;
Date expireDate = new Date(System.currentTimeMillis() + expireTime);
Expand All @@ -103,7 +102,6 @@ private String generateToken(JwtUser jwtUser, boolean isAccessToken) {
.compact();
}


private Claims extractClaims(String rawToken) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
Expand All @@ -112,9 +110,6 @@ private Claims extractClaims(String rawToken) {
.getBody();
}

/**
* HS256방식의 키를 생성한다.
*/
@Override
public void afterPropertiesSet() {
secretKey = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,5 @@ public class JwtAccessDeniedHandler implements AccessDeniedHandler {
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) {
log.error("Token : {}", request.getHeader("Authorization"));
// TODO: 에러코드 추가
response.setStatus(403);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,5 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.debug("Token : {}", request.getHeader("Authorization"));
response.setStatus(401);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
return;
}

// TODO: UserDetailsService를 통해 사용자 정보를 가져와 인증을 진행한다.
if (jwtProvider.validateToken(rawToken, true)) {
JwtUser jwtUser = jwtProvider.getJwtUser(rawToken);
Authentication authentication = new UsernamePasswordAuthenticationToken(jwtUser, null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.helpmeCookies.global.security;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;

import com.helpmeCookies.user.service.UserService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class Oauth2CustomUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
return oAuth2User;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.helpmeCookies.global.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.helpmeCookies.global.jwt.JwtUser;
import com.helpmeCookies.user.entity.User;
import com.helpmeCookies.user.entity.UserInfo;
import com.helpmeCookies.user.repository.UserRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserDetailService {
private final UserRepository userRepository;

public JwtUser loadUserByEmail(String email) throws UsernameNotFoundException {
// 만약 유저가 존재하지 않는다면 저장
User user = userRepository.findByUserInfoEmail(email)
.orElseGet(() -> {
User newUser = User.builder()
.userInfo(UserInfo.builder().email(email).build())
.build();
return userRepository.save(newUser);
});
return JwtUser.of(user.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor


@Controller
public class WebSecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
Expand All @@ -30,8 +28,31 @@ public class WebSecurityConfig {
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers("/static/**")
.requestMatchers("/test/**");
.requestMatchers("/swagger-ui")
.requestMatchers("/static/**");
}

@Bean
public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/oauth2/**")
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/oauth2/authorization/**",
"/oauth2/code/kakao/**"
).permitAll()
.anyRequest().authenticated()
)
.oauth2Login((oauth2) -> oauth2
.redirectionEndpoint(redirection -> redirection
.baseUri("/oauth2/code/*"))
.userInfoEndpoint((userInfo) -> userInfo
.userService(new Oauth2CustomUserService())
)
.defaultSuccessUrl("/oauth2/login/kakao")
);

return http.build();
}

@Bean
Expand All @@ -43,7 +64,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers(
"/login", "/signup", "/", "/user",
"/login", "/signup", "/ttt/*", "/user",
"/api/auth/**",
"/swagger-ui/**",
"/swagger-resources",
Expand All @@ -53,9 +74,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"swagger-ui/**"
).permitAll()
.anyRequest().authenticated()
);

http.exceptionHandling((exception) -> exception
).exceptionHandling((exception) -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.helpmeCookies.product.controller;

import static com.helpmeCookies.product.util.SortUtil.convertProductSort;

import com.helpmeCookies.product.controller.docs.ProductApiDocs;
import com.helpmeCookies.product.dto.FileUploadResponse;
import com.helpmeCookies.product.dto.ProductImageResponse;
import com.helpmeCookies.product.dto.ProductPage;
import com.helpmeCookies.product.dto.ProductRequest;
import com.helpmeCookies.product.dto.ProductResponse;
import com.helpmeCookies.product.entity.Product;
import com.helpmeCookies.product.service.ProductImageService;
import com.helpmeCookies.product.service.ProductService;
import com.helpmeCookies.product.util.ProductSort;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -16,9 +22,9 @@
import java.util.List;

@RestController
@RequestMapping("/api/v1/products")
@RequestMapping("/v1/products")
@RequiredArgsConstructor
public class ProductController {
public class ProductController implements ProductApiDocs {

private final ProductService productService;
private final ProductImageService productImageService;
Expand Down Expand Up @@ -59,4 +65,17 @@ public ResponseEntity<Void> deleteProduct(@PathVariable("productId") Long produc
productService.delete(productId);
return ResponseEntity.noContent().build();
}

@GetMapping
public ResponseEntity<ProductPage.Paging> getProductsByPage(
@RequestParam("query") String query,
@RequestParam(name = "size", required = false, defaultValue = "20") int size,
@RequestParam("page") int page,
@RequestParam("sort") ProductSort productSort
) {
var sort = convertProductSort(productSort);
var pageable = PageRequest.of(page, size, sort);

return ResponseEntity.ok(productService.getProductsByPage(query, pageable));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.helpmeCookies.product.controller.docs;

import com.helpmeCookies.product.dto.ProductPage.Paging;
import com.helpmeCookies.product.util.ProductSort;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

@Tag(name = "상품 관련 기능", description = "상품 관련 API")
public interface ProductApiDocs {

@Operation(summary = "상품 검색")
ResponseEntity<Paging> getProductsByPage(
String query,
@Parameter(description = "default value 20") int size,
int page,
ProductSort productSort
);

}
46 changes: 46 additions & 0 deletions src/main/java/com/helpmeCookies/product/dto/ProductPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.helpmeCookies.product.dto;

import com.helpmeCookies.product.repository.dto.ProductSearch;
import java.util.List;
import org.springframework.data.domain.Page;

public class ProductPage {

public record Info(
Long id,
String name,
String artist,
Long price,
String thumbnailUrl
) {
public static Info from(ProductSearch productSearch) {
return new Info(
productSearch.getId(),
productSearch.getName(),
productSearch.getArtist(),
productSearch.getPrice(),
productSearch.getThumbnailUrl()
);
}

public static List<Info> of(List<ProductSearch> content) {
return content.stream()
.map(Info::from)
.toList();
}
}

public record Paging (
boolean hasNext,
List<Info> products
) {

public static Paging from(Page<ProductSearch> productPage) {
return new Paging(
productPage.hasNext(),
Info.of(productPage.getContent())
);
}
}

}
Loading

0 comments on commit b619c3c

Please sign in to comment.