10. Spring/Security

07. 스프링 시큐리티 (Spring Security) - OAuth2 를 이용한 네이버, 카카오, 구글 인증 + JWT

THE HEYDAZE 2021. 8. 17. 11:22
OS Windows 10 Home 64bit 버전 1903 (OS 빌드 18362.836)
FrameWork SpringBoot 2.3.1.RELEASE ( 2.4 와 2.5 에서는 profile 전략이 다릅니다 )
Security spring-boot-starter-security 5
EditTool IntelliJ IDEA 2021.3.1
BuildTool Maven
FrontEnd Vue 2.x

요약

대략적인 구조

 

구성
  • 클래스 목록
    • config
      • AppProperties.java
        • Auth.class : JWT 토큰의 암호키와, 만료기간을 설정할 때 사용
        • OAuth2.class : 프론트 엔드 클라이언트가 /oauth2/authorize 요청에서 지정한 redirectUri 입니다
      • SecurityConfig.java : 백엔드의 전반적이 보안 설정
      • WebMvcConfig.java : cors 를 설정할 때 사용
    • controller
      • UserApi.java : 내 정보를 조회하기 위해 사용되는 api ( ROLE_USER 로 권한이 제한됨 )
      • AuthApi.java : 소셜로그인 이외에 서버에 이메일로 가입하고 싶은 경우 필요한 API
    • exception
      • BadRequestException.java : 잘못된 요청에 대해서 처리하기 위한 exception 클래스
      • OAuth2AuthenticationProcessingException.java :
        OAuth2 인증과정 중에서 발생하는 exception 클래스 AuthenticationException 클래스를 상속받고 있다
      • ResourceNotFoundException.java : DB에 존재하지 않는 것을 조회할 때 처리하기 위한 exception 클래스
    • entity
      • user
        • User.class : DB 의 User 테이블
      • model
        • Provider.Class (Enum) : 각 플랫폼명을 String 이 아닌 Enum 으로 관리하기 위한 클래스
    • payload
      • ApiResponse.java : 응답을 반환하기 위한 DTO 클래스
      • AuthResponse.java : 응답을 반환하기 위한 DTO 클래스
      • LoginRequest.java : 요청을 담기 위한 DTO 클래스
      • SignUpRequest.java : 요청을 담기 위한 DTO 클래스
    • repository
      • UserRepository.class (Interface) : DAO (Data Access Object) 클래스
    • security
      • oauth
        • user 
          • OAuth2UserInfoFactory.java : 각 플랫폼 클래스를 생성하기 위한 Factory 패턴을 사용한 클래스
          • OAuth2UserInfo.java (abstract) : 각 플랫폼 확장성을 위한 추상클래스
          • NaverOAuth2UserInfo.java : 네이버에 요청하여 유저정보를 응답받는 클래스
          • GoogleOAuth2UserInfo.java : 구글에 요청하여 유저정보를 응답받는 클래스
          • FacebookOAuth2UserInfo.java : 페이스북에 요청하여 유저정보를 응답받는 클래스
          • GithubOAuth2UserInfo.java : 깃허브에 요청하여 유저정보를 응답받는 클래스
          • KakaoOAuth2UserInfo.java : 카카오에 요청하여 유저정보를 응답받는 클래스
        • CustomOAuth2UserService.java :
          Spring OAuth2 에서 제공하는 OAuth2User 을 가공하여 OAuth2UserInfo 로 만들고 
          OAuth2UserInfo 에 Email 이 있는 지 검사와, A 라는 플랫폼으로 가입이 되어있는 데, B 플랫폼으로 가입 하려는 경우 검사를 진행하며, 이미 존재하는 계정에 경우에는 Update 를 진행하고,
          없는 경우에는 새로 Insert 하며, UserPrincipal 을 리턴한다
        • HttpCookieOAuth2AuthorizationRequestRepository.java
          OAuth2 프로토콜은 CSRF 공격을 방지하기 위해 state 매개 변수 사용을 권장합니다. 인증 중에 애플리케이션은 인증 요청에서 이 매개 변수를 전송하고, OAuth2 공급자는 OAuth2 콜백에서 변경되지 않은 이 매개 변수를 리턴합니다.
          응용 프로그램은 OAuth2 공급자에서 반환 된 state 매개 변수의 값을 초기에 보낸 값과 비교합니다. 일치하지 않으면 인증 요청을 거부합니다.
          이 흐름을 얻으려면 애플리케이션이 나중에 OAuth2 공급자에서 반환된 상태와 비교할 수 있도록 state 매개 변수를 어딘가에 저장해야합니다.
          단기(short-lived) 쿠키에 상태와 redirect_uri를 저장할 것입니다. 다음 클래스는 인증 요청을 쿠키에 저장하고 검색하는 기능을 제공합니다. - 출처 http://yoonbumtae.com/?p=3000
        • OAuth2AuthenticationFailureHandler.java :
          OAuth2 로그인 과정 중 실패했을 때 처리하는 클래스
          에러 메시지를 query 에 담아 redirect_uri 쿠키에 담겨져있던 곳으로 리다이렉트 된다
          -> http://localhost:3000/oauth2/redirect?error={errorMessage}
          cf ) redirect_uri 가 없는 경우 기본으로 http://localhost:8080/?error={errorMessage} 가 된다
        • OAuth2AuthenticationSuccessHandler : OAuth2 로그인 과정이 성공했을 때 처리하는 클래스
          -> http://localhost:3000/oauth2/redirect?token={jwt token}
          cf ) redirect_uri 가 없는 경우 기본으로 http://localhost:8080/?token={jwt token} 가 된다
      • CurrentUser.java (Annotation) :
        SecurityContextHolder.getContext().getAuthentication().getPrincipal() 을 통해 UserPrincipal 을
        가져오던 것을 간단하게 처리하기 위해 만들어진 어노테이션
        물론 @AuthenticationPrincipal  로 가능 하지만 더 짧고 명확한 명칭을 주기 위해 만든 것 뿐
      • CustomUserDetailsService.java : 
        TokenFilter 에서 DB 로 인증을 받기 위해 만들어진 Service 클래스
      • RestAuthenticationEntryPoint.java : 이 클래스는 사용자가 인증없이 보안된 리소스에 액세스하려고 할 때 호출됩니다. 이 경우 401 Unauthorized 응답만 반환합니다.
      • TokenAuthenticationFilter.java
        이 클래스는 request Header에서 Authorization 을 가져다가 JWT 인증 토큰을 읽어 인증(verify)하고, 토큰이 유효한 경우 Spring Security의 SecurityContext를 설정하는 데 사용됩니다.
      • TokenProvider.java : Json 웹 토큰을 생성하고 인증(verify)하는 코드가 포함되어 있습니다.
      • UserPrincipal.java : Security 에서 인증 객체로 사용할 클래스입니다
    • util
      • CookieUtils.java : 쿠키 저장, 삭제를 담당하는 클래스입니다
Pom.xml
더보기
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>spring-social</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>oauth2-demo</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.5.1</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

application.yml 설정

[방법1] application.yml 에 모두 작성하는 방법

더보기
spring:
    datasource:
        url: jdbc:h2:mem:testdb
        username: sa
        password:

    jpa:
        show-sql: true
        hibernate:
            ddl-auto: create

    security:
      oauth2:
        client:
          registration:
            google:
              clientId: [구글 클라이언트 아이디]
              clientSecret: [구글 클라이언트 패스워드]
              redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/google
              scope: # 각 플랫폼마다 scope 가 다르므로 직접 방문해서 api 문서를 참고 바람
                - email
                - profile
            facebook:
              clientId: [페이스북 클라이언트 아이디]
              clientSecret: [페이스북 클라이언트 패스워드]
              redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/facebook
              scope: # 각 플랫폼마다 scope 가 다르므로 직접 방문해서 api 문서를 참고 바람
                - email
                - public_profile
            github:
              clientId: [깃 허브 클라이언트 아이디]
              clientSecret: [깃 허브 클라이언트 패스워드]
              redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/github
              scope: # 각 플랫폼마다 scope 가 다르므로 직접 방문해서 api 문서를 참고 바람
                - user:email
                - read:user
            naver:
              clientId: [네이버 클라이언트 아이디]
              client-secret: [네이버 클라이언트 패스워드]
              redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}" # http://localhost:8080/oauth2/callback/naver
              authorization-grant-type: authorization_code
              scope:
                - name
                - email
                - profile_image
              client-name: Naver

          provider:
            facebook:
              authorizationUri: https://www.facebook.com/v3.0/dialog/oauth
              tokenUri: https://graph.facebook.com/v3.0/oauth/access_token
              userInfoUri: https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250)
            naver:
              authorization_uri: https://nid.naver.com/oauth2.0/authorize # api 문서 참고
              token_uri: https://nid.naver.com/oauth2.0/token # api 문서 참고
              user-info-uri: https://openapi.naver.com/v1/nid/me # api 문서 참고
              user_name_attribute: response # api 문서 참고
app:
  auth:
    tokenSecret: 926D96C90030DD58429D2751AC1BDBBC # JWT를 암호화 하기 위한 암호화 키 (32글자면 된다)
    tokenExpirationMsec: 864000000  # token 만료 기간 (24h * 60min * 60s * 1000ms)
  oauth2:
    # OAuth2 공급자로 성공적으로 인증 한 후 사용자에 대한 인증 토큰을 생성하고 토큰을
    # 프론트 엔드 클라이언트가 /oauth2/authorize 요청에서 지정한 redirectUri 입니다.
    # 쿠키는 모바일 클라이언트에서 잘 작동하지 않기 때문에 사용하지 않습니다
    authorizedRedirectUris:
      - http://localhost:3000/oauth2/redirect
      - myandroidapp://oauth2/redirect
      - myiosapp://oauth2/redirect

 

[방법2] application.yml 분리 방법

더보기

- application.yml

spring:
  profiles:
    include: h2, oauth2, app

 

- application-h2.yml

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    username: sa
    password:

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
  h2:
    console:
      enabled: true

 

- application-oauth2.yml

spring:
  profiles:
    include: naver, kakao, google

---
spring:
  profiles: naver
  security:
    oauth2:
      client:
        registration:
          naver:
            clientId: {클라이언트 아이디}
            client-secret: {클라이언트 비밀번호}
            redirect-uri: "{baseUrl}/oauth2/callback/{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 # api 문서 참고
            token_uri: https://nid.naver.com/oauth2.0/token # api 문서 참고
            user-info-uri: https://openapi.naver.com/v1/nid/me # api 문서 참고
            user_name_attribute: response # api 문서 참고
---
spring:
  profiles: kakao
  security:
    oauth2:
      client:
        registration:
          kakao:
            clientId: {클라이언트 아이디}
            client-secret: {클라이언트 비밀번호}
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - profile_nickname
              - profile_image
              - account_email
            client-authentication-method: POST
            client-name: kakao

        provider:
          kakao:
            authorization_uri: https://kauth.kakao.com/oauth/authorize # api 문서 참고
            token_uri: https://kauth.kakao.com/oauth/token # api 문서 참고
            user-info-uri: https://kapi.kakao.com/v2/user/me # api 문서 참고
            user_name_attribute: id # api 문서 참고
---
spring:
  profiles: google
  security:
    oauth2:
      client:
        registration:
          google:
            clientId: {클라이언트 아이디}
            clientSecret: {클라이언트 비밀번호}
            redirectUri: "{baseUrl}/oauth2/callback/{registrationId}"
            scope:
              - email
              - profile
          # google, github, facebook 은 기본적으로 provider 를 제공한다
---
spring:
  profiles: github
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: {클라이언트 아이디}
            clientSecret: {클라이언트 비밀번호}
            redirectUri: "{baseUrl}/oauth2/callback/{registrationId}"
            scope:
              - user:email
              - read:user
          # google, github 는 기본적으로 provider 를 제공한다
---
spring:
  profiles: facebook
  security:
    oauth2:
      client:
        registration:
          facebook:
            clientId: {클라이언트 아이디}
            clientSecret: {클라이언트 비밀번호}
            redirectUri: "{baseUrl}/oauth2/callback/{registrationId}"
            scope:
              - email
              - public_profile
        provider:
          facebook:
            authorizationUri: https://www.facebook.com/v3.0/dialog/oauth
            tokenUri: https://graph.facebook.com/v3.0/oauth/access_token
            userInfoUri: https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250)

 {baseUrl} : http://localhost:8080
 {action} : login
 {registrationId} : naver 또는 kakao 또는 google 또는 github 또는 facebook .... 등 등 각 플랫폼명

현재 위 naver, kakao, google include 해서 사용하고 있으며, github 와 facebook 도 사용하려면 include 에 추가하면 된다

- application-app.yml

app:
  auth:
    tokenSecret: 926D96C90030DD58429D2751AC1BDBBC # JWT를 암호화 하기 위한 암호화 키
    tokenExpirationMsec: 864000000  # token 만료 기간 (24h * 60min * 60s * 1000ms)
  oauth2:
    # OAuth2 공급자로 성공적으로 인증 한 후 사용자에 대한 인증 토큰을 생성하고 토큰을
    # 프론트 엔드 클라이언트가 /oauth2/authorize 요청에서 지정한 redirectUri 입니다.
    # 쿠키는 모바일 클라이언트에서 잘 작동하지 않기 때문에 사용하지 않습니다
    authorizedRedirectUris:
      - http://localhost:3000/oauth2/redirect
      - myandroidapp://oauth2/redirect
      - myiosapp://oauth2/redirect

authorizedRedirectUris 는 프론트엔드에서 url 파라미터로 redirect_uri=주소 했을 때 리다이렉트 되는 주소이다.
파라미터를 넘겨주지 않는 경우 successHandler 와 failureHandler 에서 기본적으로 http://localhost:8080/ 으로 설정한다

 

 

- [기타] MySQL Url

더보기
url: jdbc:mysql://localhost:3306/{DB명}?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false

 

AppProperties.java

코드

더보기
@Getter
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private final Auth auth = new Auth();
    private final OAuth2 oauth2 = new OAuth2();

    @Getter
    @Setter
    public static class Auth {
        private String tokenSecret;
        private long tokenExpirationMsec;
    }

    @Getter
    public static final class OAuth2 {
        private List<String> authorizedRedirectUris = new ArrayList<>();

        public OAuth2 authorizedRedirectUris(List<String> authorizedRedirectUris) {
            this.authorizedRedirectUris = authorizedRedirectUris;
            return this;
        }
    }
}

 

Security Config
더보기
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomUserDetailsService customUserDetailsService;

    private final CustomOAuth2UserService customOAuth2UserService;

    private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

    private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

    /*
      By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
      the authorization request. But, since our service is stateless, we can't save it in
      the session. We'll save the request in a Base64 encoded cookie instead.
    */
    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf()
                    .disable()
                .headers().frameOptions().disable()
                    .and()
                .formLogin()
                    .disable()
                .httpBasic()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                    .and()
                .authorizeRequests()
                    .antMatchers("/",
                        "/error",
                        "/favicon.ico",
                        "/h2-console/**")
                        .permitAll()
                    .antMatchers("/auth/**", "/oauth2/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated()
                    .and()
                .oauth2Login()
                    .authorizationEndpoint()
                        .baseUri("/oauth2/authorize")
                        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                        .and()
                    .redirectionEndpoint()
                        .baseUri("/oauth2/callback/*")
                        .and()
                    .userInfoEndpoint()
                        .userService(customOAuth2UserService)
                        .and()
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .failureHandler(oAuth2AuthenticationFailureHandler);

        // Add our custom Token based authentication filter
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

- cors 사용
- 세션 매니저 무상태성으로 변경 (세션 사용안함)
- csrf 사용안함
- header <frame></frame> 끄기 -> h2-console 을 위함
- form login 사용안함
- http basic login 사용안함
- 인증 또는 인가에 대한 exception 핸들링 클래스를 정의
- 접근 허용 url 설정 (auth 는 이메일로 가입, oauth 는 social 로 가입할 때 사용)
- authorizationEndpoint 는 각 플랫폼으로 리다이렉트를 하기위한 기본 url
   -> http://localhost:8080/oauth2/authorize/naver -> 네이버 로그인화면 이동
- redirectEndpoint
   -> 네이버로 부터 accessToken 을 받아왔을 때 우리 서버에서 처리 할 url
   -> http://localhost:8080/oauth2/callback/naver -> naver 는 registrationId 가 된다
- userInfoEndpoint
   -> accessToken 을 가지고 플랫폼(네이버)에서 해당 유저에 대한 정보를 가져온다 (restTemplate 이용)
   -> userService 를 통해 가져온 정보를 가공한다
- successHandler
   -> Client 에서 요청한 redirect_uri 파라미터가 서버 application.yml 에 authorizedRedirectUris 에 설정과 같은 지
       매칭하는 작업과 토큰을 생성하여 해당 리다이렉트 주소로 토큰을 파라미터로 넘겨주는 일을 한다
   -> { 해당 주소 } ?token={ JWT Token }
- failureHanlder
   -> 에러를 처리하는 일을 하며, 해당 리다이렉트 주소 리다이렉트 하며 에러메시지를 전달한다
   -> { 해당 주소 } ?error={ message }
- TokenAuthenticationFilter 를 UsernameAuthenticationFilter 앞에 놓아 인증을 시도하는 필터로 사용된다
   -> 클라이언트에서 Header 에 Authorization Bearer {JWT} 을 넘겨주어야 인증이 된다

필터 순서

 

OAuth2AuthorizationRequestRedirectFilter

/oauth2/authorize/{registrationId} = /oauth2/authorize/naver 로 요청이 들어왔을 때 처리하는 Filter 이다

 

 

WebMvcConfig
더보기
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final long MAX_AGE_SECS = 3600;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
        .allowedOrigins("*")
        .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
        .allowedHeaders("*")
        .allowCredentials(true)
        .maxAge(MAX_AGE_SECS);
    }
}

- allowedOrigins(*) : 외부에서 들어오는 모든 url 들을 허용한다 (보통은 파트너 또는 api 를 사용하는 곳만 열어둔다)
  http://www.naver.com/** , http://localhost:3000/** 처럼..
- Method 는 허용되는 메소드들을 정의 한다
- Headers 는 허용되는 헤더를 정의한다
- Credentials 는 자격증명을 허용한다
- maxAge 는 최대 1시간 허용

 

Contoller

AuthController (서버에 이메일로 가입하기 위한 Auth Api)

더보기
@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private TokenProvider tokenProvider;

    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getEmail(),
                        loginRequest.getPassword()
                )
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String token = tokenProvider.createToken(authentication);
        return ResponseEntity.ok(new AuthResponse(token));
    }

    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
        if(userRepository.existsByEmail(signUpRequest.getEmail())) {
            throw new BadRequestException("Email address already in use.");
        }

        // Creating user's account
        User user = new User();
        user.setName(signUpRequest.getName());
        user.setEmail(signUpRequest.getEmail());
        user.setPassword(signUpRequest.getPassword());
        user.setProvider(AuthProvider.local);

        user.setPassword(passwordEncoder.encode(user.getPassword()));

        User result = userRepository.save(user);

        URI location = ServletUriComponentsBuilder
                .fromCurrentContextPath().path("/user/me")
                .buildAndExpand(result.getId()).toUri();

        return ResponseEntity.created(location)
                .body(new ApiResponse(true, "User registered successfully@"));
    }

}

 

UserController (서버에 로그인이 성공한 경우 접근할 수 있는 User Api)

@RestController
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/user/me")
    @PreAuthorize("hasRole('USER')")
    public User getCurrentUser(@CurrentUser UserPrincipal userPrincipal) {
        return userRepository.findById(userPrincipal.getId())
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", userPrincipal.getId()));
    }
}

 

 

Entity & Model

AuthProvider

더보기
public enum  AuthProvider {
    local,
    facebook,
    google,
    github,
    naver,
    kakao
}

 

User

더보기
@Getter
@Entity
@Table(name = "users", uniqueConstraints = {
        @UniqueConstraint(columnNames = "email")
})
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Email
    @Column(nullable = false)
    private String email;

    private String imageUrl;

    @Column(nullable = false)
    private Boolean emailVerified = false;

    @JsonIgnore
    private String password;

    @NotNull
    @Enumerated(EnumType.STRING)
    private AuthProvider provider;

    private String providerId;

    @Builder(builderClassName= "social", builderMethodName = "socialBuilder")
    private User(String name, @Email String email, String imageUrl, @NotNull AuthProvider provider, String providerId) {
        this.name = name;
        this.email = email;
        this.imageUrl = imageUrl;
        this.provider = provider;
        this.providerId = providerId;
    }

    @Builder(builderClassName = "local",builderMethodName = "localBuilder")
    public User(String name, @Email String email, String imageUrl, String password, @NotNull AuthProvider provider, String providerId) {
        this.name = name;
        this.email = email;
        this.imageUrl = imageUrl;
        this.password = password;
        this.provider = provider;
        this.providerId = providerId;
    }

    public void updateNameAndImage(String name, String imageUrl) {
        this.name = name;
        this.imageUrl = imageUrl;
    }
}

 

Exception

BadRequestException

더보기
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
        super(message);
    }

    public BadRequestException(String message, Throwable cause) {
        super(message, cause);
    }
}

 

OAuth2AuthenticationProcessingException

더보기
public class OAuth2AuthenticationProcessingException extends AuthenticationException {
    public OAuth2AuthenticationProcessingException(String msg, Throwable t) {
        super(msg, t);
    }

    public OAuth2AuthenticationProcessingException(String msg) {
        super(msg);
    }
}

 

ResourceNotFoundException

더보기
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    private String resourceName;
    private String fieldName;
    private Object fieldValue;

    public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }

    public String getResourceName() {
        return resourceName;
    }

    public String getFieldName() {
        return fieldName;
    }

    public Object getFieldValue() {
        return fieldValue;
    }
}

 

Result(Response) & Payload(Request)

ApiResponse

더보기
@Getter
@Setter
@AllArgsConstructor
public class ApiResponse {
    private boolean success;
    private String message;
}

 

AuthResponse

더보기
@Getter
@Setter
public class AuthResponse {
    private String accessToken;
    private String tokenType = "Bearer";

    public AuthResponse(String accessToken) {
        this.accessToken = accessToken;
    }
}

 

LoginRequest

더보기
@Getter
public class LoginRequest {
    @NotBlank
    @Email
    private String email;

    @NotBlank
    private String password;
}

 

SignUpRequest

더보기
@Getter
public class SignUpRequest {
    @NotBlank
    private String name;

    @NotBlank
    @Email
    private String email;

    @NotBlank
    private String password;
}

 

Repository

UserRepository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);

    Boolean existsByEmail(String email);
}

 

Security

TokenAuthenticationFilter

더보기
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);

            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                Long userId = tokenProvider.getUserIdFromToken(jwt);

                UserDetails userDetails = customUserDetailsService.loadUserById(userId);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

 

TokenProvider

더보기
@Service
@Slf4j
@RequiredArgsConstructor
public class TokenProvider {

    private final AppProperties appProperties;

    public String createToken(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + appProperties.getAuth().getTokenExpirationMsec());

        return Jwts.builder()
                .setSubject(Long.toString(userPrincipal.getId()))
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, appProperties.getAuth().getTokenSecret())
                .compact();
    }

    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(appProperties.getAuth().getTokenSecret())
                .parseClaimsJws(token)
                .getBody();

        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            log.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            log.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            log.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            log.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            log.error("JWT claims string is empty.");
        }
        return false;
    }
}

 

CustomUserDetailsService

더보기
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String email)
            throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() ->
                        new UsernameNotFoundException("User not found with email : " + email)
        );

        return UserPrincipal.create(user);
    }

    @Transactional
    public UserDetails loadUserById(Long id) {
        User user = userRepository.findById(id).orElseThrow(
            () -> new ResourceNotFoundException("User", "id", id)
        );

        return UserPrincipal.create(user);
    }
}

 

UserPrincipal

더보기
@Getter
public class UserPrincipal implements OAuth2User, UserDetails {
    private Long id;
    private String email;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    @Setter
    private Map<String, Object> attributes;

    public UserPrincipal(Long id, String email, String password, Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }

    public static UserPrincipal create(User user) {
        List<GrantedAuthority> authorities = Collections.
                singletonList(new SimpleGrantedAuthority("ROLE_USER"));

        return new UserPrincipal(
                user.getId(),
                user.getEmail(),
                user.getPassword(),
                authorities
        );
    }

    public static UserPrincipal create(User user, Map<String, Object> attributes) {
        UserPrincipal userPrincipal = UserPrincipal.create(user);
        userPrincipal.setAttributes(attributes);
        return userPrincipal;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public String getName() {
        return String.valueOf(id);
    }
}

 

CurrentUser

더보기
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {
}

 

RestAuthenticationEntryPoint

더보기
@Slf4j
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {


    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException e) throws IOException, ServletException {
        log.error("승인되지 않은 오류로 응답합니다. 메세지: {}", e.getMessage());
        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
                e.getLocalizedMessage());
    }
}

HttpCookieOAuth2AuthorizationRequestRepository

더보기
@Component
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
    public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
    public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
    private static final int cookieExpireSeconds = 180;

    /** cookie 에 저장되어있던 authorizationRequest 들을 가져온다 authorizationUri, authorizationGrantType, responseType, clientId, redirectUri, scopes, additionalParameters */
    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
                .map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class))
                .orElse(null);
    }

    /** 플랫폼으로 보내기 위한 Request 를 `oauth2_auth_request` 라는  cookie 에 저장 한다 authorizationUri, authorizationGrantType, responseType, clientId, redirectUri, scopes, additionalParameters */
    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        if (authorizationRequest == null) {
            removeAuthorizationRequest(request, response);
            return;
        }

        CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds);

        /*
         * http://localhost:8080/oauth2/authorize/naver?redirect_uri=http://localhost:3000/oauth/redirect 로 요청 받았을 때
         * http://localhost:3000/oauth/redirect 를 가져온다
         * 그리고 존재하는 경우 cookie 에 넣어준다
         */
        String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
        if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
            CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds);
        }
    }

    /** remove 를 재정의 해서 cookie 를 가져오고 remove 는 successHandler 또는 failureHandler 에서 할 수 있도록 한다 */
    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
        return this.loadAuthorizationRequest(request);
    }

    /**
     * 사용자 정보를 다 가지고 온 뒤 이제 리다이렉트를 하면 기존에 남아있던 쿠키들을 제거해주기 위해 사용된다
     *  OAuth2AuthorizationRequest 와 클라이언트에서 파리미터로 요청한 redirect_uri 가 된다
     * */
    public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
        CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
        CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
    }
}

 

CustomOAuth2UserService

더보기
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);

        try {
            return processOAuth2User(oAuth2UserRequest, oAuth2User);
        } catch (AuthenticationException ex) {
            throw ex;
        } catch (Exception ex) {
            // Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
        }
    }

    private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {

        final String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();

        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());

        if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
            throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
        }

        Optional<User> userOptional = userRepository.findByEmail(oAuth2UserInfo.getEmail());
        User user;

        // 이미 존재하는 경우
        if(userOptional.isPresent()) {
            user = userOptional.get();

            // 가져온 유저의 공급자명과 넘어온 공급자명이 다른 경우
            if(!user.getProvider().equals(AuthProvider.valueOf(registrationId))) {

                // 이미 다른 공급자가 존재하기 때문에 가입할 수 없다
                throw new OAuth2AuthenticationProcessingException("Looks like you're signed up with " +
                        user.getProvider() + " account. Please use your " + user.getProvider() +
                        " account to login.");
            }
            user = updateExistingUser(user, oAuth2UserInfo);
        } else {
            user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
        }

        return UserPrincipal.create(user, oAuth2User.getAttributes());
    }

    private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {

        User user = User.socialBuilder()
                .provider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))
                .providerId(oAuth2UserInfo.getId())
                .name(oAuth2UserInfo.getName())
                .email(oAuth2UserInfo.getEmail())
                .imageUrl(oAuth2UserInfo.getImageUrl())
                .build();

        return userRepository.save(user);
    }

    private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) {
        existingUser.updateNameAndImage(oAuth2UserInfo.getName(), oAuth2UserInfo.getImageUrl());
        return userRepository.save(existingUser);
    }

}
마침 네이버 계정 이메일과 카카오 계정 이메일이 같아서 다른 플랫폼으로 가입을 시도 해봤더니, exception 발생하여 FailureHanlder 에서 처리하는 것을 볼 수 있다. (네이버로 가입하고, 프론트엔드에서 token 을 저장해둔 곳(쿠키 또는 로컬저장소 또는 vuex or redux) 에서 지워주고 테스트하면 된다
error 파라미터를 가져와서 error 로 띄워준 모습 [Vue.js]

 

OAuth2AuthenticationSuccessHandler

더보기
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final TokenProvider tokenProvider;

    private final AppProperties appProperties;

    private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String targetUrl = determineTargetUrl(request, response, authentication);

        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
            return;
        }

        clearAuthenticationAttributes(request, response);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }

    protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
                .map(Cookie::getValue);

        if(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
            throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
        }

        String targetUrl = redirectUri.orElse(getDefaultTargetUrl());

        String token = tokenProvider.createToken(authentication);

        return UriComponentsBuilder.fromUriString(targetUrl)
                .queryParam("token", token)
                .build().toUriString();
    }

    protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
        super.clearAuthenticationAttributes(request);
        httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
    }

    private boolean isAuthorizedRedirectUri(String uri) {
        URI clientRedirectUri = URI.create(uri);

        return appProperties.getOauth2().getAuthorizedRedirectUris()
                .stream()
                .anyMatch(authorizedRedirectUri -> {
                    // Only validate host and port. Let the clients use different paths if they want to
                    URI authorizedURI = URI.create(authorizedRedirectUri);
                    if(authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
                            && authorizedURI.getPort() == clientRedirectUri.getPort()) {
                        return true;
                    }
                    return false;
                });
    }
}

 

OAuth2AuthenticationFailureHandler

더보기
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
                .map(Cookie::getValue)
                .orElse(("/"));

        targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
                .queryParam("error", exception.getLocalizedMessage())
                .build().toUriString();

        httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);

        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}

OAuth2UserInfoFactory

더보기
public class OAuth2UserInfoFactory {

    public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
        System.err.println(attributes);
        switch (AuthProvider.valueOf(registrationId.toLowerCase())) {
            case naver:
                return new NaverOAuth2UserInfo(attributes);
            case kakao:
                return new KakaoOAuth2UserInfo(attributes);
            case google:
                return new GoogleOAuth2UserInfo(attributes);
            case facebook:
                return new FacebookOAuth2UserInfo(attributes);
            case github:
                return new GithubOAuth2UserInfo(attributes);
            default:
                throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet.");
        }
    }
}

 

OAuth2UserInfo

더보기
public abstract class OAuth2UserInfo {
    protected Map<String, Object> attributes;

    public OAuth2UserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    public Map<String, Object> getAttributes() {
        return attributes;
    }

    public abstract String getId();

    public abstract String getName();

    public abstract String getEmail();

    public abstract String getImageUrl();
}

 

NaverOAuth2UserInfo

더보기
/**
 * {
 *   resultcode=00,
 *   message=success,
 *   response={
 *     id=아이디,
 *     profile_image=이미지주소.png,
 *     email=이메일, name=이름
 *   }
 * }
 */
public class NaverOAuth2UserInfo extends OAuth2UserInfo {

    /** naver 는 response 안에 담겨져있기 때문에 response 를 해준다 */
    public NaverOAuth2UserInfo(Map<String, Object> attributes) {
        super((Map<String, Object>) attributes.get("response"));
    }

    @Override
    public String getId() {
        return (String) attributes.get("id");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getImageUrl() {
        return (String) attributes.get("profile_image");
    }
}

 

KakaoOAuth2UserInfo

더보기
/**
 * {
 *  id=11111111,
 *  connected_at=2021-08-14T14:02:42Z,
 *  properties={
 *     nickname=닉네임
 *  },
 *  kakao_account={
 *     profile_nickname_needs_agreement=false,
 *     profile_image_needs_agreement=false,
 *     profile={
 *         nickname=닉네임,
 *         thumbnail_image_url=xxx.jpg,
 *         profile_image_url=xxx.jpg,
 *         is_default_image=true
 *     },
 *     has_email=true,
 *     email_needs_agreement=false,
 *     is_email_valid=true,
 *     is_email_verified=true,
 *     email=xxx@naver.com
 *  }
 * }
 * */
public class KakaoOAuth2UserInfo extends OAuth2UserInfo{

    /** 카카오는 Integer 로 받아져서 그런지 (String) 또는 (Long) 으로 cascading 이 되지 않는다... 그래서 Integer 로 받아준다 */
    private Integer id;

    public KakaoOAuth2UserInfo(Map<String, Object> attributes) {
        super((Map<String, Object>) attributes.get("kakao_account"));
        this.id = (Integer) attributes.get("id");
    }

    @Override
    public String getId() {
        return this.id.toString();
    }

    @Override
    public String getName() {
        return (String) ((Map<String, Object>) attributes.get("profile")).get("nickname");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getImageUrl() {
        return (String) ((Map<String, Object>) attributes.get("profile")).get("thumbnail_image_url");
    }
}

 

GoogleOAuth2UserInfo

더보기
public class GoogleOAuth2UserInfo extends OAuth2UserInfo {

    public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
        super(attributes);
    }

    @Override
    public String getId() {
        return (String) attributes.get("sub");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getImageUrl() {
        return (String) attributes.get("picture");
    }
}

github , facebook, instargram 은 각자찾길...

 

util

CookieUtils

더보기
public class CookieUtils {

    public static Optional<Cookie> getCookie(HttpServletRequest request, String name) {
        Cookie[] cookies = request.getCookies();

        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(name)) {
                    return Optional.of(cookie);
                }
            }
        }

        return Optional.empty();
    }

    public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }

    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie: cookies) {
                if (cookie.getName().equals(name)) {
                    cookie.setValue("");
                    cookie.setPath("/");
                    cookie.setMaxAge(0);
                    response.addCookie(cookie);
                }
            }
        }
    }

    public static String serialize(Object object) {
        return Base64.getUrlEncoder()
                .encodeToString(SerializationUtils.serialize(object));
    }

    public static <T> T deserialize(Cookie cookie, Class<T> cls) {
        return cls.cast(SerializationUtils.deserialize(
                        Base64.getUrlDecoder().decode(cookie.getValue())));
    }
}

OAuth2AuthorizationRequest 를 base64 로 인코딩해서 각 플랫폼 로그인 url 에 보낼 때 사용한다

OAuth2AuthorizationRequest 

 

 

프론트 엔드 부분
<p>
  <a href="http://localhost:8080/oauth2/authorize/naver?redirect_uri=http://localhost:3000/oauth2/redirect">네이버</a>
</p>
<p>
  <a href="http://localhost:8080/oauth2/authorize/kakao?redirect_uri=http://localhost:3000/oauth2/redirect">카카오</a>
</p>
<p>
  <a href="http://localhost:8080/oauth2/authorize/google?redirect_uri=http://localhost:3000/oauth2/redirect">구글</a>
</p>

- redirect_uri 를 통해 프론트엔드 쪽으로 token 또는 error 파라미터가 넘어온다

넘어온 token 또는 error 를 받아 LocalStorage 또는 Cookie 또는 vue 의 경우 vuex, react 경우 redux 에 저장 한 뒤

권한이 필요한 요청에 대해서 Header Autorization 에 Bearer { jwt token } 을 같이 포함하여 요청하면 된다

 

네이버

https://developers.naver.com/main/

 

NAVER Developers

네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음

developers.naver.com

카카오

https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

구글

https://console.cloud.google.com/home/dashboard?project=spring-auto-test&hl=ko 

 

Google Cloud Platform

하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.

accounts.google.com