의존 관계 때문에
spring-boot-starter-oauth2-client
를 사용하지 않고 Feign Client로 구현하고 싶었다.
OAuth2 라이브러리를 사용하게 되면 Spring Security가 Application 단계에서 구현되어 main을 가지는 각각의 프로젝트들에 적용해줘야하는 번거로움이 생긴다. 그렇게 된다면 multi-module을 할 필요가 없어지는 것이다. 따라서 의존성을 줄여주기 위해 FeignClient로 구현하였다.
RestTemplate 로 구현된 코드들이 꽤 있어서 구현에는 어렵지 않았지만 몇가지 트러블 슈팅이 있었다. 아래 코드를 보고 겪었던 트러블 슈팅을 포스팅 하겠다.
oauth2:
kakao:
infoUrl: https://kapi.kakao.com
baseUrl: https://kauth.kakao.com
clientId: 5d38e6dc1f62b10c9r3dc2e34fe6d24e62
redirectUri: http://localhost/api/v1/login/kakao/oauth2
secretKey: I1nEw554k2oFM1n32P126Yro7NrRVU2G
package com.send.moduleinfra.feign.sns.kakao;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Component
@ConfigurationProperties(prefix = "oauth2.kakao")
public class KakaoInfo {
private String baseUrl;
private String clientId;
private String redirectUri;
private String secretKey;
public String kakaoUrlInit() {
Map<String, Object> params = new HashMap<>();
params.put("client_id", getClientId());
params.put("redirect_uri", getRedirectUri());
params.put("response_type", "code");
String paramStr = params.entrySet().stream()
.map(param -> param.getKey() + "=" + param.getValue())
.collect(Collectors.joining("&"));
return getBaseUrl()
+"/oauth/authorize"
+ "?"
+ paramStr;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
public String getBaseUrl() {
return baseUrl;
}
public String getClientId() {
return clientId;
}
public String getRedirectUri() {
return redirectUri;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
}
@GetMapping("/login/kakao")
public ResponseEntity<Object> kakaoLogin() {
HttpHeaders httpHeaders = accountService.kakaoLogin();
return httpHeaders != null ?
new ResponseEntity<>(httpHeaders,HttpStatus.SEE_OTHER):
ResponseEntity.badRequest().build();
}
public HttpHeaders kakaoLogin(){
return createHttpHeader(kakaoInfo.kakaoUrlInit());
}
private static HttpHeaders createHttpHeader(String str) {
try {
URI uri = new URI(str);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setLocation(uri);
return httpHeaders;
} catch (URISyntaxException e) {
e.printStackTrace();
}
return null;
}
package com.send.moduleinfra.feign.sns.kakao.dto;
import com.send.moduleinfra.feign.sns.google.GoogleInfo;
import com.send.moduleinfra.feign.sns.kakao.KakaoInfo;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class KLoginTokenReq {
private String code;
private String client_id;
private String client_secret;
private String redirect_uri;
private final String grant_type = "authorization_code";
public static KLoginTokenReq newInstance(KakaoInfo googleInfo, String code){
return KLoginTokenReq.builder()
.client_id(googleInfo.getClientId())
.client_secret(googleInfo.getSecretKey())
.redirect_uri(googleInfo.getRedirectUri())
.code(code)
.build();
}
// kakao는 Content-Type 을 application/x-www-form-urlencoded 로 받는다.
// FeignClient는 기본이 JSON으로 변경하니 아래처럼 데이터를 변환 후 보내야 한다.
@Override
public String toString() {
return
"code=" + code + '&' +
"client_id=" + client_id + '&' +
"client_secret=" + client_secret + '&' +
"redirect_uri=" + redirect_uri + '&' +
"grant_type=" + grant_type;
}
}
package com.send.moduleinfra.feign.sns.kakao.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class KLoginTokenRes {
private String access_token; // 애플리케이션이 Google API 요청을 승인하기 위해 보내는 토큰
private String expires_in; // Access Token의 남은 수명
private String refresh_token; // 새 액세스 토큰을 얻는 데 사용할 수 있는 토큰
private String scope;
private String token_type; // 반환된 토큰 유형(Bearer 고정)
private String id_token;
private String refresh_token_expires_in;
public String getAccess_token() {
return "Bearer "+access_token;
}
}
package com.send.moduleinfra.feign.sns.kakao.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class KTokenInfoRes {
private String id;
private String expires_in;
private String app_id;
}
package com.send.moduleinfra.feign.sns.kakao;
import com.send.moduleinfra.feign.sns.kakao.config.KakaoFeignConfiguration;
import com.send.moduleinfra.feign.sns.kakao.dto.KLoginTokenRes;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "kakaoLoginFeignClient", url = "${oauth2.kakao.baseUrl}", configuration = KakaoFeignConfiguration.class)
@Component
public interface KakaoLoginFeignClient {
@PostMapping(value = "/oauth/token")
KLoginTokenRes getToken(
@RequestBody String kLoginTokenReq);
}
package com.send.moduleinfra.feign.sns.kakao.config;
import com.send.moduleinfra.feign.config.FeignClientExceptionErrorDecoder;
import feign.Logger;
import feign.RequestInterceptor;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
public class KakaoFeignConfiguration {
@Bean
public RequestInterceptor requestInterceptor() {
return template -> template.header("Content-Type", "application/x-www-form-urlencoded");
}
@Bean
public ErrorDecoder errorDecoder() {
return new FeignClientExceptionErrorDecoder();
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
package com.send.moduleinfra.feign.sns.kakao;
import com.send.moduleinfra.feign.config.FeignClientConfiguration;
import com.send.moduleinfra.feign.sns.google.dto.GTokenInfoRes;
import com.send.moduleinfra.feign.sns.kakao.dto.KTokenInfoRes;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "kakaoInfoFeignClient", url = "${oauth2.kakao.infoUrl}", configuration = FeignClientConfiguration.class)
@Component
public interface KakaoInfoFeignClient {
@GetMapping("/v1/user/access_token_info")
KTokenInfoRes getInfo(@RequestHeader(name = "Authorization") String Authorization);
}
@GetMapping("/login/kakao/oauth2")
public SingleResult<Object> redirectKakaoLogin(@RequestParam(value = "code")String code) {
return responseService.getSingleResult(accountService.getKakaoTokenWithInfo(code));
}
public Object getKakaoTokenWithInfo(String code) {
String userId = SocialType.K.getType() +"_" + getKakaoInfo(code).getId();
Users users = userRepository.findByLoginId(userId).orElse(null);
if(users == null){
return SocialInfoRes.newInstance(userId,socialRandomPassword(userId),SocialType.K);
}
return createToken(users);
}
private KTokenInfoRes getKakaoInfo(String code) {
return kakaoInfoFeignClient
.getInfo(
kakaoLoginFeignClient
.getToken(
KLoginTokenReq.newInstance(kakaoInfo, code).toString())
.getAccess_token());
}
private String socialRandomPassword(String userId) {
String systemMil = String.valueOf(System.currentTimeMillis());
return passwordEncoder.encode(userId + systemMil);
}
private LoginRes createToken(Users user) {
return LoginRes.of(jwtProvider.createAccessToken(user.getLoginId(), user.getGroup().getFuncList()), jwtProvider.createRefreshToken(user.getLoginId()));
}
package com.send.apiauth.domain.auth.res;
import com.send.moduledomain.domain.user.entity.SocialType;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SocialInfoRes {
private String userId;
private String password;
private String type;
private String name;
public static SocialInfoRes newInstance(String userId, String password, SocialType socialType){
return SocialInfoRes.builder()
.type(socialType.getType())
.password(password)
.userId(userId)
.build();
}
}
FeignClient로 소셜로그인을 구현해 보았다. OAuth2를 의존하지 않게 됨으로써 모듈의 분리, 재결합 등을 수월하게 할 수 있고, 코드의 재사용성 또한 좋아진다. 지금 구현한 코드는 어디서나 다시 재활용 하여도 의존하지 않기때문에 몇 가지 데이터만 있으면 다른프로젝트에 이식할 수 있는 환경이 되었다.