From c2f1ea67e672b95fda1e727753deb4f6119e89e7 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Fri, 19 Apr 2019 02:04:23 +0900 Subject: [PATCH] =?UTF-8?q?SpringBoot2=EB=A1=9C=20Rest=20api=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0(10)=20=E2=80=93=20Social=20Login=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99(kakao)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rest/api/advice/ExceptionAdvice.java | 16 +++++-- .../exception/CCommunicationException.java | 15 ++++++ .../advice/exception/CUserExistException.java | 15 ++++++ .../security/SecurityConfiguration.java | 2 +- .../api/controller/v1/SignController.java | 47 ++++++++++++++++++- src/main/java/com/rest/api/entity/User.java | 9 ++-- .../rest/api/model/social/KakaoProfile.java | 19 ++++++++ .../java/com/rest/api/repo/UserJpaRepo.java | 2 + .../rest/api/service/user/UserService.java | 35 ++++++++++++++ src/main/resources/application.yml | 2 +- src/main/resources/i18n/exception_en.yml | 8 +++- src/main/resources/i18n/exception_ko.yml | 8 +++- 12 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/rest/api/advice/exception/CCommunicationException.java create mode 100644 src/main/java/com/rest/api/advice/exception/CUserExistException.java create mode 100644 src/main/java/com/rest/api/model/social/KakaoProfile.java create mode 100644 src/main/java/com/rest/api/service/user/UserService.java diff --git a/src/main/java/com/rest/api/advice/ExceptionAdvice.java b/src/main/java/com/rest/api/advice/ExceptionAdvice.java index 7ed7d40..8ff576d 100644 --- a/src/main/java/com/rest/api/advice/ExceptionAdvice.java +++ b/src/main/java/com/rest/api/advice/ExceptionAdvice.java @@ -1,8 +1,6 @@ package com.rest.api.advice; -import com.rest.api.advice.exception.CAuthenticationEntryPointException; -import com.rest.api.advice.exception.CEmailSigninFailedException; -import com.rest.api.advice.exception.CUserNotFoundException; +import com.rest.api.advice.exception.*; import com.rest.api.model.response.CommonResult; import com.rest.api.service.ResponseService; import lombok.RequiredArgsConstructor; @@ -55,6 +53,18 @@ public class ExceptionAdvice { return responseService.getFailResult(Integer.valueOf(getMessage("accessDenied.code")), getMessage("accessDenied.msg")); } + @ExceptionHandler(CCommunicationException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public CommonResult communicationException(HttpServletRequest request, CCommunicationException e) { + return responseService.getFailResult(Integer.valueOf(getMessage("communicationError.code")), getMessage("communicationError.msg")); + } + + @ExceptionHandler(CUserExistException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public CommonResult communicationException(HttpServletRequest request, CUserExistException e) { + return responseService.getFailResult(Integer.valueOf(getMessage("existingUser.code")), getMessage("existingUser.msg")); + } + // code정보에 해당하는 메시지를 조회합니다. private String getMessage(String code) { return getMessage(code, null); diff --git a/src/main/java/com/rest/api/advice/exception/CCommunicationException.java b/src/main/java/com/rest/api/advice/exception/CCommunicationException.java new file mode 100644 index 0000000..ef60d0c --- /dev/null +++ b/src/main/java/com/rest/api/advice/exception/CCommunicationException.java @@ -0,0 +1,15 @@ +package com.rest.api.advice.exception; + +public class CCommunicationException extends RuntimeException { + public CCommunicationException(String msg, Throwable t) { + super(msg, t); + } + + public CCommunicationException(String msg) { + super(msg); + } + + public CCommunicationException() { + super(); + } +} diff --git a/src/main/java/com/rest/api/advice/exception/CUserExistException.java b/src/main/java/com/rest/api/advice/exception/CUserExistException.java new file mode 100644 index 0000000..ff97170 --- /dev/null +++ b/src/main/java/com/rest/api/advice/exception/CUserExistException.java @@ -0,0 +1,15 @@ +package com.rest.api.advice.exception; + +public class CUserExistException extends RuntimeException { + public CUserExistException(String msg, Throwable t) { + super(msg, t); + } + + public CUserExistException(String msg) { + super(msg); + } + + public CUserExistException() { + super(); + } +} diff --git a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java index a0f0357..a7d1e19 100644 --- a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java +++ b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java @@ -31,7 +31,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증할것이므로 세션필요없으므로 생성안함. .and() .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크 - .antMatchers("/*/signin", "/*/signup", "/social/**").permitAll() // 가입 및 인증 주소는 누구나 접근가능 + .antMatchers("/*/signin", "/*/signin/**", "/*/signup", "/*/signup/**", "/social/**").permitAll() // 가입 및 인증 주소는 누구나 접근가능 .antMatchers(HttpMethod.GET, "/helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능 .and() diff --git a/src/main/java/com/rest/api/controller/v1/SignController.java b/src/main/java/com/rest/api/controller/v1/SignController.java index 82e2528..a26724d 100644 --- a/src/main/java/com/rest/api/controller/v1/SignController.java +++ b/src/main/java/com/rest/api/controller/v1/SignController.java @@ -1,20 +1,28 @@ package com.rest.api.controller.v1; +import com.google.gson.Gson; import com.rest.api.advice.exception.CEmailSigninFailedException; -import com.rest.api.entity.User; +import com.rest.api.advice.exception.CUserExistException; +import com.rest.api.advice.exception.CUserNotFoundException; import com.rest.api.config.security.JwtTokenProvider; +import com.rest.api.entity.User; import com.rest.api.model.response.CommonResult; import com.rest.api.model.response.SingleResult; +import com.rest.api.model.social.KakaoProfile; import com.rest.api.repo.UserJpaRepo; import com.rest.api.service.ResponseService; +import com.rest.api.service.user.UserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; import java.util.Collections; +import java.util.Optional; @Api(tags = {"1. Sign"}) @RequiredArgsConstructor @@ -26,6 +34,10 @@ public class SignController { private final JwtTokenProvider jwtTokenProvider; private final ResponseService responseService; private final PasswordEncoder passwordEncoder; + private final RestTemplate restTemplate; + private final Environment env; + private final Gson gson; + private final UserService userService; @ApiOperation(value = "로그인", notes = "이메일 회원 로그인을 한다.") @PostMapping(value = "/signin") @@ -39,6 +51,17 @@ public class SignController { return responseService.getSingleResult(jwtTokenProvider.createToken(String.valueOf(user.getMsrl()), user.getRoles())); } + @ApiOperation(value = "소셜 로그인", notes = "소셜 회원 로그인을 한다.") + @PostMapping(value = "/signin/{provider}") + public SingleResult signinByProvider( + @ApiParam(value = "서비스 제공자 provider", required = true, defaultValue = "kakao") @PathVariable String provider, + @ApiParam(value = "소셜 access_token", required = true) @RequestParam String accessToken) { + + KakaoProfile profile = userService.getKakaoProfile(accessToken); + User user = userJpaRepo.findByUidAndProvider(String.valueOf(profile.getId()), provider).orElseThrow(CUserNotFoundException::new); + return responseService.getSingleResult(jwtTokenProvider.createToken(String.valueOf(user.getMsrl()), user.getRoles())); + } + @ApiOperation(value = "가입", notes = "회원가입을 한다.") @PostMapping(value = "/signup") public CommonResult signup(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, @@ -53,4 +76,26 @@ public class SignController { .build()); return responseService.getSuccessResult(); } + + @ApiOperation(value = "소셜 계정 가입", notes = "소셜 계정 회원가입을 한다.") + @PostMapping(value = "/signup/{provider}") + public CommonResult signupProvider(@ApiParam(value = "서비스 제공자 provider", required = true, defaultValue = "kakao") @PathVariable String provider, + @ApiParam(value = "소셜 access_token", required = true) @RequestParam String accessToken, + @ApiParam(value = "이름", required = true) @RequestParam String name) { + + KakaoProfile profile = userService.getKakaoProfile(accessToken); + Optional user = userJpaRepo.findByUidAndProvider(String.valueOf(profile.getId()), provider); + if(user.isPresent()) + throw new CUserExistException(); + + User inUser = User.builder() + .uid(String.valueOf(profile.getId())) + .provider(provider) + .name(name) + .roles(Collections.singletonList("ROLE_USER")) + .build(); + + userJpaRepo.save(inUser); + return responseService.getSuccessResult(); + } } diff --git a/src/main/java/com/rest/api/entity/User.java b/src/main/java/com/rest/api/entity/User.java index 83689e9..5156ad8 100644 --- a/src/main/java/com/rest/api/entity/User.java +++ b/src/main/java/com/rest/api/entity/User.java @@ -1,10 +1,7 @@ package com.rest.api.entity; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -28,10 +25,12 @@ public class User implements UserDetails { @Column(nullable = false, unique = true, length = 50) private String uid; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) - @Column(nullable = false, length = 100) + @Column(length = 100) private String password; @Column(nullable = false, length = 100) private String name; + @Column(length = 100) + private String provider; @ElementCollection(fetch = FetchType.EAGER) @Builder.Default diff --git a/src/main/java/com/rest/api/model/social/KakaoProfile.java b/src/main/java/com/rest/api/model/social/KakaoProfile.java new file mode 100644 index 0000000..7fa45b1 --- /dev/null +++ b/src/main/java/com/rest/api/model/social/KakaoProfile.java @@ -0,0 +1,19 @@ +package com.rest.api.model.social; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class KakaoProfile { + private Long id; + private Properties properties; + + @Getter + @Setter + private static class Properties { + private String nickname; + private String thumbnail_image; + private String profile_image; + } +} diff --git a/src/main/java/com/rest/api/repo/UserJpaRepo.java b/src/main/java/com/rest/api/repo/UserJpaRepo.java index 3a267ec..f555f94 100644 --- a/src/main/java/com/rest/api/repo/UserJpaRepo.java +++ b/src/main/java/com/rest/api/repo/UserJpaRepo.java @@ -7,4 +7,6 @@ import java.util.Optional; public interface UserJpaRepo extends JpaRepository { Optional findByUid(String email); + + Optional findByUidAndProvider(String uid, String provider); } diff --git a/src/main/java/com/rest/api/service/user/UserService.java b/src/main/java/com/rest/api/service/user/UserService.java new file mode 100644 index 0000000..d431ea9 --- /dev/null +++ b/src/main/java/com/rest/api/service/user/UserService.java @@ -0,0 +1,35 @@ +package com.rest.api.service.user; + +import com.google.gson.Gson; +import com.rest.api.advice.exception.CCommunicationException; +import com.rest.api.model.social.KakaoProfile; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@RequiredArgsConstructor +@Service +public class UserService { + + private final RestTemplate restTemplate; + private final Environment env; + private final Gson gson; + + public KakaoProfile getKakaoProfile(String accessToken) { + // Set header : Content-type: application/x-www-form-urlencoded + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", "Bearer " + accessToken); + + // Set http entity + HttpEntity> request = new HttpEntity<>(null, headers); + ResponseEntity response = restTemplate.postForEntity(env.getProperty("spring.social.kakao.url.profile"), request, String.class); + if (response.getStatusCode() == HttpStatus.OK) + return gson.fromJson(response.getBody(), KakaoProfile.class); + else + throw new CCommunicationException(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f7ff651..84d4c43 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,7 @@ spring: secret: govlepel@$& social: kakao: - client_id: XXXXXXXXXXXXXXXXXXXXXXXX # 앱생성시 받은 REST API 키 + client_id: XXXXXXXXXXXXXXXXXXXXXXXXXX # 앱생성시 받은 REST API 키 redirect: /social/login/kakao url: login: https://kauth.kakao.com/oauth/authorize diff --git a/src/main/resources/i18n/exception_en.yml b/src/main/resources/i18n/exception_en.yml index 47db4aa..ff111c2 100644 --- a/src/main/resources/i18n/exception_en.yml +++ b/src/main/resources/i18n/exception_en.yml @@ -12,4 +12,10 @@ entryPointException: msg: "You do not have permission to access this resource." accessDenied: code: "-1003" - msg: "A resource that can not be accessed with the privileges it has." \ No newline at end of file + msg: "A resource that can not be accessed with the privileges it has." +communicationError: + code: "-1004" + msg: "An error occurred during communication." +existingUser: + code: "-1005" + msg: "You are an existing member." \ No newline at end of file diff --git a/src/main/resources/i18n/exception_ko.yml b/src/main/resources/i18n/exception_ko.yml index 7213648..e09b6dd 100644 --- a/src/main/resources/i18n/exception_ko.yml +++ b/src/main/resources/i18n/exception_ko.yml @@ -12,4 +12,10 @@ entryPointException: msg: "해당 리소스에 접근하기 위한 권한이 없습니다." accessDenied: code: "-1003" - msg: "보유한 권한으로 접근할수 없는 리소스 입니다." \ No newline at end of file + msg: "보유한 권한으로 접근할수 없는 리소스 입니다." +communicationError: + code: "-1004" + msg: "통신 중 오류가 발생하였습니다." +existingUser: + code: "-1005" + msg: "이미 가입한 회원입니다. 로그인을 해주십시오." \ No newline at end of file