다시 시작된 월요일!!

image

드디어 프로젝트를 해야할 시간이 다가오기 시작한다.

현재까지 배운 기술을 이용해 무언가 만들어본다는게 너무 설레고 기대된다.
여태까지 배운 기술들에 대한 내용 중 크게 이해가 안되는 부분은 없었고
Advance한 공부가 필요한 내용들이 많았다.

프로젝트를 기회로 깊이 파헤쳐보는 기회가 올 것 같아 너무나 기대된다.
오늘은 OAuth2에 대한 마지막 시간이다.

그 동안 배운 Spring Security와 JWT, OAuth2를
전부 이용해서 인증과 권한부여 코드를 작성해보는 것이다.


OAuth2 인증 하기

저번주에 배웠던 이론을 토대로
OAuth2 인증을 실제로 적용해 보자

우선 Frontend와 Backend가 분리되어있는
CSR 방식의 애플리케이션에서 Google OAuth2 인증을 기준으로
적용해볼 생각이다. Frontend 쪽 애플리케이션은
Apache 웹 서버를 이용해 테스트 할 것이다.

그리고 저번주에 사전에 준비했던 구글 API 콘솔 애플리케이션을
미리 만들어두어야 테스트가 가능하다.

1). 구글 OAuth2 로그인 인증하기

image

이제 가장 중요한 처리 흐름에 대해 이해해야한다.
현재 빨간색으로 되어있는 부분은 우리가 구현해줘야하는 부분이고
파란색 부분은 구현하지 않아도 Spring Security에서 처리해준다.

처음으로 (1)번 로그인 링크를 Frontend 서버에서 클릭을하면

image
(테스트 용도로 Apache 웹서버에 해당 html을 만들었다.)

(2)번과 같은 형식의 인증 URI로 request 전송을 해줘야한다.
OAuth2LoginAuthenticationFilter 클래스가 이부분을 처리해준다.
해당 클래스도 UsernamePasswordAuthenticationFilter와 같이
AbstractAuthenticationProcessingFilter 추상 클래스를 상속하고 있다.
즉 필터가 시작되면 추상클래스에 정의된 doFilter();메서드를 실행하는 것이다.

그리고 (3)번에서 Google을 로그인 화면을 요청하는 URI로 리다이렉트한다.
또한 Authorization Server가 Backend 애플리케이션 쪽으로
Authorization Code를 전송할 Redirect URI(http://localhost:8080/login/oauth2/code/google)를
쿼리 파리미터로 전달해준다. Redirect URI는 Spring Security가 내부적으로 제공해준다.

image

즉, https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?redirect_uri=http://localhost:8080/login/oauth2/code/google
와 같은 형식으로 리다이렉션해 (4)번인 로그인 화면을 보여준다. 이외에 쿼리들도 있지만 따로 적지는 않겠다.

그러면 이제 구글 로그인 화면이 Resource Owner한테 보일테니
(5)번 로그인을 해주면 된다. 그렇게 되면 Backend 애플리케이션 서버에서
위에서 받은 Redirect URI로 Authorization Code를 요청하고
그럼과 같은 순서대로 Access Token을 받고, Resource Server에
요청하여 데이터를 접근할 수 있게되는 것이다.

그리고 마지막으로 전송받으면 로그인 완료 Redirect를 진행할
URI를 만들어 Frontend 애플리케이션으로 던져주면
OAuth2 로그인 인증 구현에는 끝난다.
(JWT 토큰도 만들어 함께 보낸다)


코드로 한번 살펴보자

우선 JWT를 만들어주는 클래스를 먼저 작성해보자
이전에 JWT 코드와 동일하다

@Component
public class JwtTokenizer {

    @Getter
    @Value("${jwt.key.secret}")
    private String securityKey;

    @Getter
    @Value("${jwt.access-token-expiration-minutes}")
    private int accessTokenExpirationMinutes;

    @Getter
    @Value("${jwt.refresh-token-expiration-minutes}")
    private int refreshTokenExpirationMinutes;

    public String encodeBase64SecretKey(String securityKey){
        return Encoders.BASE64.encode(securityKey.getBytes(StandardCharsets.UTF_8));
    }

    public String generateAccessToken(Map<String , Object> claims, String subject, Date expiration, String base64EncodedSecretKey){

        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        return Jwts.builder()
                .setSubject(subject)
                .setClaims(claims)
                .setIssuedAt(Calendar.getInstance().getTime())
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey){

        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(Calendar.getInstance().getTime())
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    public Jws<Claims> getClaims(String jws, String base64EncodedSecretKey){

        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        Jws<Claims> claimsJws = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jws);

        return claimsJws;
    }

    public Date getTokenExpiration(int expirationMinutes){
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.MINUTE, expirationMinutes);
        Date expiration = instance.getTime();

        return expiration;
    }

    public Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey){
        byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

달라진점은 없고, 후에 JWT를 만들때 DI받아 사용해주면 된다.


두번째로는 SecurityFilterChain을 구성해보자

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfigurationV2 {

    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;
    private final MemberService memberService;

    public SecurityConfigurationV2(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils, MemberService memberService) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
        this.memberService = memberService;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .headers().frameOptions().sameOrigin()
                .and()
                .csrf().disable()
                .cors(withDefaults())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .exceptionHandling()
                .authenticationEntryPoint(new MemberAuthenticationEntryPoint())
                .accessDeniedHandler(new MemberAccessDeniedHandler())
                .and()
                .apply(new CustomFilterConfigurer())
                .and()
                .authorizeHttpRequests(authorize -> authorize // url authorization 전체 추가
                                .antMatchers(HttpMethod.POST, "/*/coffees").hasRole("ADMIN")
                                .antMatchers(HttpMethod.PATCH, "/*/coffees/**").hasRole("ADMIN")
                                .antMatchers(HttpMethod.GET, "/*/coffees/**").hasAnyRole("USER", "ADMIN")
                                .antMatchers(HttpMethod.GET, "/*/coffees").permitAll()
                                .antMatchers(HttpMethod.DELETE, "/*/coffees").hasRole("ADMIN")
                                .antMatchers(HttpMethod.POST, "/*/orders").hasRole("USER")
                                .antMatchers(HttpMethod.PATCH, "/*/orders").hasAnyRole("USER", "ADMIN")
                                .antMatchers(HttpMethod.GET, "/*/orders/**").hasAnyRole("USER", "ADMIN")
                                .antMatchers(HttpMethod.DELETE, "/*/orders").hasRole("USER")
                                .anyRequest().permitAll()
                )
                .oauth2Login(oauth2 -> oauth2
                        .successHandler(new OAuth2MemberSuccessHandler(jwtTokenizer, authorityUtils, memberService))
                );

        return http.build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
        @Override
        public void configure(HttpSecurity builder) throws Exception {
            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);

            builder.addFilterAfter(jwtVerificationFilter, OAuth2LoginAuthenticationFilter.class);
        }
    }
}

필터체인 구성도 크게 바뀐건 없다.
달라진점은 JWT 인증을 사용했을 때는, jwtAuthenticationFilter라는 필터 클래스를
우리가 직접 구현해줘서, 필터체인에 등록해주었었다. 그 부분이 사라지고
우리는 OAuth2 인증을 이용하기 위해 .oauth2Login() 가 추가되었고, 추가됨에 따라
OAuth2AuthorizationRequestRedirectFilter
OAuth2LoginAuthenticationFilter 가 추가된다.
해당 필터들이 추가됨에따라 OAuth2 인증을 진행해주고, 인증에 성공시 AbstractAuthenticationProcessingFilter 클래스에서
successfulAuthentication();메서드를 호출하게 되어진다.

호출되면 인증된 객체가 SecurityContext에 저장되고
AuthenticationSuccessHandler 인터페이스에 onAuthenticationSuccess();를 호출하게된다.
말은 즉슨 해당 인터페이스를 상속해 구현하는 클래스를 우리가 만들어서 Frontend쪽 서버로
JWT 토큰을 만들어서 보낸다거나, 리다이렉트할 URI를 생성한다든가 조작이 가능하다는 점이다.

그렇게 조작하는 클래스를 우리가 만들어보자

public class OAuth2MemberSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;
    private final MemberService memberService;

    public OAuth2MemberSuccessHandler(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils, MemberService memberService) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
        this.memberService = memberService;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 인증된 객체로 부터 Resource Owner의 이메일 주소를 얻을 수 있다.
        System.out.println("# Redirect to Frontend");
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        String name = oAuth2User.getName();
        String email = "";

        /*
         * 플랫폼에 따른 email 가져오기
         * */
        if(name.equals("삭제")){ // 해당 부분은 getName();의 번호를 가져오는 부분으로 비교함. 혹시 몰라서 번호는 삭제
            // 구글
            System.out.println("# Google 로그인 실행");
            email = (String) oAuth2User.getAttributes().get("email");
        } else if(name.equals("삭제")){
            //카카오
            System.out.println("# Kakao 로그인 실행");
            Map<String, Object> attributes = oAuth2User.getAttributes();
            Map<String, Object> kakao_account = (Map<String, Object>) attributes.get("kakao_account");
            email = (String) kakao_account.get("email");
        } else if(name.equals("삭제")){
            System.out.println("# GitHub 로그인 실행");
            Map<String, Object> attributes = oAuth2User.getAttributes();
            email = (String) attributes.get("login");
        } else {
            System.out.println("# Naver 로그인 실행");
            Map<String, Object> attributes = oAuth2User.getAttributes();
            Map<String, Object> naver_account = (Map<String, Object>) attributes.get("response");
            email = (String) naver_account.get("email");
        }

        // 강제 admin 권한 부여 (테스트 용도)
        String adminCheck = "admin@gmail.com";
        // 얻은 email 주소로 권한 List 만들기
        List<String> authorities = authorityUtils.createRoles(adminCheck);

        // email을 토대로 Member 객체 만들어서 DB에 저장
        // 현재는 저장안하도록 테스트만진행
//        saveMember(email);

        // 리다이렉트를 하기위한 정보들을 보내줌
        redirect(request,response,email,authorities);
    }


    private void redirect(HttpServletRequest request, HttpServletResponse response, String username, List<String> authorities) throws IOException {
        // 받은 정보를 토대로 AccessToken, Refresh Token을 만듬
        String accessToken = delegateAccessToken(username, authorities);
        String refreshToken = delegateRefreshToken(username);

        // Token을 토대로 URI를 만들어서 String으로 변환
        String uri = createURI(accessToken, refreshToken).toString();

        // 만든 URI로 리다이렉트 보냄
        getRedirectStrategy().sendRedirect(request,response,uri);
    }

    private String delegateAccessToken(String username, List<String> authorities){

        Map<String, Object> claims = new HashMap<>();
        claims.put("username",username);
        claims.put("roles",authorities);

        String subject = username;
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecurityKey());

        String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);
        return accessToken;
    }

    private String delegateRefreshToken(String username){

        String subject = username;
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecurityKey());
        String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);

        return refreshToken;
    }

    private URI createURI(String accessToken, String refreshToken){
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("access_token", accessToken);
        queryParams.add("refresh_token", refreshToken);

        return UriComponentsBuilder
                .newInstance()
                .scheme("http")
                .host("localhost")
//                .port(80) // 기본 포트가 80이기 때문에 괜찮다
                .path("/receive-token.html")
                .queryParams(queryParams)
                .build()
                .toUri();
    }
}

SimpleUrlAuthenticationSuccessHandler클래스를 상속받고 있다.
해당 클래스는 AuthenticationSuccessHandler 인터이스를 상속받고있다.
위에서 얘기했듯이 인증에 성공시 해당 인터페이스의 onAuthenticationSuccess();
호출되어지고, 이 메서드를 Overriding하여 구현하면 된다.

구현 내용은 인증된 객체에서 우선 정보를 꺼내도록했다.
email 정보를 꺼내서 추후에 DB에 관리한다면 저장할 예정이다.
또한 여러가지 Google,Kakao,Github,Kakao 등을 구별하기위해
getName();의 값을 사용했지만… 이부분은 따로 header에서 구분할 수 있는
무언가를 가져와야할 것 같은 느낌이든다. (아직 해결못한 숙제다)

그래도 우선 구현테스트를 위해 name값으로 케이스를 나눠
getAttributes(); 안에있는 email 정보를 꺼내서 저장하는 것을 구현했다.

그리고 가장 중요한 부분인 Redirect 부분인데
현재 구글 인증이 정상적으로 이루어져 흐름이 이쪽으로 넘어온 것이고
이제 우리는 로그인을 완료했으니 다른페이지를 Redirect 해야하며
JWT을 만들어서 header에 싦어주는 코드를 구현해야한다.

그 구현 부분이 redirect();으로 작성했고
해당 내용은 accessToken, refreshToken을 만들고
UriComponentsBuilder를 통해서 우리가 Redirect할 주소와
queryParams로 추가해서 URI만들었다.

이제 Redirect만 진행하면된다. getRedirectStrategy().sendRedirect();를 구현해
Redirect를 진행하도록했고 해당 메서드는 AbstractAuthenticationTargetUrlRequestHandler
인터페이스의 메서드로 마찬가지로 SimpleUrlAuthenticationSuccessHandler와 상속관계라
Overriding하여 우리가 구현이 가능한 부분이다. 매개 변수로는

HttpServletRequest 객체, HttpServletResponse 객체, String uri
이렇게 매개변수로 넘겨주면 Redirect하게 되어지는 것이다.

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            clientId: ${G_CLIENT_ID} # 구글 API콘솔로 발급받은 ID
            clientSecret: ${G_CLIENT_SECRET} # 구글 API콘솔로 발급받은 SECRET
            scope:
              - email
              - profile

이제 마지막으로 .yml 파일에 필요한 설정정보를 입력해주면
Springn Security에서 자동으로 진행해준다.

정말 대단하다… 내가 직접구현한 코드는 완전 극히 일부일텐데
이모든걸 자동화시켜놓고 우리같은 초짜 개발자도 이용할 수 있다는게…
후에 실제 Filter 동작에 디버그를 걸어놓고 어떻게 OAuth2 인증 과정을
거치가는지는 면밀한 파악이 필요할 것 같다.


이렇게 구현을 완료한 다음에
실제로 로그인을 진행해보았을 때, 구글 로그인 완료 후에

image

개발자도구로 원하는 페이지에 리다이렉션이 되었고
우리가 만들어준 accessToken과 refreshToken이 정상적으로
로컬 스토리지에 담겨있는 모습을 확인해 볼 수 있다.

로컬 스토리지에 담긴 이유는 Frontend 쪽 서버에
javaScript언어로 저장하도록 코드를 추가해놓아서 저장되어지는 것이다.
물론 response.setHeader();메서드를 통해서 header로 전송할 수도 있다.


2). 카카오톡 OAuth2 로그인 인증하기

구글 로그인 인증을 완료해 보았으니
다른 로그인도 한번 시도해보고 싶었다.

위에 코드는 전부 그대로 두고 .yml 설정 파일의 설정 정보만 변경하면 똑같이 인증이 가능하다.

구글 콘솔 API처럼 카카오톡도 카카오톡 로그인 애플리케이션을 만들어 줘야한다.
해당 사이트에 들어가 애플리케이션을 만들고

image

client-id와 client-secret를 발급 받으면 된다.
그 외에 scope 정보 동의 설정들도 가능했다.

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            clientId: ${G_CLIENT_ID} # 구글 API콘솔로 발급받은 ID
            clientSecret: ${G_CLIENT_SECRET} # 구글 API콘솔로 발급받은 SECRET
            scope:
              - email
              - profile
          kakao:
            client-id: ${K_CLIENT_ID} # 앱 설정 -> 앱 키 -> REST API 키
            client-secret: ${K_CLIENT_SECRET}  # 제품 설정 -> 카카오 로그인 -> 보안 -> Client Secret
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            authorization-grant-type: authorization_code
            client-authentication-method: POST
            client-name: Kakao
            scope:
              - profile_nickname
              - profile_image
              - account_email
        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

기존 구글설정에 밑에 카카오관련 설정을 넣어주고

Frontend 서버에서 http://localhost:8080/oauth2/authorization/kakao 로 요청을 보내주면

image

우리가 설정한대로 카카오톡 로그인 인증이 동일하게 진행할 수 있다.


3). 네이버 OAuth2 로그인 인증하기

네이버도 마찬가지다 네이버 로그인 애플리케이션에 들어가
애플리케이션을 만들어준다.

image

동일하게 id와 secretKey를 받았을 것이다.

spring:
  security:
      oauth2:
        client:
          registration:
            google:
              clientId: ${G_CLIENT_ID} # 구글 API콘솔로 발급받은 ID
              clientSecret: ${G_CLIENT_SECRET} # 구글 API콘솔로 발급받은 SECRET
              scope:
                - email
                - profile
            kakao:
              client-id: ${K_CLIENT_ID} # 앱 설정 -> 앱 키 -> REST API 키
              client-secret: ${K_CLIENT_SECRET}  # 제품 설정 -> 카카오 로그인 -> 보안 -> Client Secret
              redirect-uri: http://localhost:8080/login/oauth2/code/kakao
              authorization-grant-type: authorization_code
              client-authentication-method: POST
              client-name: Kakao
              scope:
                - profile_nickname
                - profile_image
                - account_email
            naver:
              client-id: ${N_CLIENT_ID}
              client-secret: ${N_CLIENT_SECRET}
              redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" # (== http://localhost:8080/login/oauth2/code/naver)
              authorization-grant-type: authorization_code
              scope: name, email, profile_image
              client-name: Naver
          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
            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

네이버도 동일하게 설정을 완료해주고
Frontend 서버에서 http://localhost:8080/oauth2/authorization/naver 로 요청을 보내주면

image

위와 같이 네이버 로그인 인증을 사용할 수 있게된다.


4). Github OAuth2 로그인 인증하기

마지막으로 Github OAuth2도 적용해보고 싶어 시도해봤다.

GitHub 홈페이지에서
Setting -> Developer settings -> OAuth Apps
경로로 접속하면 아래와 같이 애플리케이션을 만들수 있는 화면이 나온다.

image

나는 practice라는 이름으로 애플리케이션을 만들었다.

spring:
  security:
      oauth2:
        client:
          registration:
            google:
              clientId: ${G_CLIENT_ID} # 구글 API콘솔로 발급받은 ID
              clientSecret: ${G_CLIENT_SECRET} # 구글 API콘솔로 발급받은 SECRET
              scope:
                - email
                - profile
            kakao:
              client-id: ${K_CLIENT_ID} # 앱 설정 -> 앱 키 -> REST API 키
              client-secret: ${K_CLIENT_SECRET}  # 제품 설정 -> 카카오 로그인 -> 보안 -> Client Secret
              redirect-uri: http://localhost:8080/login/oauth2/code/kakao
              authorization-grant-type: authorization_code
              client-authentication-method: POST
              client-name: Kakao
              scope:
                - profile_nickname
                - profile_image
                - account_email
            naver:
              client-id: ${N_CLIENT_ID}
              client-secret: ${N_CLIENT_SECRET}
              redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" # (== http://localhost:8080/login/oauth2/code/naver)
              authorization-grant-type: authorization_code
              scope: name, email, profile_image
              client-name: Naver
            github:
              clientId: ${H_CLIENT_ID}
              clientSecret: ${H_CLIENT_SECRET}
              redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" # (== http://localhost:8080/login/oauth2/code/google)
              authorization-grant-type: authorization_code
              client-name: GitHub
              scope:
                - email
                - profile
          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
            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

위에서 했던 구글,카카오,네이버와 동일하게
.yml 파일에 설정 정보만 추가해주면 인증 사용이 가능하고

Frontend 서버에서 http://localhost:8080/oauth2/authorization/github 로 요청을 보내주면

image

동일하게 로그인 요청을 보내면
정상적으로 인증이되고 AccessToken과 Refresh Token이 발급되는
모습을 확인할 수 있다.



이렇게 오늘은 OAuth2 인증에 대한
여러가지 벤더들의 로그인 기능을 사용해 보았다.
실질적으로 어떤 회사냐에 따라 변경하는 부분은 크게 어렵지 않은 것 같다.

하지만 Spring이 상당히 많은 부분을 작업해주고
내가 구현하는 부분인 정망 극히 일부이기 때문에
OAuth2에 대한 완벽한 이해가 되었다고는 할 수 없을 것 같다.

계속 공부하면서 느끼는 것은 프레임워크에 대한 세부 내용
이해하는 것이 정말 중요하다고 느끼고
추후에 이부분에 대한 흐름들은 따로 공부를하려고한다.

오늘 공부는 여기서 끝 !


오늘의 커피량: ☕️ ☕️
오늘의 점심: 라면, 밥