OAuth2 와 Spring Security
카우치코딩에선 Firebase 를 기반으로 Google OAuth 를 개발할 것을 권장하지만 일반적으로는 Firebase 를 거치지 않고 사용하는 경우가 더 많으므로 Firebase 없이 OAuth 를 사용하는 방법을 알아보았다.
-
OAuth 2.0
: 인증을 위한 프로토콜
개발하고 있는 서비스 대신에 이미 유명한 거대 서비스(구글, 페이스북 등)를 통해서 인증을 거치고 유저 정보를 받아오는 방식이다. -
기본 용어
- Resource Owner
: 서비스의 사용자를 말한다
일반 유저를 의미하기도 하고, 서버간의 통신이라면 서버가 될 수도 있다. - Client
: 사용자가 이용하는 서비스를 말한다
어떤 서비스를 개발중이라면 그 서비스가 바로 Client 이다. - Resource Server
: OAuth 를 통해서 Resource 를 제공해주는 서버를 말한다 - Authorization Server
: OAuth 를 사용할때 인증, 인가를 담당하는 서버를 말한다
OAuth 기반의 소셜 로그인을 제공하는 유명한 거대 서비스(구글 등)에서 가지고 있는 Resource Server 와 Authroization Server 를 통해서 유저 정보를 인증 받고 유저 정보를 받아와서 현재 개발중인 서비스의 DB 에 저장하여 활용하는 것이 소셜로그인 방식이다.
- Resource Owner
-
흐름
전체적인 작업흐름을 velog 와 github 로그인을 예시로 들면 다음과 같음
출처
-
Grant Type
일반적으로 웹에서 사용되는 OAuth 는 Authorization Code Grant 방식을 사용한다. 토큰을 받아서 검증하는 방식을 의미하고 이 방식이 일반적인 웹 개발에선 가장 많이 사용된다.
참조 -
Authorization Code Grant
: Authorization Server 로 부터 프론트엔드에서 Authorization Code 를 발급받으면 이 발급받은 코드를 백엔드로 넘겨준뒤에 백엔드에서 Authorization Code 를 가지고 Authorization Server 에게 accessToken 을 요청한뒤, access token 과 refresh token 을 받아서 resource server 에 요청하여 사용자 정보를 받아오는 방식으로 처리하면 된다.
-
Spring Security + OAuth2
-
build.gradle
dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' }
-
OAuth 에 서비스 등록
: 이건 소셜로그인을 제공하는 서비스 마다 다르므로 해당 개발자 사이트 참조 -
application.yml 설정
: oauth 와 관련한 client-id, client-secret 등은 application.yml 에 직접 올리지 않고 별도의 yml 을 만들어서 처리함
그러므로 application.yml 에 별도의 yml 을 include 할 수 있도록 다음의 구문을 추가spring: profiles: include: oauth
그리고 oauth 정보를 담는 application-oauth.yml 추가
다른 OAuth 서비스도 아래와 거의 유사하게 작성됨
다만 Google, Github 같은 글로벌 서비스가 아니면 Spring Security 가 인식하지 못하므로 별도의 설정이 조금 더 필요함.spring: security: oauth2: client: registration: github: client-id: 6c34d9a6903231c5a301 client-secret: 비밀키 scope: name,email,avatar_url # naver 와 같은 국내 한정 서비스들은 아래처럼 별도의 추가 설정들이 필요함. naver: client-id: sCfhQHgPVQFFf8RTGjVe client-secret: 비밀키 redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" authorization_grant_type: authorization_code scope: name,email,profile_image client-name: Naver provider: naver: authorization_uri: https://nid.naver.com/oauth2.0/authorize token_uri: https://nid.naver.com/oauth2.0/token user-info-uri: https://openapi.naver.com/v1/nid/me user_name_attribute: response
-
SecurityConfig 설정
: Spring Security 에서 사용하도록 아래와 유사하게 작성@EnableWebSecurity // spring security 설정을 활성화시켜주는 어노테이션 public class SecurityConfig extends WebSecurityConfigurerAdapter { private final OAuthService oAuthService; public SecurityConfig(OAuthService oAuthService) { this.oAuthService = oAuthService; } @Override protected void configure(HttpSecurity http) throws Exception { http.oauth2Login() // OAuth2 로그인 설정 시작점 .userInfoEndpoint() // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당 .userService(oAuthService); // OAuth2 로그인 성공 시, 후작업을 진행할 UserService 인터페이스 구현체 등록 } }
-
Entity 설정
: OAuth2 Resource Server 로 부터 받아올 데이터를 기준으로 엔티티를 작성한다.@Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String oauthId; private String name; private String email; private String imageUrl; @Enumerated(EnumType.STRING) private Role role; // 생성자, 기타 메소드들 생략 } // OAuthService 로직 내에서 사용될 객체 public class UserProfile { private final String oauthId; private final String name; private final String email; private final String imageUrl; // 생성자 등 생략.. } public enum Role { GUEST("ROLE_GUEST"), USER("ROLE_USER"); private final String key; Role(String key) { this.key = key; } public String getKey() { return key; } }
-
OAuthService
: OAuth 서버에서 얻어온 유저정보를 가져오고 현재 개발중인 서비스의 DB 에 유저가 없으면 저장하고 있으면 기존 멤버를 반환하는 로직으로 작성한다.@Service @RequiredArgsConstructor public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { private final MemberRepository memberRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2UserService delegate = new DefaultOAuth2UserService(); OAuth2User oAuth2User = delegate.loadUser(userRequest); // OAuth 서비스(github, google, naver)에서 가져온 유저 정보를 담고있음 String registrationId = userRequest.getClientRegistration() .getRegistrationId(); // OAuth 서비스 이름(ex. github, naver, google) String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails() .getUserInfoEndpoint().getUserNameAttributeName(); // OAuth 로그인 시 키(pk)가 되는 값 Map<String, Object> attributes = oAuth2User.getAttributes(); // OAuth 서비스의 유저 정보들 UserProfile userProfile = OAuthAttributes.extract(registrationId, attributes); // registrationId에 따라 유저 정보를 통해 공통된 UserProfile 객체로 만들어 줌 Member member = saveOrUpdate(userProfile); // DB에 저장 return new DefaultOAuth2User( Collections.singleton(new SimpleGrantedAuthority(member.getRoleKey())), attributes, userNameAttributeName); } private Member saveOrUpdate(UserProfile userProfile) { Member member = memberRepository.findByOauthId(userProfile.getOauthId()) .map(m -> m.update(userProfile.getName(), userProfile.getEmail(), userProfile.getImageUrl())) // OAuth 서비스 사이트에서 유저 정보 변경이 있을 수 있기 때문에 우리 DB에도 update .orElse(userProfile.toMember()); return memberRepository.save(member); } }
-
OAuthAttributes enum
: 각 OAuth 서비스별 매칭을 위한 enumpublic enum OAuthAttributes { GITHUB("github", (attributes) -> { return new UserProfile( String.valueOf(attributes.get("id")), (String) attributes.get("name"), (String) attributes.get("email"), (String) attributes.get("avatar_url") ); }), GOOGLE("google", (attributes) -> { return new UserProfile( String.valueOf(attributes.get("sub")), (String) attributes.get("name"), (String) attributes.get("email"), (String) attributes.get("picture") ); }), NAVER("naver", (attributes) -> { Map<String, Object> response = (Map<String, Object>) attributes.get("response"); return new UserProfile( (String) response.get("id"), (String) response.get("name"), (String) response.get("email"), (String) response.get("profile_image") ); }); private final String registrationId; private final Function<Map<String, Object>, UserProfile> of; OAuthAttributes(String registrationId, Function<Map<String, Object>, UserProfile> of) { this.registrationId = registrationId; this.of = of; } public static UserProfile extract(String registrationId, Map<String, Object> attributes) { return Arrays.stream(values()) .filter(provider -> registrationId.equals(provider.registrationId)) .findFirst() .orElseThrow(IllegalArgumentException::new) .of.apply(attributes); } }
-
-
Firebase
: Firebase 를 써서 처리를 하게 되면, 위와 같이 Firebase 를 쓰지 않는 로직보다는 간소화 될 수는 있겠으나, Spring Security 내에서 어떻게 OAuth 로그인을 처리하는지에 대한 내부 구조를 상세하게 알기는 어렵다.
그러므로 어렵고 복잡하더라도 Firebase 를 통해서 OAuth 로그인 처리가 아닌 직접 구현을 시도해보는 것이 좋을 듯하다.
참고
Author And Source
이 문제에 관하여(OAuth2 와 Spring Security), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@vel1024/OAuth2-와-Spring-Security저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)