From ffbb871822eab3ac4143e0e002ab3d0253e36873 Mon Sep 17 00:00:00 2001 From: oxdjww Date: Fri, 5 Jul 2024 04:07:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20OAuth=20+=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20s3=20multiUpload=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 ++- .../TimeLapse/config/CorsConfig.java | 32 +++++++++ .../TimeLapse/config/SecurityConfig.java | 63 +++++++++++++++++ .../hackathon/TimeLapse/domain/Member.java | 55 ++++++++++----- .../TimeLapse/member/MemberRepository.java | 6 +- .../TimeLapse/oauth/AuthController.java | 21 ++++++ .../hackathon/TimeLapse/oauth/AuthTokens.java | 21 ++++++ .../TimeLapse/oauth/AuthTokensGenerator.java | 33 +++++++++ .../TimeLapse/oauth/ClientConfig.java | 14 ++++ .../oauth/JwtAuthenticationFilter.java | 54 ++++++++++++++ .../TimeLapse/oauth/JwtTokenProvider.java | 50 +++++++++++++ .../TimeLapse/oauth/KakaoApiClient.java | 70 +++++++++++++++++++ .../TimeLapse/oauth/KakaoInfoResponse.java | 42 +++++++++++ .../TimeLapse/oauth/KakaoLoginParams.java | 25 +++++++ .../TimeLapse/oauth/KakaoTokens.java | 29 ++++++++ .../TimeLapse/oauth/MemberController.java | 33 +++++++++ .../TimeLapse/oauth/OAuthApiClient.java | 7 ++ .../TimeLapse/oauth/OAuthInfoResponse.java | 7 ++ .../TimeLapse/oauth/OAuthLoginParams.java | 9 +++ .../TimeLapse/oauth/OAuthLoginService.java | 38 ++++++++++ .../TimeLapse/oauth/OAuthProvider.java | 5 ++ .../oauth/RequestOAuthInfoService.java | 25 +++++++ src/main/resources/application-prod.yml | 10 +++ 23 files changed, 637 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/hackathon/TimeLapse/config/CorsConfig.java create mode 100644 src/main/java/com/hackathon/TimeLapse/config/SecurityConfig.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/AuthController.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/AuthTokens.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/AuthTokensGenerator.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/ClientConfig.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/JwtTokenProvider.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/KakaoApiClient.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/KakaoInfoResponse.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/KakaoLoginParams.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/KakaoTokens.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/MemberController.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/OAuthApiClient.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/OAuthInfoResponse.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginParams.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginService.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/OAuthProvider.java create mode 100644 src/main/java/com/hackathon/TimeLapse/oauth/RequestOAuthInfoService.java diff --git a/build.gradle b/build.gradle index 240ba58..b4afeac 100644 --- a/build.gradle +++ b/build.gradle @@ -27,11 +27,9 @@ dependencies { // core implementation 'org.springframework.boot:spring-boot-starter-web' - // validation - implementation 'org.springframework.boot:spring-boot-starter-validation' - // jpa implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' runtimeOnly 'com.mysql:mysql-connector-j' // lombok @@ -48,6 +46,11 @@ dependencies { // S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' } tasks.named('test') { diff --git a/src/main/java/com/hackathon/TimeLapse/config/CorsConfig.java b/src/main/java/com/hackathon/TimeLapse/config/CorsConfig.java new file mode 100644 index 0000000..89827d0 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/config/CorsConfig.java @@ -0,0 +1,32 @@ +package com.hackathon.TimeLapse.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +public class CorsConfig implements WebMvcConfigurer { + + public static CorsConfigurationSource apiConfigurationSource() { + + CorsConfiguration configuration = new CorsConfiguration(); + + ArrayList allowedOriginPatterns = new ArrayList<>(); + allowedOriginPatterns.add("http://localhost:8080"); + allowedOriginPatterns.add("http://localhost:3000"); + // allowedOriginPatterns.add("{프론트 배포 URL}"); + + ArrayList allowedHttpMethods = new ArrayList<>(List.of("GET", "POST")); + + configuration.setAllowedOrigins(allowedOriginPatterns); + configuration.setAllowedMethods(allowedHttpMethods); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/config/SecurityConfig.java b/src/main/java/com/hackathon/TimeLapse/config/SecurityConfig.java new file mode 100644 index 0000000..bc3fe23 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/config/SecurityConfig.java @@ -0,0 +1,63 @@ +package com.hackathon.TimeLapse.config; + +import java.util.Arrays; +import java.util.stream.Stream; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import com.hackathon.TimeLapse.oauth.JwtAuthenticationFilter; +import com.hackathon.TimeLapse.oauth.JwtTokenProvider; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + private final String[] swaggerUrls = {"/","/swagger-ui/**", "/v3/api-docs/**"}; + private final String[] authUrls = {"/kakao/callback/**", "/api/auth/kakao", "/api/files/**"}; + private final String[] allowedUrls = Stream.concat(Arrays.stream(swaggerUrls), Arrays.stream(authUrls)) + .toArray(String[]::new); + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http + .cors((cors) -> cors + .configurationSource(CorsConfig.apiConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .authorizeHttpRequests((auth) -> auth + .requestMatchers(allowedUrls).permitAll() + .requestMatchers("/test").permitAll() + .anyRequest().authenticated() + ); + + http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/domain/Member.java b/src/main/java/com/hackathon/TimeLapse/domain/Member.java index 667d614..120493a 100644 --- a/src/main/java/com/hackathon/TimeLapse/domain/Member.java +++ b/src/main/java/com/hackathon/TimeLapse/domain/Member.java @@ -1,33 +1,54 @@ package com.hackathon.TimeLapse.domain; -import com.hackathon.TimeLapse.domain.common.BaseEntity; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.DynamicInsert; -import org.hibernate.annotations.DynamicUpdate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - import java.util.ArrayList; import java.util.List; -@Entity -@Getter -@DynamicUpdate -@DynamicInsert -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) +import com.hackathon.TimeLapse.domain.common.BaseEntity; +import com.hackathon.TimeLapse.oauth.OAuthProvider; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor @AllArgsConstructor +@Getter +@Entity public class Member extends BaseEntity { @Id @GeneratedValue( - strategy = GenerationType.IDENTITY + strategy = GenerationType.IDENTITY ) private Long id; + private String email; + + private String nickname; + + @Enumerated(EnumType.STRING) + private OAuthProvider oAuthProvider; + @OneToMany( - mappedBy = "member", - cascade = {CascadeType.ALL} + mappedBy = "member", + cascade = {CascadeType.ALL}, + fetch = FetchType.EAGER ) - @Builder.Default private List
articleList = new ArrayList<>(); + + @Builder + public Member(String email, String nickname, OAuthProvider oAuthProvider) { + this.email = email; + this.nickname = nickname; + this.oAuthProvider = oAuthProvider; + } } diff --git a/src/main/java/com/hackathon/TimeLapse/member/MemberRepository.java b/src/main/java/com/hackathon/TimeLapse/member/MemberRepository.java index 3643f23..c6e3a26 100644 --- a/src/main/java/com/hackathon/TimeLapse/member/MemberRepository.java +++ b/src/main/java/com/hackathon/TimeLapse/member/MemberRepository.java @@ -1,7 +1,11 @@ package com.hackathon.TimeLapse.member; -import com.hackathon.TimeLapse.domain.Member; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import com.hackathon.TimeLapse.domain.Member; + public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/AuthController.java b/src/main/java/com/hackathon/TimeLapse/oauth/AuthController.java new file mode 100644 index 0000000..fc4e370 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/AuthController.java @@ -0,0 +1,21 @@ +package com.hackathon.TimeLapse.oauth; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class AuthController { + private final OAuthLoginService oAuthLoginService; + + @PostMapping("/kakao") + public ResponseEntity loginKakao(@RequestBody KakaoLoginParams params) { + return ResponseEntity.ok(oAuthLoginService.login(params)); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/AuthTokens.java b/src/main/java/com/hackathon/TimeLapse/oauth/AuthTokens.java new file mode 100644 index 0000000..f42eeec --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/AuthTokens.java @@ -0,0 +1,21 @@ +package com.hackathon.TimeLapse.oauth; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AuthTokens { + private String accessToken; + private String refreshToken; + private String grantType; + private Long expiresIn; + + public static AuthTokens of(String accessToken, String refreshToken, String grantType, Long expiresIn) { + return new AuthTokens(accessToken, refreshToken, grantType, expiresIn); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/AuthTokensGenerator.java b/src/main/java/com/hackathon/TimeLapse/oauth/AuthTokensGenerator.java new file mode 100644 index 0000000..c654012 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/AuthTokensGenerator.java @@ -0,0 +1,33 @@ +package com.hackathon.TimeLapse.oauth; + +import java.util.Date; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AuthTokensGenerator { + private static final String BEARER_TYPE = "Bearer"; + private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; // 30분 + private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; // 7일 + + private final JwtTokenProvider jwtTokenProvider; + + public AuthTokens generate(Long memberId) { + long now = (new Date()).getTime(); + Date accessTokenExpiredAt = new Date(now + ACCESS_TOKEN_EXPIRE_TIME); + Date refreshTokenExpiredAt = new Date(now + REFRESH_TOKEN_EXPIRE_TIME); + + String subject = memberId.toString(); + String accessToken = jwtTokenProvider.generate(subject, accessTokenExpiredAt); + String refreshToken = jwtTokenProvider.generate(subject, refreshTokenExpiredAt); + + return AuthTokens.of(accessToken, refreshToken, BEARER_TYPE, ACCESS_TOKEN_EXPIRE_TIME / 1000L); + } + + public Long extractMemberId(String accessToken) { + return Long.valueOf(jwtTokenProvider.extractSubject(accessToken)); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/ClientConfig.java b/src/main/java/com/hackathon/TimeLapse/oauth/ClientConfig.java new file mode 100644 index 0000000..d786032 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/ClientConfig.java @@ -0,0 +1,14 @@ +package com.hackathon.TimeLapse.oauth; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class ClientConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/JwtAuthenticationFilter.java b/src/main/java/com/hackathon/TimeLapse/oauth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..94aa62e --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/JwtAuthenticationFilter.java @@ -0,0 +1,54 @@ +package com.hackathon.TimeLapse.oauth; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@AllArgsConstructor +@Slf4j +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + log.info("[*] Jwt Filter"); + String token = resolveToken(request); + log.info("[*] Jwt token >>>>> " + token); + if (token != null) { + Authentication authentication = getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); //SecurityContextHolder에 담기 + } + chain.doFilter(request, response); + } + private Authentication getAuthentication(String token) { + log.info("[*] subject: "+jwtTokenProvider.extractSubject(token)); + return new UsernamePasswordAuthenticationToken(jwtTokenProvider.extractSubject(token), null, + Collections.singleton(new SimpleGrantedAuthority("ROLE_MEMBER"))); + } + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.split(" ")[1]; + } + return null; + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/JwtTokenProvider.java b/src/main/java/com/hackathon/TimeLapse/oauth/JwtTokenProvider.java new file mode 100644 index 0000000..aa70e2c --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/JwtTokenProvider.java @@ -0,0 +1,50 @@ +package com.hackathon.TimeLapse.oauth; + +import java.security.Key; +import java.util.Date; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; + +@Component +public class JwtTokenProvider { + + private final Key key; + + public JwtTokenProvider(@Value("${jwt.secret-key}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public String generate(String subject, Date expiredAt) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(expiredAt) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + } + + public String extractSubject(String accessToken) { + Claims claims = parseClaims(accessToken); + return claims.getSubject(); + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/KakaoApiClient.java b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoApiClient.java new file mode 100644 index 0000000..197a7b8 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoApiClient.java @@ -0,0 +1,70 @@ +package com.hackathon.TimeLapse.oauth; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class KakaoApiClient implements OAuthApiClient { + + private static final String GRANT_TYPE = "authorization_code"; + + @Value("${oauth.kakao.url.auth}") + private String authUrl; + + @Value("${oauth.kakao.url.api}") + private String apiUrl; + + @Value("${oauth.kakao.client-id}") + private String clientId; + + private final RestTemplate restTemplate; + + @Override + public OAuthProvider oAuthProvider() { + return OAuthProvider.KAKAO; + } + + @Override + public String requestAccessToken(OAuthLoginParams params) { + String url = authUrl + "/oauth/token"; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap body = params.makeBody(); + body.add("grant_type", GRANT_TYPE); + body.add("client_id", clientId); + + HttpEntity request = new HttpEntity<>(body, httpHeaders); + + KakaoTokens response = restTemplate.postForObject(url, request, KakaoTokens.class); + + assert response != null; + return response.getAccessToken(); + } + + @Override + public OAuthInfoResponse requestOauthInfo(String accessToken) { + String url = apiUrl + "/v2/user/me"; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + httpHeaders.set("Authorization", "Bearer " + accessToken); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("property_keys", "[\"kakao_account.email\", \"kakao_account.profile\"]"); + + HttpEntity request = new HttpEntity<>(body, httpHeaders); + + return restTemplate.postForObject(url, request, KakaoInfoResponse.class); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/KakaoInfoResponse.java b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoInfoResponse.java new file mode 100644 index 0000000..fe47c51 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoInfoResponse.java @@ -0,0 +1,42 @@ +package com.hackathon.TimeLapse.oauth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoInfoResponse implements OAuthInfoResponse { + + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoAccount { + private KakaoProfile profile; + private String email; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoProfile { + private String nickname; + } + + @Override + public String getEmail() { + return kakaoAccount.email; + } + + @Override + public String getNickname() { + return kakaoAccount.profile.nickname; + } + + @Override + public OAuthProvider getOAuthProvider() { + return OAuthProvider.KAKAO; + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/KakaoLoginParams.java b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoLoginParams.java new file mode 100644 index 0000000..4e8720d --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoLoginParams.java @@ -0,0 +1,25 @@ +package com.hackathon.TimeLapse.oauth; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KakaoLoginParams implements OAuthLoginParams { + private String authorizationCode; + + @Override + public OAuthProvider oAuthProvider() { + return OAuthProvider.KAKAO; + } + + @Override + public MultiValueMap makeBody() { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", authorizationCode); + return body; + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/KakaoTokens.java b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoTokens.java new file mode 100644 index 0000000..e3afd4a --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/KakaoTokens.java @@ -0,0 +1,29 @@ +package com.hackathon.TimeLapse.oauth; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KakaoTokens { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("expires_in") + private String expiresIn; + + @JsonProperty("refresh_token_expires_in") + private String refreshTokenExpiresIn; + + @JsonProperty("scope") + private String scope; +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/MemberController.java b/src/main/java/com/hackathon/TimeLapse/oauth/MemberController.java new file mode 100644 index 0000000..cd24ca4 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/MemberController.java @@ -0,0 +1,33 @@ +package com.hackathon.TimeLapse.oauth; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.hackathon.TimeLapse.domain.Member; +import com.hackathon.TimeLapse.member.MemberRepository; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/members") +public class MemberController { + private final MemberRepository memberRepository; + private final AuthTokensGenerator authTokensGenerator; + + @GetMapping + public ResponseEntity> findAll() { + return ResponseEntity.ok(memberRepository.findAll()); + } + + @GetMapping("/{accessToken}") + public ResponseEntity findByAccessToken(@PathVariable String accessToken) { + Long memberId = authTokensGenerator.extractMemberId(accessToken); + return ResponseEntity.ok(memberRepository.findById(memberId).get()); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/OAuthApiClient.java b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthApiClient.java new file mode 100644 index 0000000..67deffc --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthApiClient.java @@ -0,0 +1,7 @@ +package com.hackathon.TimeLapse.oauth; + +public interface OAuthApiClient { + OAuthProvider oAuthProvider(); + String requestAccessToken(OAuthLoginParams params); + OAuthInfoResponse requestOauthInfo(String accessToken); +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/OAuthInfoResponse.java b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthInfoResponse.java new file mode 100644 index 0000000..86616e4 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthInfoResponse.java @@ -0,0 +1,7 @@ +package com.hackathon.TimeLapse.oauth; + +public interface OAuthInfoResponse { + String getEmail(); + String getNickname(); + OAuthProvider getOAuthProvider(); +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginParams.java b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginParams.java new file mode 100644 index 0000000..0b28878 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginParams.java @@ -0,0 +1,9 @@ +package com.hackathon.TimeLapse.oauth; + +import org.springframework.util.MultiValueMap; + +public interface OAuthLoginParams { + OAuthProvider oAuthProvider(); + + MultiValueMap makeBody(); +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginService.java b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginService.java new file mode 100644 index 0000000..9ea96f4 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthLoginService.java @@ -0,0 +1,38 @@ +package com.hackathon.TimeLapse.oauth; + +import org.springframework.stereotype.Service; + +import com.hackathon.TimeLapse.domain.Member; +import com.hackathon.TimeLapse.member.MemberRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class OAuthLoginService { + private final MemberRepository memberRepository; + private final AuthTokensGenerator authTokensGenerator; + private final RequestOAuthInfoService requestOAuthInfoService; + + public AuthTokens login(OAuthLoginParams params) { + OAuthInfoResponse oAuthInfoResponse = requestOAuthInfoService.request(params); + Long memberId = findOrCreateMember(oAuthInfoResponse); + return authTokensGenerator.generate(memberId); + } + + private Long findOrCreateMember(OAuthInfoResponse oAuthInfoResponse) { + return memberRepository.findByEmail(oAuthInfoResponse.getEmail()) + .map(Member::getId) + .orElseGet(() -> newMember(oAuthInfoResponse)); + } + + private Long newMember(OAuthInfoResponse oAuthInfoResponse) { + Member member = Member.builder() + .email(oAuthInfoResponse.getEmail()) + .nickname(oAuthInfoResponse.getNickname()) + .oAuthProvider(oAuthInfoResponse.getOAuthProvider()) + .build(); + + return memberRepository.save(member).getId(); + } +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/OAuthProvider.java b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthProvider.java new file mode 100644 index 0000000..15437e5 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/OAuthProvider.java @@ -0,0 +1,5 @@ +package com.hackathon.TimeLapse.oauth; + +public enum OAuthProvider { + KAKAO +} diff --git a/src/main/java/com/hackathon/TimeLapse/oauth/RequestOAuthInfoService.java b/src/main/java/com/hackathon/TimeLapse/oauth/RequestOAuthInfoService.java new file mode 100644 index 0000000..ab34343 --- /dev/null +++ b/src/main/java/com/hackathon/TimeLapse/oauth/RequestOAuthInfoService.java @@ -0,0 +1,25 @@ +package com.hackathon.TimeLapse.oauth; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +@Component +public class RequestOAuthInfoService { + private final Map clients; + + public RequestOAuthInfoService(List clients) { + this.clients = clients.stream().collect( + Collectors.toUnmodifiableMap(OAuthApiClient::oAuthProvider, Function.identity()) + ); + } + + public OAuthInfoResponse request(OAuthLoginParams params) { + OAuthApiClient client = clients.get(params.oAuthProvider()); + String accessToken = client.requestAccessToken(params); + return client.requestOauthInfo(accessToken); + } +} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 68c8dbc..b744da7 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -23,3 +23,13 @@ cloud: credentials: accessKey: {accesskey} secretKey: {secretkey} + +jwt: + secret-key: {secret} + +oauth: + kakao: + client-id: {client-id} + url: + auth: https://kauth.kakao.com + api: https://kapi.kakao.com