[SpringSecurity/OAuth2] 스프링 OAuth2 클라이언트 JWT

황인찬·2024년 8월 12일
1
post-thumbnail

동작 원리 및 프론트/백 책임 분배

OAuth2 Code Grant 순서

  1. 로그인 페이지
  2. 성공 후 코드 발급(redirect_uri)
  3. 코드를 통해 Access 토큰 요청
  4. Access 토큰 발급
  5. Access 토큰을 통해 유저 정보 요청
  6. 유저 정보 획득

세션 방식에서 OAuth2 동작 원리

  • 세션 방식은 로그인이 성공하면 세션을 생성하여 앞으로의 요청에도 사용자를 알아챌 수 있음

JWT 방식에서의 OAuth2 구현 고민

JWT 방식에서는 로그인(인증)이 성공하면 JWT 발급 문제와 웹/하이브리드/네이티브앱 방식의 특징에 따라 OAuth2 Code Grant 방식의 책임을 프론트에 둘 것인지 백에 둘 것인지 고민

  • 로그인(인증)이 성공하면 JWT를 발급해야하는 문제
    • 프론트에서 로그인 경로의 하이퍼링크를 실행하면 소셜 로그인 창이 뜨고 로그인 로직 수행
    • 로그인이 성공되면 JWT를 발급받는데 하이퍼링크로 실행해서 JWT를 발급 받는 로직이 없음(redirect_url 설정에 따라 많은 고민이 필요)
    • API Client(axios, fetch)로 요청을 보내면 백엔드 측으로 요청이 전송되지만 외부 서비스 로그인 페이지를 확인 할 수 없음
  • 웹/하이브리드/네이티브앱 별 특징
    • 웹에서 편하게 사용할 수 있는 웹페이지가 앱에서는 웹뷰로 보이기 때문에 UX 관점에서 사용자 경험이 좋지 못할 수 있음
    • 앱 환경에서 쿠키 소멸 현상이 있음

위와 같은 문제로 OAuth2 Code Grant 동작에 대한 redirect_uri, Access 토큰 발급 문제를 어느 단에서 처리해야 하는지 고민이 많고 잘못된 구현 방법도 많이 있음

프론트/백 책임 분배

  • 모든 책임을 프론트가 맡음

프론트에서(로그인 -> Access 토큰 발급 -> 유저 정보 획득) 과정을 수행한 뒤 백엔드에서(유저 정보 확인 -> JWT 발급) 과정을 수행하며 주로 네이티브 앱에서 사용하는 방식
단, 프론트에서 보낸 유저 정보의 진위 여부를 파악하는 추가 로직 필요

  • 책임을 프론트/백이 나눠 가짐: 잘못된 방식

    • 프론트에서(로그인 -> 코드 발급) 과정을 수행하고 백에서(코드 -> Access 토큰 발급 -> 유저 정보 획득 -> JWT 발급) 과정을 수행
    • 프론트에서 (로그인 → 코드 발급 → Access 토큰) 과정을 수행하고 Access 토큰을 백엔드에 전송, 백엔드에서 (Access 토큰 → 유저 정보 획득 → JWT 발급) 과정 수행
      대형 서비스 개발 포럼 및 보안 규격에서는 코드/Access 토큰을 전송하는 방법은 지양함
  • 모든 책임을 백엔드가 맡음

프론트에서 백엔드의 OAuth2 경로로 하이퍼 링크를 요청하고 백엔드에서(로그인 페이지 요청 -> 코드 발급 -> Access 토근 -> 유저 정보 획득 -> JWT 발급) 과정을 수행. 주로 웹앱/모바일앱 통합 환경 서버에서 사용
-> 백엔드에서 JWT를 발급하는 방식과 프론트에서 받는 로직 고민 필요


프로젝트 생성 및 의존성 추가

필수 의존성

  • Lombok
  • Spring Web
  • Spring Security
  • OAuth2 Client
  • Spring Data JPA
  • MySQL Driver

JWT 의존성 추가

  • 0.12.3 버전으로 진행
dependencies {
    implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
    implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}

기본 Controller 생성

  • MainController
@RestController
public class MainController {

    @GetMapping("/")
    public String mainAPI() {
        return "main route";
    }
}
  • MyController
@RestController
public class MyController {

    @GetMapping("/my")
    public String myAPI() {
        return "my route";
    }
}

동작 원리

동작 모식도

각각의 필터가 동작하는 주소

  • JWTFilter
모든 주소에서 동작
  • OAuth2AuthorizationRequestRedirectFilter
/oauth2/authorization/서비스명

/oauth2/authorization/naver
/oauth2/authorization/google
  • OAuth2LoginAuthenticationFilter
/login/oauth2/code/서비스명

/login/oauth2/code/naver
/login/oauth2/code/google

OAuth2 클라이언트에서 구현해야 하는 부분

  • OAuth2UserService
  • OAuth2User
  • LoginSuccessHandler

JWT에서 구현해야 하는 부분

  • JWTFilter
  • JWTUtil: JWT를 발급 및 검증하는 클래스

OAuth2 변수 역할

OAuth2 변수 설정

  • application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          서비스명:
            client-name: 서비스명
            client-id: 서비스에서 발급 받은 아이디
            client-secret: 서비스에서 발급 받은 비밀번호
            redirect-uri: 서비스에서 등록한 우리쪽 로그인 성공 uri
            authorization-grant-type: authorization_code
            scope: 리소스 서버에서 가져올 데이터 범위
        provider:
          서비스명:
            authorization-uri: 서비스 로그인 창 주소
            token-uri: 토큰 발급 서버 주소
            user-info-uri: 사용자 데이터 획득 주소
            user-name-attribute: 응답 데이터 변수

registration과 provider

  • registration은 외부 서비스에서 우리 서비스를 특정하기 위해 등록 필수
  • provider는 서비스 별로 정해진 값이 있으며 OAuth2 클라이언트 의존성이 유명한 서비스의 경우 내부적으로 데이터를 가지고 있음(구글, Github 등)

OAuth2UserService 응답 받기

OAuth2UserService

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        System.out.println(oAuth2User);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        OAuth2Response oAuth2Response = null;
        if (registrationId.equals("naver")) {
            oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
        } else if (registrationId.equals("google")) {
            oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
        } else {
            return null;
        }

        //추후 작성

    }
}

OAuth2Response

  • 네이버 데이터 : json
{
	resultcode=00, message=success, response={id=123123123, name=찬이}
}
  • 구글 데이터 : json
{
	resultcode=00, message=success, id=123123123, name=찬이
}
  • OAuth2Response(dto)
public interface OAuth2Response {

    //제공자 ex) naver, google ...
    String getProvider();
    //제공자에서 발급해주는 아이디(번호)
    String getProviderId();
    //이메일
    String getEmail();
    //사용자 이름
    String getName();
}
  • NaverResponse(dto)
public class NaverResponse implements OAuth2Response{

    private final Map<String, Object> attribute;

    public NaverResponse(Map<String, Object> attribute) {
        //response 안에 데이터가 감싸져 들어오기 때문
        this.attribute = (Map<String, Object>) attribute.get("response");
    }

    @Override
    public String getProvider() {
        return "naver";
    }

    @Override
    public String getProviderId() {
        return attribute.get("id").toString();
    }

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

    @Override
    public String getName() {
        return attribute.get("name").toString();
    }
}
  • GoogleResponse(dto)
public class GoogleResponse implements OAuth2Response{

    private final Map<String, Object> attribute;

    public GoogleResponse(Map<String, Object> attribute) {
        this.attribute = attribute;
    }

    @Override
    public String getProvider() {
        return "google";
    }

    @Override
    public String getProviderId() {
        return attribute.get("sub").toString();
    }

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

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

SecurityConfig에 OAuth2UserService 등록

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomClientRegistrationRepo customClientRegistrationRepo;
    private final CustomOAuth2UserService customOAuth2UserService;

    public SecurityConfig(CustomClientRegistrationRepo customClientRegistrationRepo, CustomOAuth2UserService customOAuth2UserService) {
        this.customClientRegistrationRepo = customClientRegistrationRepo;
        this.customOAuth2UserService = customOAuth2UserService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //csrf disable
        http
                .csrf((csrf) -> csrf.disable());
        //formLogin disable
        http
                .formLogin((auth) -> auth.disable());
        //httpBasic disable
        http
                .httpBasic((auth) -> auth.disable());
        //oauth2 설정
        http
                .oauth2Login((oauth2) -> oauth2
                        .clientRegistrationRepository(customClientRegistrationRepo.clientRegistrationRepository())
                        .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
                                .userService(customOAuth2UserService)));
        //경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/").permitAll()
                        .anyRequest().authenticated());
        //세션 설정 STATELESS
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));   

        return http.build();
    }
}

OAuth2UserService 나머지 구현

OAuth2UserService 완료

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        System.out.println(oAuth2User);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        OAuth2Response oAuth2Response = null;
        if (registrationId.equals("naver")) {
            oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
        } else if (registrationId.equals("google")) {
            oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
        } else {
            return null;
        }

        //리소스 서버에서 발급 받은 정보로 사용자를 특정할 아이디 값 생성
        String username = oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
        UserDto userDto = new UserDto();
        userDto.setUsername(username);
        userDto.setName(oAuth2Response.getName());
        userDto.setRole("ROLE_USER");

        return new CustomOAuth2user(userDto);
    }
}

UserDto 작성

@Getter
@Setter
public class UserDto {

    private String username;
    private String name;
    private String role;
}

CustomOAuth2User 작성

public class CustomOAuth2user implements OAuth2User {

    private final UserDto userDto;

    public CustomOAuth2user(UserDto userDto) {
        this.userDto = userDto;
    }

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userDto.getRole();
            }
        });
        return collection;
    }

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

    public String getUsername() {
        return userDto.getUsername();
    }
}

유저 정보 DB 저장

User 엔티티 작성

@Entity
@Getter
@Setter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String name;

    private String email;

    private String role;
}

UserRepository 작성

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

CustomOAuth2UserService DB 저장 로직 추가

@Service
@Transactional(readOnly = true)
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

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

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

        System.out.println(oAuth2User);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        OAuth2Response oAuth2Response = null;
        if (registrationId.equals("naver")) {
            oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
        } else if (registrationId.equals("google")) {
            oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
        } else {
            return null;
        }

        //리소스 서버에서 발급 받은 정보로 사용자를 특정할 아이디 값 생성
        String username = oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
        User findUser = userRepository.findByUsername(username);
        if (findUser == null) {
            User user = new User();
            user.setUsername(username);
            user.setName(oAuth2Response.getName());
            user.setEmail(oAuth2Response.getEmail());
            user.setRole("ROLE_USER");
            userRepository.save(user);

            UserDto userDto = new UserDto();
            userDto.setUsername(username);
            userDto.setName(oAuth2Response.getName());
            userDto.setRole("ROLE_USER");

            return new CustomOAuth2user(userDto);
        } else {
            findUser.setEmail(oAuth2Response.getEmail());
            findUser.setName(oAuth2Response.getName());

            UserDto userDto = new UserDto();
            userDto.setUsername(findUser.getUsername());
            userDto.setName(oAuth2Response.getName());
            userDto.setRole(findUser.getRole());

            return new CustomOAuth2user(userDto);
            
        }

    }
}

JWT 발급 및 검증 클래스

JwtUtil

  • 토큰에 저장될 정보
    • username
    • role
    • 생성일
    • 만료일
  • 구현 메소드
    • JwtUtil 생성자
    • username 확인 메소드
    • role 확인 메소드
    • 만료일 확인 메소드
  • JwtUtil 코드
@Component
public class JwtUtil {

    private SecretKey secretKey;

    public JwtUtil(@Value("${spring.jwt.secret}") String secretKey) {
        this.secretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    public String getUsername(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
    }

    public String getRole(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
    }

    public Boolean isExpired(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

    public String createJwt(String username, String role, Long expiredMs) {
        return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(secretKey)
                .compact();
    }
}

로그인 성공 JWT 발급

CustomSuccessHandler 및 JwtUtil 주입

@Component
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtUtil jwtUtil;

    public CustomSuccessHandler(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //OAuth2User
        CustomOAuth2user customUserDetails = (CustomOAuth2user) authentication.getPrincipal();

        //JWT 발급할 때 username과 role 값을 넣기로 설정했기 때문에 추출해야함
        //username 추출
        String username = customUserDetails.getUsername();
        //role 추출
        Collection<? extends GrantedAuthority> authorities = customUserDetails.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority auth = iterator.next();
        String role = auth.getAuthority();


        String token = jwtUtil.createJwt(username, role, 60 * 60 * 60L);

        response.addCookie(createCookie("Authorization", token));
        response.sendRedirect("http://localhost:3000/");

    }

    private Cookie createCookie(String key, String value) {

        Cookie cookie = new Cookie(key, value);
        cookie.setMaxAge(60 * 60 * 60); //쿠키가 살아있을 시간
//        cookie.setSecure(true); https에서만 동작하도록 하는 설정(local 환경은 http이므로 주석처리)
        cookie.setPath("/"); //전역에서 쿠키 확인 가능
        cookie.setHttpOnly(true); //자바 스크립트가 쿠키를 가져가지 못하게 하는 설정

        return cookie;
    }

}

SecurityConfig에 CustomSuccessHandler 등록

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomClientRegistrationRepo customClientRegistrationRepo;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final CustomSuccessHandler customSuccessHandler;
    private final JwtUtil jwtUtil;

    public SecurityConfig(CustomClientRegistrationRepo customClientRegistrationRepo, CustomOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JwtUtil jwtUtil) {
        this.customClientRegistrationRepo = customClientRegistrationRepo;
        this.customOAuth2UserService = customOAuth2UserService;
        this.customSuccessHandler = customSuccessHandler;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //csrf disable
        http
                .csrf((csrf) -> csrf.disable());
        //formLogin disable
        http
                .formLogin((auth) -> auth.disable());
        //httpBasic disable
        http
                .httpBasic((auth) -> auth.disable());
        //oauth2 설정
        http
                .oauth2Login((oauth2) -> oauth2
                        .clientRegistrationRepository(customClientRegistrationRepo.clientRegistrationRepository())
                        .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
                                .userService(customOAuth2UserService))
                        .successHandler(customSuccessHandler));
        //경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/").permitAll()
                        .anyRequest().authenticated());
        //세션 설정 STATELESS
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }

JWT 검증 필터

JwtFilter 구현

public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

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

        //cookie들을 불러온 뒤 Authorization key에 담긴 쿠키를 찾음
        String authorization = null;
        Cookie[] cookies = request.getCookies();

        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("Authorization")) {
                authorization = cookie.getValue();
            }
        }

        //Authorization 검증
        if(authorization == null) {
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료(필수)
            return;
        }

        //토큰
        String token = authorization;

        //토큰 소멸 시간 검증
        if (jwtUtil.isExpired(token)) {
            System.out.println("token is expired");
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료(필수)
            return;
        }

        //토큰에서 username과 role 획득
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        //userDTO를 생성하여 값 set
        UserDto userDto = new UserDto();
        userDto.setUsername(username);
        userDto.setRole(role);

        //UserDetails에 회원 정보 객체 담기
        CustomOAuth2user customOAuth2user = new CustomOAuth2user(userDto);

        //스프링 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2user, null, customOAuth2user.getAuthorities());

        //세션에 사용자 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}

SecurityConfig에 등록

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomClientRegistrationRepo customClientRegistrationRepo;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final CustomSuccessHandler customSuccessHandler;
    private final JwtUtil jwtUtil;

    public SecurityConfig(CustomClientRegistrationRepo customClientRegistrationRepo, CustomOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JwtUtil jwtUtil) {
        this.customClientRegistrationRepo = customClientRegistrationRepo;
        this.customOAuth2UserService = customOAuth2UserService;
        this.customSuccessHandler = customSuccessHandler;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //csrf disable
        http
                .csrf((csrf) -> csrf.disable());
        //formLogin disable
        http
                .formLogin((auth) -> auth.disable());
        //httpBasic disable
        http
                .httpBasic((auth) -> auth.disable());
        //필터 등록
        http
                .addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
        //oauth2 설정
        http
                .oauth2Login((oauth2) -> oauth2
                        .clientRegistrationRepository(customClientRegistrationRepo.clientRegistrationRepository())
                        .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
                                .userService(customOAuth2UserService))
                        .successHandler(customSuccessHandler));
        //경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/").permitAll()
                        .anyRequest().authenticated());
        //세션 설정 STATELESS
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

CORS 설정

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomClientRegistrationRepo customClientRegistrationRepo;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final CustomSuccessHandler customSuccessHandler;
    private final JwtUtil jwtUtil;

    public SecurityConfig(CustomClientRegistrationRepo customClientRegistrationRepo, CustomOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JwtUtil jwtUtil) {
        this.customClientRegistrationRepo = customClientRegistrationRepo;
        this.customOAuth2UserService = customOAuth2UserService;
        this.customSuccessHandler = customSuccessHandler;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //cors 설정
        http
                .cors((corsCustomizer) -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration configuration = new CorsConfiguration();
                        configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000")); //프론트 주소
                        configuration.setAllowedMethods(Collections.singletonList("*")); //get, put, post 등 모든 요청 허용
                        configuration.setAllowCredentials(true); //credential 값 가지고 올 수 있도록 허용
                        configuration.setAllowedHeaders(Collections.singletonList("*")); //어떤 헤더를 받을 수 있는지 설정
                        configuration.setMaxAge(3600L);

                        //우리가 데이터를 줄 경우 웹페이지에서 보이게 할 수 있는 방법
                        configuration.setExposedHeaders(Collections.singletonList("Set-Cookie"));
                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));

                        return configuration;
                    }
                }));
        //csrf disable
        http
                .csrf((csrf) -> csrf.disable());
        //formLogin disable
        http
                .formLogin((auth) -> auth.disable());
        //httpBasic disable
        http
                .httpBasic((auth) -> auth.disable());
        //필터 등록
        http
                .addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
        //oauth2 설정
        http
                .oauth2Login((oauth2) -> oauth2
                        .clientRegistrationRepository(customClientRegistrationRepo.clientRegistrationRepository())
                        .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
                                .userService(customOAuth2UserService))
                        .successHandler(customSuccessHandler));
        //경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/").permitAll()
                        .anyRequest().authenticated());
        //세션 설정 STATELESS
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

CorsMvcConfig

@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .exposedHeaders("Set-Cookie")
                .allowedOrigins("http://localhost:3000");
    }
}

재로그인 무한 루프 오류

1. SecurityConfig 수정

//JWTFilter 추가
http
        .addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class);

2. JwtFilter 수정

String requestUri = request.getRequestURI();

if (requestUri.matches("^\\/login(?:\\/.*)?$")) {

    filterChain.doFilter(request, response);
    return;
}
if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {

    filterChain.doFilter(request, response);
    return;
}

참조링크
개발자 유미 - 스프링 OAuth2 클라이언트 JWT

profile
찬이's 개발로그

0개의 댓글