- build.gradle
implementation group: 'org.springframework.security', name: 'spring-security-oauth2-client', version: '5.6.3'
implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '9.30.1'
implementation group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.70'
- application.yml
- application-oauth.yml를 include 한다.
- (application.yml에 spring security oauth2 client를 설정했다면 무시)
spring:
profiles:
include: oauth
.
.
.
- application-oauth.yml
- . p8파일은 키파일이며 한번 다운로드하면 다시 다운로드할 수 없으니 유의해야 함. 파일이름에 확장자인. p8 포함해서 기재
spring:
config:
activate:
on-profile: {profile ex) local, develop...}
security:
oauth2:
client:
registration:
apple:
clientId: {clientId}
clientSecret: {.p8 파일 이름}/{KeyId}/{TeamId}
redirect-uri: {domain}/login/oauth2/code/apple
authorization-grant-type: authorization_code
client-authentication-method: POST
client-name: Apple
scope:
- email
provider:
apple:
authorizationUri: https://appleid.apple.com/auth/authorize?response_mode=form_post
tokenUri: https://appleid.apple.com/auth/token
- CustomRequestEntityConverter.java
- 애플의 token uri를 호출할 파라미터를 Custom하는 소스
public class CustomRequestEntityConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;
public CustomRequestEntityConverter() {
defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
}
@Override
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest req) {
RequestEntity<?> entity = defaultConverter.convert(req);
String registrationId = req.getClientRegistration().getRegistrationId();
MultiValueMap<String, String> params = (MultiValueMap<String,String>) entity.getBody();
//Apple일 경우 secret key를 동적으로 세팅
if(registrationId.contains("apple")){
params.set("client_secret", createAppleClientSecret(params.get("client_id").get(0), params.get("client_secret").get(0)));
}
return new RequestEntity<>(params, entity.getHeaders(),
entity.getMethod(), entity.getUrl());
}
//Apple Secret Key를 만드는 메서드
public String createAppleClientSecret(String clientId, String secretKeyResource) {
String clientSecret = "";
//application-oauth.yml에 설정해놓은 apple secret Key를 /를 기준으로 split
String[] secretKeyResourceArr = secretKeyResource.split("/");
try {
InputStream inputStream = new ClassPathResource("{.p8 파일 경로}" + secretKeyResourceArr[0]).getInputStream();
File file = File.createTempFile("appleKeyFile",".p8");
try {
FileUtils.copyInputStreamToFile(inputStream, file);
} finally {
IOUtils.closeQuietly(inputStream);
}
String appleKeyId = secretKeyResourceArr[1];
String appleTeamId = secretKeyResourceArr[2];
String appleClientId = clientId;
PEMParser pemParser = new PEMParser(new FileReader(file));
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) pemParser.readObject();
PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo);
clientSecret = Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, appleKeyId)
.setIssuer(appleTeamId)
.setAudience("https://appleid.apple.com")
.setSubject(appleClientId)
.setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(privateKey, SignatureAlgorithm.ES256)
.compact();
} catch (IOException e) {
log.error("Error_createAppleClientSecret : {}-{}", e.getMessage(), e.getCause());
}
log.info("createAppleClientSecret : {}", clientSecret);
return clientSecret;
}
}
여기서 convert된 param은 라이브러리의 DefaultAuthorizationCodeTokenResponseClient 클래스의 getTokenResponse 메서드로 리턴되고 getResponse를 통해 애플로부터 AccessToken과 id_token을 가져온다.
- UserOAuth2Service.java
- 다른 provider 회원은 회원 정보를 provider로 부터 받아와야 하므로 delegate.loadUser(userRequest)를 호출하도록 하고 애플은 id_token을 decode해서 회원 정보를 가져오게 한다. 만약 애플 로그인만 연동했다면 분기 처리 안해도 된다.
@RequiredArgsConstructor
@Service
public class UserOAuth2Service extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
String registrationId = userRequest.getClientRegistration().getRegistrationId();
//access token을 이용해 provider 서버로부터 사용자 정보를 받아온다.
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
//Apple의 경우 id_token에 회원정보가 있으므로 회원정보 API 호출과정 생략
Map<String, Object> attributes;
if(registrationId.contains("apple")){
String idToken = userRequest.getAdditionalParameters().get("id_token").toString();
attributes = decodeJwtTokenPayload(idToken);
attributes.put("id_token", idToken);
}else{
OAuth2User oAuth2User = delegate.loadUser(userRequest);
attributes = oAuth2User.getAttributes();
}
//provider에서 넘어온 회원 정보에서 필요한 파라미터만 추출해 DTO로 변환
OAuthAttributes customAttributes = OAuthAttributes.of(registrationId, attributes);
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
customAttributes.getAttributes(),
customAttributes.getNameAttributeKey());
}
//JWT Payload부분 decode 메서드
public Map<String, Object> decodeJwtTokenPayload(String jwtToken){
Map<String, Object> jwtClaims = new HashMap<>();
try {
String[] parts = jwtToken.split("\\.");
Base64.Decoder decoder = Base64.getUrlDecoder();
byte[] decodedBytes = decoder.decode(parts[1].getBytes(StandardCharsets.UTF_8));
String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.readValue(decodedString, Map.class);
jwtClaims.putAll(map);
} catch (JsonProcessingException e) {
logger.error("decodeJwtToken: {}-{} / jwtToken : {}", e.getMessage(), e.getCause(), jwtToken);
}
return jwtClaims;
}
}
- SecurityConfiguration.java
- custom class들을 security config에 추가한다.
- successHandler는 추가해도되고 안해도 된다. 저 부분의 소스는 공개하지 않았다.
private final UserOAuth2Service userOAuth2Service;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
@Bean
protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.
.
.
.oauth2Login()
.tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient())
.and()
.userInfoEndpoint().userService(userOAuth2Service)
.and()
.successHandler(oAuth2AuthenticationSuccessHandler);
return httpSecurity.build();
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient(){
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter());
return accessTokenResponseClient;
}
}
반응형
'개발 > Spring' 카테고리의 다른 글
#2) Spring WebFlux(리액티브 프로그래밍 특징, 리액티브 스트림즈) (0) | 2024.01.30 |
---|---|
#1) Spring WebFlux (함수 호출 관점 동기/비동기 Blocking/Non-Blocking, 함수형 인터페이스, WebFlux 장점) (2) | 2023.12.03 |
#1) Spring Security Oauth2 Client + Apple 로그인 연동하기 (0) | 2023.03.27 |
컴포넌트 스캔 간단 정리 (0) | 2020.06.19 |
스프링 DI(의존주입) 간단 정리 (0) | 2020.06.19 |