본문 바로가기

Spring

Spring Boot security Oauth2 로그인 연동 (구글/카카오)

728x90
반응형

Spring security


스프링 시큐리티는 사용자 정의가 가능한 인증 및 액세스 제어 프레임워크다. Spring 기반 애플리케이션을 보호하기 위한 사실상의 표준이다. 인증과 권한 부여를 제공하는데 중점을 둔 프레임워크.

 

Spring Security Oauth 프로젝트는 더이상 제공되지 않고 Spring Security에서 포괄적인 Oauth2 지원을 제공한다.

 

 

Google Oauth2 연동 SetUp


1. 프로젝트생성

구글 개발자 콘솔

 

API Project를 선택

 

🔽

 

새 프로젝트를 만든다.

🔽

 

 

2. API 및 서비스 > Oauth동의화면

 

🔽

 

앱정보 입력

Oauth동의 화면에서 앱이름과 사용자지원이메일, 개발자 연락처 정보를 입력한다.  

 

🔽

 

범위에서 범위 추가 또는 삭제를 통해서 userInfo.email, profile, openid를 추가한다. 

선택적 추가 정보는 입력하지 않아도 된다.

 

3. Oauth Client ID  생성

사용자 인증 정보 > Oauth 클라이언트 ID

 

🔽

 

리디렉션 URI에는 

http://localhost:8080/login/oauth2/code/google을 넣어준다.

생성하고 나면 클라이언트 ID와 Secret을 발급받을 수 있다. 

 

 

 

Kakao Oauth2 연동 SetUp


1. 애플리케이션 생성

https://developers.kakao.com/

개발자 콘솔 로그인 > 내 애플리케이션 

애플리케이션 추가하기

🔽

 

적당한 이름을 넣고 저장

 

2. 플랫폼 등록

플랫폼 > Web > Web플랫폼 등록

 

🔽

 

도메인 등록 

 

3. 카카오 로그인 활성화

카카오로그인 > 활성화 설정 > ON

 

🔽

Redirect URI 등록 (http://localhost:8080/login/oauth2/code/kakao)

 

4. 동의항목 설정

동의항목 > 개인 정보 > 닉네임 > 설정 

 

🔽

 

필수 동의 체크 후 저장 

 

개인정보 항목에서 닉네임, 프로필 사진, 카카오계정을 필수동의(카카오계정은 선택동의) 체크 후 저장한다. 

 

카카오 Oauth2 클라이언트 ID는 애플리케이션 요약 정보에 있는 앱키 중 REST API 키이다.

시크릿키는 필수가 아니기 때문에 클라이언트 아이디만 사용해도 된다. 

 

 

Spring Boot Setting


build.gradle

implementation 'org.springframework.security:spring-security-oauth2-client'
implementation 'org.springframework.security:spring-security-oauth2-jose'
implementation 'org.springframework.boot:spring-boot-starter-security'

spring-security-auth2-client: 클라이언트 자동 인증 설정을 위한 라이브러리

spring-security-oauth2-jose: JWT(Json Web Token) 관련한 권한을 안전하게 전송하기 위한 프레임워크(JWT의 암/복호화 및 일정한 기능 제공)

spring-boot-starter-security: Oauth2 관련 설정을 security에서 제공한다.

 

application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: google-client-id
            client-secret: google-secret
        provider:
          kakao:
            authorization_uri: https://kauth.kakao.com/oauth/authorize
            token_uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user_name_attribute: id
            
custom:
  oauth2:
    kakao:
      client-id: kakao-client-id

시큐리티의 Oauth2에서는 여러 소셜 정보를 기본값으로 제공해주고 있다. 

spring.security.oauth2.client.registration은 Oauth client property의 기본 prefix다.

기본 제공해주는 소셜 정보는 google, facebook, github, okta로 카카오는 커스터마이징으로 등록해야한다.

provider에 카카오 oauth2로 인증할 url 정보를 추가해주고 프로퍼티에 카카오 클라이언트 아이디를 추가한다. 

 

CustomOauth2Provider.java

public enum CustomOauth2Provider {
    KAKAO {
        @Override
        public ClientRegistration.Builder getBuilder(String registrationId, OAuth2ClientProperties.Provider provider) {
            ClientRegistration.Builder builder = getBuilder(registrationId,
                    ClientAuthenticationMethod.CLIENT_SECRET_POST, DEFAULT_LOGIN_REDIRECT_URL);
            builder.scope("profile_nickname", "profile_image", "account_email");
            builder.authorizationUri(provider.getAuthorizationUri());
            builder.tokenUri(provider.getTokenUri());
            builder.userInfoUri(provider.getUserInfoUri());
            builder.userNameAttributeName(provider.getUserNameAttribute());
            builder.clientName(registrationId);
            return builder;
        }
    };

    private static final String DEFAULT_LOGIN_REDIRECT_URL = "{baseUrl}/login/oauth2/code/{registrationId}";

    protected final ClientRegistration.Builder getBuilder(String registrationId,
                                                          ClientAuthenticationMethod method, String redirectUri) {
        ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId);
        builder.clientAuthenticationMethod(method);
        builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
        builder.redirectUri(redirectUri);
        return builder;
    }

    public abstract ClientRegistration.Builder getBuilder(String registrationId, OAuth2ClientProperties.Provider provider);

}

Kakao Provider를 만들어준다. authorizationUri와 tokenUri 등은 application.yml에서 설정한 프로바이더 설정 정보를 가지고 등록할 수 있다. 나머지 정보들은 CommonOauth2Provider를 참고하면 된다.

 

SecurityConfig.java

@Configuration
//웹에서 시큐리티 기능을 사용하겠다는 어노테이션
// 자동설정이 적용된다.
@EnableWebSecurity
// 권한, 요청 등 세부 설정을 위해 WebSecurityConfigurerAdapter를 상속받고
// configure(HttpSecurity http)메소드를 오버라이드하여 시큐리티 설정
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        http.authorizeRequests() // 인증 메커니즘을 요청한 HttpServletRequest기반으로 설정
                // 요청패턴을 리스트 형식으로 설정
                .antMatchers("/", "/oauth2/**")
                // 누구나 접근을 허용
                        .permitAll()
                .antMatchers("/kakao").hasAuthority(SocialType.KAKAO.getRoleType())
                // 설정한 요청이의외 리퀘스트는 인증된 사용자만 요청할 수 있음
                .anyRequest().authenticated()
            .and()
                .oauth2Login() // 기본적으로 제공되는 구글, 페이스북 인증 적용
                .defaultSuccessUrl("/loginSuccess")
                .failureUrl("/loginFailure")
            .and()
                // 응답헤더에 대한 설정
                // XFrameOptionsHeaderWriter의 최적화 설정을 허용하지 않음
                    .headers().frameOptions().disable()
            .and()
                    .exceptionHandling()
                    //인증의 진입지점 (인증되지 않은 사용자가 리퀘스트를 요청하면 /login으로 이동  
                    .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
            .and()
                    .formLogin()
                    // 로그인 성공시 아래 경로로 포워딩  
                    .successForwardUrl("/list")
            .and()
                    .logout()
                    .logoutUrl("/logout")
                    //로그아웃 요청시 삭제될 쿠키값 지정
                    .deleteCookies("JSESSIONID")
                    // 세션무효화 수행 
                    .invalidateHttpSession(true)
            .and()
                    // 첫 번째 인자보다 먼저 시작될 필터를 등록
                    .addFilterBefore(filter, CsrfFilter.class)
                    .csrf().disable();
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(
            // Oauth2ClientProperties에는 구글, 페이스북의 정보가 들어있고 카카오는 따로 등록했기 때문에
            // @Value어노테이션을 이용해 수동으로 불러옴
            OAuth2ClientProperties oAuth2ClientProperties, @Value("${custom.oauth2.kakao.client-id}") String kakaoClientId) {

        List<ClientRegistration> registrations = oAuth2ClientProperties.getRegistration()
                .keySet().stream().map(client -> getRegistration(oAuth2ClientProperties, client))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

		// 카카오는 커스텀하게 만든 프로바이더를 통해 등록해준다 
        registrations.add(getProvider(oAuth2ClientProperties, kakaoClientId));
        return new InMemoryClientRegistrationRepository(registrations);
    }

    // 구글, 페이스북 인증 정보를 빌드시켜줌
    private ClientRegistration getRegistration(OAuth2ClientProperties oAuth2ClientProperties, String client) {
        if("google".equals(client)) {
            OAuth2ClientProperties.Registration registration = oAuth2ClientProperties
                    .getRegistration().get("google");
            return CommonOAuth2Provider.GOOGLE.getBuilder(client)
                    .clientId(registration.getClientId())
                    .clientSecret(registration.getClientSecret())
                    .build();
        }

        return null;
    }

	// 카카오 인증 정보를 빌드 
    private ClientRegistration getProvider(OAuth2ClientProperties oAuth2ClientProperties, String clientId) {
        OAuth2ClientProperties.Provider provider =oAuth2ClientProperties.getProvider().get("kakao");
        return CustomOauth2Provider.KAKAO.getBuilder("kakao", provider)
                .clientId(clientId)
                .build();
    }


}

 

 

LoginController.java

// 인증 성공 후 호출되는 API
@GetMapping(value = "/loginSuccess")
public String loginComplete(@SocialUser User user) {
    return "redirect:/list";
}

 

SocialUser.java

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface SocialUser {
}

 

 

UserArgumentResolver.java

//HandlerMethodArgumentResolver는 전략패턴의 일종으로
// 컨트롤러 메서드에 특정조건에 해당하는 파라미터가 있으면 생성한 로직 처리 후
// 해당파라미터에 바인딩해주는 전략 인터페이스
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    private UserRepository userRepository;

    public UserArgumentResolver(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 해당 파라미터를 지원할지 여부를 반환
    // true이면 resolveArgument메서드가 수행됨
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 파라미터에 @SocialUser어노테이션이 있고 타입이 User인 파라미터만 true를 반환
        // 처음 한 번 체크된 부분은 캐시되어 동일 호출은 체크하지 않고 결과값을 바로 반환
        return parameter.getParameterAnnotation(SocialUser.class) != null &&
                parameter.getParameterType().equals(User.class);
    }

    // 파라미터 인잣값에 대한 정보를 바탕으로 실제 객체를 생성해서 해당 파라미터에 바인딩
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpSession session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
        User user = (User) session.getAttribute("user");
        return getUser(user, session);
    }

    private User getUser(User user, HttpSession session) { // 인증된 User객체를 만드는 메서드
        if(user == null) {
            try {
                // 액세스 토큰까지 제공한다는 의미에서 Oauth2AuthenticationToken 지원
                // SecurityContextHolder에서 토큰을 가져온다.
                OAuth2AuthenticationToken authentication = (OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
                // 개인정보를 getAttributes메서드를 사용해 불러온다.
                Map<String, Object> map = authentication.getPrincipal().getAttributes();
                // getAuthorizedClientRegistrationdId()로 인증된 소셜미디어를 확인할 수 있다.
                User convertUser = convertUser(authentication.getAuthorizedClientRegistrationId(), map);
                user = userRepository.findByEmail(convertUser.getEmail());
                if (user == null) {
                    user = userRepository.save(convertUser);
                }

                setRoleIfNotSave(user, authentication, map);
                session.setAttribute("user", user);
            }catch (ClassCastException e) {
                return user;
            }
        }
        return user;
    }


    private User convertUser(String authority, Map<String, Object> map) { // 사용자의 인증된 소셜미디어타입에 따라 빌더를 이용하여 User객체를 만듬
        if(SocialType.GOOGLE.isEquals(authority)) return getModernUser(SocialType.GOOGLE, map);
        else if(SocialType.KAKAO.isEquals(authority)) return getKaKaoUser(map);
        return null;
    }

    private User getModernUser(SocialType socialType, Map<String, Object> map) {
        return User.builder()
                .name(String.valueOf(map.get("name")))
                .email(String.valueOf(map.get("email")))
                .principal(String.valueOf(map.get("id")))
                .socialType(socialType)
                .createdDate(LocalDateTime.now())
                .build();
    }

    private User getKaKaoUser(Map<String, Object> map) {
        HashMap<String, String> propertyMap = (HashMap<String, String>) map.get("properties");
        return User.builder()
                .name(propertyMap.get("nickname"))
                .email(String.valueOf(map.get("kaccount_email")))
                .principal(String.valueOf(map.get("id")))
                .socialType(SocialType.KAKAO)
                .createdDate(LocalDateTime.now())
                .build();
    }

    // 인증된 authentication이 권한을 갖고 있는지 체크
    private void setRoleIfNotSave(User user, OAuth2AuthenticationToken authenticationToken, Map<String, Object> map) {
        if(!authenticationToken.getAuthorities().contains(
                new SimpleGrantedAuthority(user.getSocialType().getRoleType()))) {
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(map, "N/A",
                            AuthorityUtils.createAuthorityList(user.getSocialType().getRoleType())));
        }
    }
}

 

 

 

출처

처음 배우는 스프링부트2

 

728x90
반응형