diff --git a/build.gradle b/build.gradle index a9f95136..6d143d1f 100644 --- a/build.gradle +++ b/build.gradle @@ -21,13 +21,19 @@ repositories { dependencies { developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -// implementation 'org.springframework.boot:spring-boot-starter-security' + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' + // JSON Parser + implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1' +// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.session:spring-session-jdbc' // swagger setting implementation 'io.springfox:springfox-boot-starter:3.0.0' + implementation 'mysql:mysql-connector-java' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2:1.4.200' diff --git a/src/main/java/com/ajou/travely/config/CorsConfig.java b/src/main/java/com/ajou/travely/config/CorsConfig.java new file mode 100644 index 00000000..2e52859b --- /dev/null +++ b/src/main/java/com/ajou/travely/config/CorsConfig.java @@ -0,0 +1,20 @@ +package com.ajou.travely.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedOrigins("http://localhost:3000") + .allowCredentials(true); + } + }; + } +} diff --git a/src/main/java/com/ajou/travely/config/CustomAuthentication.java b/src/main/java/com/ajou/travely/config/CustomAuthentication.java new file mode 100644 index 00000000..c0e7f41d --- /dev/null +++ b/src/main/java/com/ajou/travely/config/CustomAuthentication.java @@ -0,0 +1,50 @@ +package com.ajou.travely.config; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.io.Serializable; +import java.util.Collection; + +public class CustomAuthentication implements Authentication, Serializable { + private final Long kakaoId; + + public CustomAuthentication(Long kakaoId) { + this.kakaoId = kakaoId; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getDetails() { + return kakaoId; + } + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + + } + + @Override + public String getName() { + return null; + } +} diff --git a/src/main/java/com/ajou/travely/config/RestTemplateConfig.java b/src/main/java/com/ajou/travely/config/RestTemplateConfig.java new file mode 100644 index 00000000..111d18ba --- /dev/null +++ b/src/main/java/com/ajou/travely/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package com.ajou.travely.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/ajou/travely/config/SecurityConfig.java b/src/main/java/com/ajou/travely/config/SecurityConfig.java new file mode 100644 index 00000000..46b0cf0a --- /dev/null +++ b/src/main/java/com/ajou/travely/config/SecurityConfig.java @@ -0,0 +1,19 @@ +package com.ajou.travely.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +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.configuration.WebSecurityConfigurerAdapter; + +@RequiredArgsConstructor +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable(); + http.authorizeRequests() + .antMatchers("/**").permitAll(); + } +} diff --git a/src/main/java/com/ajou/travely/controller/auth/AuthController.java b/src/main/java/com/ajou/travely/controller/auth/AuthController.java new file mode 100644 index 00000000..a3ab1796 --- /dev/null +++ b/src/main/java/com/ajou/travely/controller/auth/AuthController.java @@ -0,0 +1,33 @@ +package com.ajou.travely.controller.auth; + +import com.ajou.travely.service.AuthService; + +import lombok.RequiredArgsConstructor; +import org.json.simple.JSONObject; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +public class AuthController { + private final AuthService authService; + + @GetMapping("/oauth2/authorization/kakao") + public JSONObject login(@RequestParam("code") String code) { + return authService.kakaoAuthentication(code); + } + + @GetMapping("/isLogin") + public Boolean isLogin(@RequestHeader("Cookie") Optional header) { + if (header.isPresent()) { + return true; + } else { + return false; + } + } +} diff --git a/src/main/java/com/ajou/travely/controller/travel/dto/user/UserCreateRequestDto.java b/src/main/java/com/ajou/travely/controller/travel/dto/user/UserCreateRequestDto.java index 02eaeb18..7ce9ff46 100644 --- a/src/main/java/com/ajou/travely/controller/travel/dto/user/UserCreateRequestDto.java +++ b/src/main/java/com/ajou/travely/controller/travel/dto/user/UserCreateRequestDto.java @@ -5,13 +5,14 @@ import lombok.Getter; @Getter -public class UserCreateRequestDto { - private String userType; - private String name; - private String email; - private String phoneNumber; + public class UserCreateRequestDto { + private String userType; + private String name; + private String email; + private String phoneNumber; + private Long kakaoId; public User toEntity() { - return User.builder().type(Type.valueOf(this.userType)).name(this.name).email(this.email).phoneNumber(this.phoneNumber).build(); + return User.builder().type(Type.valueOf(this.userType)).name(this.name).email(this.email).phoneNumber(this.phoneNumber).kakaoId(this.kakaoId).build(); } } diff --git a/src/main/java/com/ajou/travely/domain/AuthorizationKakao.java b/src/main/java/com/ajou/travely/domain/AuthorizationKakao.java new file mode 100644 index 00000000..13ad26a5 --- /dev/null +++ b/src/main/java/com/ajou/travely/domain/AuthorizationKakao.java @@ -0,0 +1,13 @@ +package com.ajou.travely.domain; + +import lombok.Getter; + +@Getter +public class AuthorizationKakao { + private String access_token; + private String token_type; + private String refresh_token; + private String expires_in; + private String scope; + private String refresh_token_expires_in; +} diff --git a/src/main/java/com/ajou/travely/domain/user/User.java b/src/main/java/com/ajou/travely/domain/user/User.java index d0b530ce..24b56fc6 100644 --- a/src/main/java/com/ajou/travely/domain/user/User.java +++ b/src/main/java/com/ajou/travely/domain/user/User.java @@ -43,13 +43,16 @@ public class User { @Enumerated(EnumType.STRING) private Mbti mbti; + private Long kakaoId; + private LocalDate birthday; @Builder - public User(@NonNull Type type, @NonNull String email, @NonNull String name, @NonNull String phoneNumber) { + public User(@NonNull Type type, @NonNull String email, @NonNull String name, @NonNull String phoneNumber, @NonNull Long kakaoId) { this.type = type; this.email = email; this.name = name; this.phoneNumber = phoneNumber; + this.kakaoId = kakaoId; } } diff --git a/src/main/java/com/ajou/travely/repository/UserRepository.java b/src/main/java/com/ajou/travely/repository/UserRepository.java index 3adc7fa8..7ac690e3 100644 --- a/src/main/java/com/ajou/travely/repository/UserRepository.java +++ b/src/main/java/com/ajou/travely/repository/UserRepository.java @@ -2,6 +2,12 @@ import com.ajou.travely.domain.user.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface UserRepository extends JpaRepository { + @Query("select u from User u where u.kakaoId = :kakaoId") + public Optional findByKakaoId(@Param("kakaoId") Long kakaoId); } diff --git a/src/main/java/com/ajou/travely/service/AuthService.java b/src/main/java/com/ajou/travely/service/AuthService.java new file mode 100644 index 00000000..eb4f61db --- /dev/null +++ b/src/main/java/com/ajou/travely/service/AuthService.java @@ -0,0 +1,20 @@ +package com.ajou.travely.service; + +import com.ajou.travely.domain.AuthorizationKakao; +import lombok.RequiredArgsConstructor; +import org.json.simple.JSONObject; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final Oauth2Service oauth2Service; + + public JSONObject kakaoAuthentication(String code) { + AuthorizationKakao authorizationKakao = oauth2Service.callTokenApi(code); + JSONObject userInfoFromKakao = oauth2Service.callGetUserByAccessToken(authorizationKakao.getAccess_token()); + Long kakaoId = (Long) userInfoFromKakao.get("id"); + JSONObject result = oauth2Service.setSessionOrRedirectToSignUp(kakaoId); + return result; + } +} diff --git a/src/main/java/com/ajou/travely/service/Oauth2Service.java b/src/main/java/com/ajou/travely/service/Oauth2Service.java new file mode 100644 index 00000000..535f453e --- /dev/null +++ b/src/main/java/com/ajou/travely/service/Oauth2Service.java @@ -0,0 +1,116 @@ +package com.ajou.travely.service; + +import com.ajou.travely.config.CustomAuthentication; +import com.ajou.travely.domain.AuthorizationKakao; +import com.ajou.travely.domain.user.User; +import com.ajou.travely.repository.UserRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class Oauth2Service { + private final UserRepository userRepository; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + @Value("${auth.kakaoOauth2ClinetId}") + private String kakaoOauth2ClinetId; + @Value("${auth.frontendRedirectUrl}") + private String frontendRedirectUrl; + + public AuthorizationKakao callTokenApi(String code) { + String grantType = "authorization_code"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", grantType); + params.add("client_id", kakaoOauth2ClinetId); + params.add("redirect_uri", frontendRedirectUrl + "oauth/kakao/callback"); + params.add("code", code); + + HttpEntity> request = new HttpEntity<>(params, headers); + System.out.println("request.getBody() = " + request.getBody()); + String url = "https://kauth.kakao.com/oauth/token"; + try { + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + System.out.println("response.getBody() = " + response.getBody()); + System.out.println("response.getHeaders() = " + response.getHeaders()); + AuthorizationKakao authorization = objectMapper.readValue(response.getBody(), AuthorizationKakao.class); + + return authorization; + } catch (RestClientException | JsonProcessingException ex) { + ex.printStackTrace(); + throw new RestClientException("error"); + } + } + + public JSONObject callGetUserByAccessToken(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + HttpEntity> request = new HttpEntity<>(params, headers); + + String url = "https://kapi.kakao.com/v2/user/me"; + try { + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + SecurityContext context = SecurityContextHolder.getContext(); + JSONObject userInfo = stringToJson(response.getBody()); +// System.out.println("userInfo.get(\"id\") = " + userInfo.get("id")); +// JSONObject properties = (JSONObject) userInfo.get("properties"); +// System.out.println("properties.get(\"nickname\") = " + properties.get("nickname")); +// Long kakaoId = (Long) userInfo.get("id"); +// context.setAuthentication(new CustomAuthentication(kakaoId)); + return userInfo; + }catch (RestClientException | ParseException ex) { + ex.printStackTrace(); + throw new RestClientException("error"); + } + } + + public JSONObject setSessionOrRedirectToSignUp(Long kakaoId) { + Optional user = userRepository.findByKakaoId(kakaoId); + JSONObject result = new JSONObject(); + if(!user.isPresent()) { + result.put("status", 301); + result.put("kakaoId", kakaoId); + return result; + } else { + SecurityContext context = SecurityContextHolder.getContext(); + User exUser = user.get(); + context.setAuthentication(new CustomAuthentication(kakaoId)); + result.put("status", 200); + } + return result; + } + public JSONObject stringToJson(String userInfo) throws ParseException { + JSONParser jsonParser = new JSONParser(); + Object object = jsonParser.parse(userInfo); + JSONObject jsonObject = (JSONObject) object; + return jsonObject; + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2522dafb..a46734a6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,22 +1,29 @@ spring: datasource: - url: jdbc:mariadb://localhost:3306/travely + url: jdbc:mysql://localhost:3306/travely username: root - password: password - driver-class-name: org.mariadb.jdbc.Driver + password: ajoulee1214 + driver-class-name: com.mysql.jdbc.Driver # swagger setting mvc: pathmatch: matching-strategy: ant_path_matcher + session: + store-type: jdbc + jdbc: + initialize-schema: always + jpa: -# hibernate: -# ddl-auto: create + hibernate: + ddl-auto: create properties: hibernate: show_sql: true format_sql: true profiles: + include: + - "auth" active: default jackson: serialization.WRITE_DATES_AS_TIMESTAMPS=false diff --git a/src/test/java/com/ajou/travely/service/Oauth2ServiceTest.java b/src/test/java/com/ajou/travely/service/Oauth2ServiceTest.java new file mode 100644 index 00000000..cf780894 --- /dev/null +++ b/src/test/java/com/ajou/travely/service/Oauth2ServiceTest.java @@ -0,0 +1,59 @@ +package com.ajou.travely.service; + +import com.ajou.travely.domain.user.Type; +import com.ajou.travely.domain.user.User; +import com.ajou.travely.repository.UserRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.simple.JSONObject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest( + properties = { + "auth.kakaoOauth2ClinetId=test", + "auth.frontendRedirectUrl=test", + } +) +class Oauth2ServiceTest { + @Value("${auth.kakaoOauth2ClinetId}") + public String kakaoOauth2ClientId; + @Value("${auth.frontendRedirectUrl}") + public String frontendRedirectUrl; + @Autowired + private UserRepository userRepository; + @Autowired + private Oauth2Service oauth2Service; + + @Test + @DisplayName("카카오 유저 아이디를 통해 유저를 찾고 회원가입 여부 반환") + void testWhetherUserExists() { + Long kakaoId = 123456789L; + User user = User.builder() + .type(Type.USER) + .email("test@email.com") + .name("NAME") + .phoneNumber("0101010101010") + .kakaoId(kakaoId) + .build(); + + userRepository.save(user); + + JSONObject s1 = oauth2Service.setSessionOrRedirectToSignUp(kakaoId); + Long kakaoIdInAuthenticationDetail = (Long) SecurityContextHolder.getContext().getAuthentication().getDetails(); + JSONObject s2 = oauth2Service.setSessionOrRedirectToSignUp(123L); + + assertThat(s1.get("status")).isEqualTo(200); + assertThat(s2.get("status")).isEqualTo(301); + assertThat(kakaoIdInAuthenticationDetail).isEqualTo(kakaoId); + } +} \ No newline at end of file diff --git a/src/test/java/com/ajou/travely/service/TravelServiceTest.java b/src/test/java/com/ajou/travely/service/TravelServiceTest.java index 99f538d2..a6737a73 100644 --- a/src/test/java/com/ajou/travely/service/TravelServiceTest.java +++ b/src/test/java/com/ajou/travely/service/TravelServiceTest.java @@ -30,7 +30,7 @@ class TravelServiceTest { @Test @DisplayName("여행 객체를 만들 수 있다.") public void testCreateTravel() { - User user = userRepository.save(new User(Type.USER, "sophoca@ajou.ac.kr", "홍성빈", "112")); + User user = userRepository.save(new User(Type.USER, "sophoca@ajou.ac.kr", "홍성빈", "112", 0L)); TravelResponseDto travelResponseDto = travelService.createTravel(new TravelCreateRequestDto("첫 여행", LocalDate.now(), LocalDate.of(2030, 11, 9), user.getId())); Assertions.assertThat(travelRepository.findAll()).hasSize(1); Assertions.assertThat(travelResponseDto.getUsers()).hasSize(1); @@ -39,10 +39,10 @@ public void testCreateTravel() { @Test @DisplayName("여행에 유저를 초대할 수 있다.") public void testAddUserToTravel() { - User user = userRepository.save(new User(Type.USER, "sophoca@ajou.ac.kr", "홍성빈", "112")); + User user = userRepository.save(new User(Type.USER, "sophoca@ajou.ac.kr", "홍성빈", "112", 0L)); TravelResponseDto travelResponseDto = travelService.createTravel(new TravelCreateRequestDto("첫 여행", LocalDate.now(), LocalDate.of(2030, 11, 9), user.getId())); - User newUser = userRepository.save(new User(Type.USER, "errander@ajou.ac.kr", "이호용", "119")); + User newUser = userRepository.save(new User(Type.USER, "errander@ajou.ac.kr", "이호용", "119", 0L)); travelService.addUserToTravel(travelResponseDto.getId(), newUser.getId()); List users = travelService.getSimpleUsersOfTravel(travelResponseDto.getId()); Assertions.assertThat(users).hasSize(2);