거의  2~3달 가까이 퇴근하고 틈틈이 만들지만 아직까지 Spring Bean Handling에 미숙함이 많아 시간이 많이 걸리지만 Spring Lifecycle에 대해 좀 더 심도 있게 파고드는 부분은 정말 도움이 많이된다.

또한, Spring 대부분이 상속(Extend) 또는 구현(Implement)를 통해 커스텀(Custom) 되는 부분으로 인해 소스를 Custom 전 파악 후 하는 부분에 있어서 나도 개발이 자연스러워지는 점에선 큰 도움이 되고있다.

※ 출처 : https://javadeveloperzone.com/spring-boot/spring-security-oauth-2-0-authentication-server/

1. 개발 요지

 1) Spring F/W 자체로는 구성이 쉬움. But Security 와 OAuth2를 적용하기엔 난이도가 급상승하여 

개발을 막 시작한 사용자들이 개발하기엔 난이도가 존재한다.

→실제로 프로젝트에도 Spring Security를 적용하지 않는 경우가 허다하고 대체로 AspectJ AOP 형태(Intercept)로 개발하는 경우가 많다.

→ 운영 입장에서도 Spring Security의 Lifecycle을 모른다면 쉽지 않음.

→ Spring Security  는 Role으로 시작해서 Role로 끝나는 FW으로 봐도 무방하다.

 

 2) OAuth2는 정규스펙이 아닌 비정규 스펙으로 정의하여 만들어둔 기업이 몇몇 존재한다.

ex) /authorize => /token 시 Response 된 access_token으로 사용자(Member) 정보를 가져오는 경우가 일반적인데 

Payco 처럼 /token 발행시 access_token_secret , access_token 이중으로 발행하여, 초기에 authorize, token시 발급받은 access_token_secret과 API가 일치하지 않아 별도로 로직 개발이 필요한 경우 매우 난감하다.

// 비정규 스펙으로 인해 별도로 userInfo 부분만 Custom 하였다.
String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.add("client_id", oAuth2UserRequest.getClientRegistration().getClientId());
httpHeaders.add("access_token", oAuth2UserRequest.getAccessToken().getTokenValue());

URI uri = UriComponentsBuilder.fromUriString(oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()).build().toUri();

RequestEntity requestEntity = new RequestEntity(httpHeaders, HttpMethod.POST, uri);

ResponseEntity<Map> response = restTemplate.exchange(requestEntity, Map.class);

Map<String, Object> userAttributes = (Map) response.getBody();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = oAuth2UserRequest.getAccessToken();
token.getScopes().forEach(scope -> authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope)));
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);

 

2. 개발 방향

 1) 별도 envConfig.yml(가칭) 에서 active 목록에 ex) Google, Payco 등을 넣고 @Component로 설정 Bean을 생성하면 자동으로 로그인이 사용 될 수 있는 구조가 되도록 작성하려한다.

  → spring-security-config-5.5.0.jar > CommonOAuth2Provider.class 

  ※ 종류가 적다 : Google, Github, Facebook, Okta

  → 다이나믹(Dynamic) 형태로 국내 지원을 포함한 FW Library를 만들어 보고싶다.

  → 개발 완료 

     - 국내 : Kakao, Naver, Payco

     - 해외 : Google, Facebook, Github, Gitlab

  → 개발 중 

     - 국내 : Searching....

     - 해외 : TwitchOkta

 

3. 개발 구조

  1) 설정 파일 

Facebook 설정 Yaml Formatting

  2) 소스 구성

   a. SpringSecurityUserAttributeFaceBook.java

   b. OAuthForFacebook.java (@Configuration) / implement OAuthAPI.java 

 

  →  Q : @Value를 이용하여 설정을 사용한 이유

       A : OAuth 종류별로 Configuation 파일 생성됨에 따라 일회성 클래스인데 불구하고 로드가 계속 필요하기 때문에 

자원 및 구조 낭비 방지를 위해 Class in values 이용.

 

  →  Q : Apache-commons BeanUtils 대신 Jackson ObjectMapper 이용 이유

       A : Apche-commons BeanUtils의 경우 describe , Populate시 Property가 완전 일치되어야만 정상 처리 됨에 따라 핸들링이 나쁜 점이 존재하여 Jackson ObjectMapper를 이용하면 @JsonIgnore 와 같은 Jackson 내장 Annotation을 이용하여 처리함.

@Configuration(value = "oAuthFacebook")
@Getter
@Setter
@ToString
@Slf4j
public class OAuthForFacebook implements OAuthAPI {

	private static String svrName = "Facebook";

	@Autowired
	private JsonMapper jsonMapper;

	@Value(value = "${OAuth.Facebook.api}")
	private String oAuthKey;

	@Value(value = "${OAuth.Facebook.secret}")
	private String oAuthSecret;

	@Value(value = "${OAuth.Facebook.callback}")
	private String oAuthcallback;

	@Value(value = "${OAuth.Facebook.request.profile}")
	private String requestProfile;

	@Value(value = "${OAuth.Facebook.request.authorize}")
	private String requestAuthorize;

	@Value(value = "${OAuth.Facebook.request.token}")
	private String requestToken;

	@Value(value = "${OAuth.Facebook.request.user-name-attribute}")
	private String requestUserNameAttribute;

	@Value(value = "${OAuth.Facebook.response.redirectUri}")
	private String responseRedirectUri;

	@Value(value = "${OAuth.Facebook.response.redirectUriStr}")
	private String responseRedirectUriStr;

	@Value(value = "${OAuth.Facebook.scope}")
	private String[] scope;

	@Override
	public String getOAuthApiKey() {
		return this.getOAuthApiKey();
	}

	@Override
	public LinkedMultiValueMap getParameter() {
		return null;
	}

	@Override
	public SessionUser<OAuthForFacebook> getOAuthForUserInfo() {
		return null;
	}

	@Override
	public SessionUser<OAuthForFacebook> getOAuthForProfile() {
		return null;
	}

	@Override
	public ClientRegistration clientRegistration() {
		return ClientRegistration.withRegistrationId(this.svrName)
				.clientId(this.oAuthKey)
				.clientSecret(this.oAuthSecret)
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
				.scope(this.scope)
				.userNameAttributeName(this.requestUserNameAttribute)
				.authorizationUri(this.requestAuthorize)
				.tokenUri(this.requestToken)
				.userInfoUri(this.requestProfile)
				.clientName(this.svrName)
				.build();
	}

	@Override
	public String getPopupUrl() throws Exception {
		return new URIBuilder("/oauth2/authorization/" + this.svrName).toString();
	}

	@Override
	public SpringSecurityUser<OAuthForFacebook> of(String userNameAttributeName, Map<String, Object> attributes) {
		Map<String, Object> pictureMap = (Map<String, Object>) attributes.get("picture");
		Map<String, Object> pictureDataMap = (Map<String, Object>) pictureMap.get("data");

		SpringSecurityUserAttributeFaceBook springSecurityUserAttributeFaceBook = jsonMapper.convertValue(attributes, SpringSecurityUserAttributeFaceBook.class);
		springSecurityUserAttributeFaceBook.setPicture_width((Integer) pictureDataMap.get("width"));
		springSecurityUserAttributeFaceBook.setPicture_height((Integer) pictureDataMap.get("height"));
		springSecurityUserAttributeFaceBook.setPicture_is_silhouette((Boolean) pictureDataMap.get("is_silhouette"));
		springSecurityUserAttributeFaceBook.setPicture_url((String) pictureDataMap.get("url"));

		return SpringSecurityUser.create()
				.socialData(springSecurityUserAttributeFaceBook)
				.name(springSecurityUserAttributeFaceBook.getName())
				.picture(springSecurityUserAttributeFaceBook.getPicture_url())
				.email(springSecurityUserAttributeFaceBook.getEmail())
				.socialKey(springSecurityUserAttributeFaceBook.getId())
				.addAuthority(Role.GUEST)
				.addAuthority(Role.FACEBOOK)
				;
	}

	@Override
	public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws Exception {
		return null;
	}

	@Override
	public boolean isDefaultLogin() throws Exception {
		return true;
	}
}

 

#스프링시큐리티스프링시큐리티,#스프링 OAuth2스프링 OAuth2,#Naver OAuth2Naver OAuth2,#Kakao OAuth2Kakao OAuth2,#Payco OAuth2Payco OAuth2,#Google OAuth2Google OAuth2,#Facebook OAuth2Facebook OAuth2,#Github OAuth2Github OAuth2,#Gitlab OAuth2Gitlab OAuth2,#스프링 시큐리티 OAuth2스프링 시큐리티 OAuth2,

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기