[Spring Security] Oauth2 + Jwt 소셜로그인을 진행해보자!

수깡·2024년 3월 11일

Spring Security

목록 보기
9/11

이번에는 스프링 시큐리티, OAuth2(구글, 카카오), Jwt (Access Token, Refresh Token) 까지 모두 이용하여 소셜로그인을 구현해보자!!

💛 이해하기

🌟 OAuth 란?

제3의 서비스에 계정 관리를 맡기는 방식

  • 리소스 오너: 자신의 정보를 사용하도록 인증 서버에 허가하는 주체. 사용자가 이에 해당
  • 리소스 서버: 리소스 오너의 정보를 가지고, 보호하는 주체. 네이버, 구글, 카카오 등
  • 인증 서버: 클라이언트에게 리소스 오너의 정보에 접근할 수 있는 토큰을 발급하는 역할을 하는 애플리케이션
  • 클라이언트 어플리케이션: 인증 서버에게 인증을 받고 리소스 오너의 리소스를 사용하는 주체. (지금 개발하는 서비스)

인증 서버에서 발급받은 토큰을 이용하여 리소스 서버에 리소스 오너의 정보를 요청하고, 인증서버에게 인증을 받고, 응답받아 사용할 수 있다.

🌟 리소스 오너 정보를 취득하는 4가지 방법

  • 권한 부여 코드 승인 타입(authorization code grant type): 가장 잘 알려진 인증밥법으로, 클라이언트가 리소스에 접근하는데 사용한다. 권한에 접근할 수 있는 코드와 리소스 오너에 대한 액세스 토큰을 발급받는다.
  • 임시적 승인 타입(implicit grant type): 서버가 없는 js 웹 애플리케이션에서 사용한다. 클라이언트가 요청을 보내면 리소스 오너의 인증 과정 이외에는 권한 코드 교환 등 별다른 인증 과정을 거치지 않고 액세스 토큰을 발급받는다.
  • 리소스 소유자 암호 자격증명 승인 타입(resource owner password credentials): 클라이언트의 패스워드를 이용하여 액세스 토큰에 대한 사용자 자격 증명을 교환한다.
  • 클라이언트 자격증명 승인 타입(client credentials grant): 클라이언트가 컨텍스트 외부에서 액세스 토큰을 얻어 특정 리소스에 접근을 요청할 때 사용.

🌟 권한 부여 코드 승인 타입 방식

  1. 권한 요청

클라이언트(스프링부트 서버)가 사용자 데이터에 접근하기 위해 권한 서버(카카오,구글)에 요청을 보내는 것.

클라이언트 ID, 리다이렉트 URI, 응답 타입 등을 파라미터로 보내 요청한다.

실제 요청 예시)

GET spring-auhorization-server.example/authorize?
client_id=48a4893g
redirect_uri=http://localhost:8080/myapp&
response_type=code&
scope=profile
  • client_id: 인증 서버가 클라이언트에 할당한 고유 식별자. 클라이언트 애플리케이션을 OAuth 서비스에 등록할 때 서비스에서 생성하는 값이다.
  • redirect_uri: 로그인 시 이동해야 하는 URI
  • response_type: 클라이언트가 제공받길 원하는 응답 타입. 인증 코드를 받을 때는 code값을 포함해야한다.
  • scope: 제공받고자 하는 리소스 오너의 정보 목록
  1. 데이터 접근용 권한 부여

인증 서버에 요청을 처음 보내는 경우 사용자에게 보이는 페이지를 로그인 페이지로 변경하고 사용자 데이터에 접근 동의를 얻는다.(최초 1회). 이후에는 로그인만 진행한다. 로그인이 성공하면 권한 부여 서버는 데이터에 접근할 수 있게 인증 및 권한 부여를 수신한다.

  1. 인증 코드 제공

사용자가 로그인에 성공하면 권한 요청시 파라미터로 보낸 redirect_uri로 리다이렉션한다.

이때 파라미터에 인증 코드를 함께 제공한다.

GET http://localhost:8080/myapp?code=a2sdf38s

인증 코드를 받으면 액세스 토큰으로 교환해야 한다!

액세스 토큰은 로그인 세션에 대한 보안 자격을 증명하는 식별 코드.

POST spring-auhorization-server.example/token {
"client_id" :"48a4893g"
"client_secret" : "shuefh",
"redirect_uri" : "http://localhost:8080/myapp"
"grant_type" : "authorization_code",
"code" : "siuefhufei1"
}
  • client_secret : Oauth 서비스에 등록할 때 제공받는 비밀 키
  • grant_type : 권한 유형을 확인하는데 사용하는데, 이때는 authorization_code로 설정해야 한다. 권한 서버는 요청 값을 기반으로 유효한 정보인지 확인하고, 유효한 정보라면 액세스 토큰으로 응답한다.
//토큰 응답 값
{
	"access_token": "siufeh",
	"token_type" : "Bearer",
	"expires_in" : 3600,
	"scope" : "openid profile"
}
  1. 액세스 토큰으로 API 응답 & 반환

제공받은 엑세스 토큰으로 리소스 오너의 정보를 가져온다. 정보가 필요할 때마다 API호출을 통해 정보를 가져오고, 리소스 서버는 액세스 토큰이 유효한지 검사 후 응답한다.

//리소스 오너의 정보를 가져오기 위한 요청
GET spring-authorization-resource-server.example.com/userinfo
Header : Authorization: Bearer aasdffb

이렇게 Userinfo를 받아올 수 있다. 그럼 이제 실제로 구현을 해보자!!!!!!!!!!

💛 구현하기

🍩 application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/[db이름]?serverTimezone=Asia/Seoul
    username: root
    password: 11111111
  data:
    redis:
      host: localhost
      database: 0
      port: 6379
  jpa:
    database-platform: org.hibernate.dialect.MySQL8Dialect
    open-in-view: true

    hibernate:
      ddl-auto: update    
  
  # OAuth2 설정 
  security:
    oauth2:
      client:
        registration:
          google:
            client-id:
            client-secret: 
            scope:
              - profile
              - email
          kakao:
            client-id: 
            client-secret: 
            authorization-grant-type: authorization_code
            client-authentication-method: POST
            client-name: kakao
            scope:
              - profile_nickname
              - account_email
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            
        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
#jwt 비밀키            
jwt:
  secret_key: springboot-jwt

먼저 yml 설정을 시작한다. DB는 Mysql, RefreshToken을 저장하기 위한 NoSql은 Redis를 사용하였다!

  • OAuth2는 나는 카카오, 구글만 사용하기 때문에, 두 소셜 로그인에 정보들과 받고싶은 scope를 등록하였다. 카카오는 provider로 uri들을 지정해주어야 한다. 구글은 지정하지 않아도 된다.

🥨 SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtUtils jwtUtils;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final RefreshTokenRepository refreshTokenRepository;
    private final MemberService memberService;
    private final CustomAccessDeniedHandler accessDeniedHandler;
    private final CustomAuthenticationEntryPoint authenticationEntryPoint;

    @Bean
    public OAuth2AuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository
            () {
        return new OAuth2AuthorizationRequestBasedOnCookieRepository();
    }

    @Bean
    public OAuth2SuccessHandler oAuth2SuccessHandler() {
        return new OAuth2SuccessHandler(jwtUtils,
                refreshTokenRepository, memberService, oAuth2AuthorizationRequestBasedOnCookieRepository());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .httpBasic(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/api/**").authenticated()
                        .anyRequest().permitAll())
                .addFilterBefore(new JwtAuthenticationFilter(jwtUtils), UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(exception -> exception.accessDeniedHandler(accessDeniedHandler)
                        .authenticationEntryPoint(authenticationEntryPoint))
                .oauth2Login(oauth2 ->
                        oauth2.authorizationEndpoint(authorization -> authorization.authorizationRequestRepository(
                                        oAuth2AuthorizationRequestBasedOnCookieRepository()))
                                .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
                                 .successHandler(oAuth2SuccessHandler()))
                .build();
    }
}

본격적으로 코드를 짜기 전에, SecurityConfigfilterChain을 살펴보자. oAuth2와 Jwt를 이용할 것이기 때문에 httpBasic, formLogin은 비활성화하고, session을 사용하지 않을 것이기 때문에 STATELESS로 설정해준다.

  • authoirizeHttpRequests : 나는 api/**주소로 오는 요청은 인증이 필요하도록 설정했고, 나머지 요청에 대해서는 permit으로 두었다. 나중에 Role 권한도 추가하면서 애플리케이션에 맞게 지정해주면된다.
  • addFilterBefore : Jwt 토큰 인증을 위한 JwtAuthenticationFilter를 추가한다.
  • exceptionHandling : accessDeniedHander는 인증된 사용자이지만 ROLE 권한이 없을 때를 위한 핸들러, authenticationEntryPoint는 인증되지 않은 사용자가 인증이 필요한 요청을 했을 때 에러 처리를 도와준다.
  • oauth2Login: oauth2 로그인에 관한 정보들을 설정한다.
  • autorizationEndpoint: 자격 증명을 얻기 위한 엔트포인트로, Authorization request와 관련된 state(redirect-uri, client-id.. 등)를 저장하기 위해 oAuth2AuthorizationRequestBasedOnCookieRepository를 사용한다. 저장하는 이유는 oauth2 로그인 프로세스가 여러번의 요청으로 진행이 되기 때문에, 이 요청정보를 유지하여 프로세스를 처리하기 위해서이다. 스프링 시큐리티가 이 클래스를 사용하여 쿠키에 저장된 인증 요청 정보를 가져와 인증프로세스를 진행할 때 필요한 데이터를 제공받는다.
  • 콜백 결과 성공이고 인가코드를 얻었다면, spring security가 소셜의 accessToken을 얻기 위해 token 요청을 하고, token을 받아온다.
  • customOAuth2UserService : 소셜 accessToken을 통해 UserInfo를 받아와 UserDetails를 생성하는 서비스이다. 이것까지 완료가 됐다면, successHandler로 이동한다.
  • oAuth2SuccessHandler: 생성한 UserDetails Authentication을 통해 Jwt token을 생성하고, 리다이렉트를 진행한다.

그럼 이제 필요한 클래스들을 생성해보자!

🍪 CookieUtil

refreshToken을 쿠키에 저장해 보낼 것이기 때문에, 쿠키관련 Util을 설정해준다.

public class CookieUtil {

    //요청값(이름, 값, 만료 기간)바탕으로 쿠키 추가
    public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
        Cookie cookie = new Cookie(name,value);
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }

    //쿠키의 이름을 입력받아 쿠키 삭제
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
                                    String name) {
        Cookie[] cookies = request.getCookies();

        if( cookies == null) return;

        for (Cookie cookie : cookies) {
            if (name.equals(cookie.getName())) {
                cookie.setPath("/");
                cookie.setMaxAge(0);
                cookie.setValue("");
                response.addCookie(cookie);
            }
        }
    }

    //객체를 직렬화해 쿠키 값으로 변환
    public static String serialize(Object obj) {
        return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize((Serializable) obj));
    }

    //쿠키를 역직렬화 해 객체로 변환
    public static <T> T deserialize(Cookie cookie, Class<T> cls) {
        return cls.cast(
                SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
    }


}

🍪 OAuth2AuthorizationRequestBasedOnCookieRepository

인증 요청 정보 request를 저장하기 위한 레포지토리를 생성한다.


/*
인증 요청 정보 http request를 쿠키에 저장, 불러오기 ,삭제
 */
public class OAuth2AuthorizationRequestBasedOnCookieRepository
        implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
    public final static String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
    private final static int COOKIE_EXPIRE_SECONDS = 18000;
    
   //인증 요청 삭제
    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
                                                                 HttpServletResponse response) {
        return this.loadAuthorizationRequest(request);
    }
    
    // 쿠키로부터 요청 정보를 가져오기
    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
        return CookieUtil.deserialize(cookie,OAuth2AuthorizationRequest.class);
    }

    //인증 요청 정보를 쿠키에 저장하기
    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
                                         HttpServletRequest request, HttpServletResponse response) {
        if (authorizationRequest == null) {
            removeAuthorizationRequestCookies(request,response);
            return;
        }
        CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
                CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
    }

    //쿠키에 등록된 인증 요청 정보를 삭제
    public void removeAuthorizationRequestCookies(HttpServletRequest request,
                                                  HttpServletResponse response) {
        CookieUtil.deleteCookie(request,response,OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
    }

🍥 OAuth2Attributes

그전에 서로 다른 소셜 로그인을 처리하기 위한 OAuthAttributes 클래스를 생성해보자!~

@Getter
@ToString
@Builder
public class OAuthAttributes {
    private Map<String, Object> attributes;  //oAuth2 반환하는 유저의 정보
    private String nameAttributesKey;
    private String name;
    private String email;
    private String profileImage;

	//social name에 따라 userinfo를 얻어옴
    public static OAuthAttributes of(String socialName, Map<String, Object> attributes) {
        if ("kakao".equals(socialName))
            return ofKakao("id", attributes);
        else if ("google".equals(socialName))
            return ofGoogle("sub", attributes);

        return null;
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
        return OAuthAttributes.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .profileImage((String) attributes.get("picture"))
                .attributes(attributes)
                .nameAttributesKey(userNameAttributeName)
                .build();
    }

    private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
        Map<String, Object> kakaoProfile = (Map<String, Object>) attributes.get("profile");
        return OAuthAttributes.builder()
                .email(String.valueOf(kakaoAccount.get("email")))
                .name(String.valueOf(kakaoProfile.get("nickname")))
                .profileImage(String.valueOf(kakaoProfile.get("profile_image_url")))
                .nameAttributesKey(userNameAttributeName)
                .attributes(attributes)
                .build();

    }

    public Member toEntity(String registrationId) {
        return Member.builder()
                .name(name)
                .email(email)
                .image(profileImage)
                .role(Role.USER)
                .socialType(SocialType.valueOf(registrationId))
                .build();
    }
}

내가 소셜로그인에서 얻어올 정보는 사용자 이름, 사용자의 이메일, 그리고 사용자의 프로필 사진이기 때문에 3개의 필드를 작성하였다.

  • ofGoogle : 구글이 userinfo를 제공하는 리턴값에 맞추어 해당 필드들을 채워준다.
  • ofKakao : 카카오가 userinfo를 제공하는 리턴값에 맞추어 해당 필드들을 채워준다. 카카오는 kakaoAccount 하위에 kakaoProfile이라는 attributes가 또 있기 때문에 유의해서 가져오면 된다.
  • toEntity : 해당 애트리뷰트를 통해 Member entity를 생성한다.

🍥 CustomOAuth2UserService

@Service
@Transactional
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final MemberRepository memberRepository;


    //유저의 정보를 받아오는 함수
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> service = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = service.loadUser(userRequest);

        Map<String, Object> originAttributes = oAuth2User.getAttributes();

        //oAuth2 서비스 id (google, kakao)
        String registrationId = userRequest.getClientRegistration().getRegistrationId(); //소셜 정보 가져오기

        //attribute를 서비스 유형에 맞게 담음
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, originAttributes);
        Member member = saveOrUpdate(registrationId, attributes);
        Set<GrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority(member.getRole().toString()));

        return new DefaultOAuth2User(authorities, attributes.getAttributes(), attributes.getNameAttributesKey());

    }


    //이미 존재하는 회원이라면 이름과 프로필 사진 업데이트,
    //처음 가입한다면 DB에 생성
    private Member saveOrUpdate(String registrationId, OAuthAttributes attributes) {
        Member member = memberRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getProfileImage()))
                .orElse(attributes.toEntity(registrationId.toUpperCase()));

        return memberRepository.save(member);
    }


}

loadUser를 통해 유저의 정보를 받아와 DefaultOAuth2User를 반환한다. attributes를 서비스 유형에 맞게 담고, saveOrUpdate함수를 통해, 이미 존재하는 회원이라면 이름과 프로필 사진을 업데이트시키고, 처음 가입한다면 DB에 생성하도록 한다. 여기서 나는 사용자의 Email로만 비교를 하기 때문에, 소셜타입이 달라도 이메일이 같으면 같은 사용자로 취급이 되게 되어있다. 따라서 이메일이같아도 소셜타입이 다르면 다른 사용자로 로그인 되게 하려면 추가적인 로직이 필요하다!

🍭 OAuth2SuccessHandler

@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(14);
    private static final Duration ACCESS_TOKEN_DURATION = Duration.ofMinutes(30);


    private final JwtUtils jwtUtils;
    private final RefreshTokenRepository refreshTokenRepository;
    private final MemberService memberService;
    private final OAuth2AuthorizationRequestBasedOnCookieRepository
            oAuth2AuthorizationRequestBasedOnCookieRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication) throws IOException {
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        Member member = memberService.findByEmail((String) oAuth2User.getAttributes().get("email"));

        //리프레시 토큰 생성 -> 저장 -> 쿠키에 저장
        String refreshToken = jwtUtils.generateToken(member, REFRESH_TOKEN_DURATION);
        saveRefreshToken(member.getId(), refreshToken);
        addRefreshTokenToCookie(request, response, refreshToken);

        //액세스 토큰 생성
        String accessToken = jwtUtils.generateToken(member, ACCESS_TOKEN_DURATION);
        String targetUrl = getTargetUrl(accessToken);

        //인증 관련 설정값, 쿠키를 제거
        clearAuthenticationAttributes(request, response);

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

    //생성된 리프레시 토큰을 db에 저장
    private void saveRefreshToken(Long memberId, String newRefreshToken) {
        RefreshToken refreshToken = refreshTokenRepository.findByMemberId(memberId)
                .map(entity -> entity.update(newRefreshToken))
                .orElse(new RefreshToken(memberId, newRefreshToken));

        refreshTokenRepository.save(refreshToken);
    }

    //생성된 리프레시 토큰은 쿠키에 저장
    private void addRefreshTokenToCookie(HttpServletRequest request,
                                         HttpServletResponse response, String refreshToken) {
        int cookieMaxAge = (int) REFRESH_TOKEN_DURATION.toSeconds();
        CookieUtil.deleteCookie(request, response, REFRESH_TOKEN);
        CookieUtil.addCookie(response, REFRESH_TOKEN, refreshToken, cookieMaxAge);
    }


    //인증 관련 설정값, 쿠키 제거
    private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
        super.clearAuthenticationAttributes(request);
        oAuth2AuthorizationRequestBasedOnCookieRepository.removeAuthorizationRequestCookies(request, response);
    }

    //액세스 토큰을 패스에 추가
    private String getTargetUrl(String token) {
        return UriComponentsBuilder.fromUriString("/")
                .queryParam("token", token)
                .build().toString();
    }


}
  • onAuthenticationSuccess : 이 메소드를 오버라이드 하여 oauth2Login 성공 시 해야할 일들을 실행해준다.
  • 로그인 성공 후 반환된 oAuth2User를 통해 member를 찾고, refreshToken을 생성하여 쿠키에 저장한다. 물론 redis DB에도 저장해준다.
  • accessToken도 생성한다. accessTokentargetUrl에 추가하여 param으로 전달해준다.
  • clearAuthenticationAttributes를 통해 인증 관련 설정값과 쿠키를 제거해준 후, targetUrl로 redirect를 보내준다. 프론트와 통신을 할 것이라면 redirect를 프론트쪽으로 넘겨주면 된다.

    sendRedirect에서 디버깅을 찍어보면, 응답 쿠키에 refresh_token과, 삭제한(만료설정한) oauth request가 담겨있는 것을 볼 수 있다 !!

🍰 소셜로그인 완료!

로그인을 진행하면,

http://localhost:8080/?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTAxMzM0NjQsImV4cCI6MTcxMDEzMzUyNCwic3ViIjoidG5hbHMyMzg0QGdtYWlsLmNvbSIsImlkIjoxfQ.8y627jGX2sc9YBnZAdkV_ACs4NMSOgiSQWRcQhrWsVk

이와 같은 경로로 리다이렉트 되고, 파라미터로 accessToken이, 쿠키로 refreshToken이 전달되게 된다.

프론트에서는 이 token을 저장해두고, accessToken은 요청할때 헤더에 담아 요청하면 된다. 만약 accessToken의 유효기간이 끝나 더이상 요청할 수 없다면, 새로운 accessToken을 발급받는 요청을 refreshToken과 함께 보내면 된다. 그러면 이제 헤더의 accessToken을 확인하여 인증을 진행하는 JwtAuthenticationFilter를 살펴보자.
JWT Util과 redis를 통한 refreshToken은 여기를 참고하자.
스프링 시큐리티 + JWT 적용하기
스프링부트에서 Redis를 사용해보자!

🍫 JwtAuthenticationFilter

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String tokenValue = jwtUtils.getJwtFromHeader(req);
        //가져온 토큰이 유효한지 확인
        if(tokenValue != null) {
            if (jwtUtils.validateToken(tokenValue)) {
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                Authentication authentication = jwtUtils.getAuthentication(tokenValue);
                context.setAuthentication(authentication);

                SecurityContextHolder.setContext(context);

            }
        }

        filterChain.doFilter(req, res);
    }
}

이 filter에서는, 헤더의 accessToken을 통해 authentication을 부여하여 인증이 필요한 요청을 수행할 수 있도록 해준다.
먼저 헤더의 토큰을 가져온 다음, 토큰이 비어있지 않다면 가져온 토큰이 유효한 토큰인지 확인 후, jwt 토큰을 통해 우리 DB의 Member 정보와 비교하여 인증 정보를 생성하여 SecurityContextHolder에 저장한다.

만약 이 필터에서 멤버정보로 authentication이 생성되지 않고, 인증되지않은 사용자로 남게 된다면, securityChain의 authenticationEntryPoint로 이동하게 된다.


이렇게 JWT와 OAUTH를 이용하여 로그인을 구현해봤다! 정말 어려웠지만 많은 공부가 되었다..
하지만 프론트와 직접 연동하기 위해서는 더 공부가 필요할 것도 같다. 다음엔 WebClient를 통해 소셜로그인을 처리하는 방법도 공부해보자!

0개의 댓글