๐Ÿ‘€ ํ˜„์žฌ ์ƒํ™ฉ ๋ฐ ๋ฐฐ๊ฒฝ ์„ค๋ช…

  • ํ˜„์žฌ JWT ๊ธฐ๋ฐ˜ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ ์ค‘์ด๋ฉฐ, ํด๋ผ์ด์–ธํŠธ๋Š” React Native๋กœ, ์„œ๋ฒ„๋Š” Spring Boot๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  ์žˆ๋‹ค. 
  • ์†Œ์…œ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์™ธ๋ถ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์ธ์ฆ์„ ๋งˆ์นœ ํ›„, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

 

โ–ถ OAuth ๋กœ๊ทธ์ธ์˜ ์ธ์ฆ ํ๋ฆ„

  1. ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ผ์ด์–ธํŠธ๋ฅผ ํ†ตํ•ด ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ์‹œ๋„ํ•œ๋‹ค.
  2. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ธ์ฆ ์„œ๋ฒ„๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ์š”์ฒญํ•œ๋‹ค.
  3. ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ฆ์„ ์™„๋ฃŒํ•˜๋ฉด, ์ธ์ฆ ์„œ๋ฒ„๋Š” Authorization Code๋ฅผ ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌํ•œ๋‹ค.
  4. ํด๋ผ์ด์–ธํŠธ๋Š” Authorization Code๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ ์„œ๋ฒ„์— Access Token์„ ์š”์ฒญํ•˜๊ณ  ๋ฐœ๊ธ‰๋ฐ›๋Š”๋‹ค.
  5. Access Token์„ ์‚ฌ์šฉํ•ด ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„์— ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์š”์ฒญํ•œ๋‹ค.
  6. ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋กœ๊ทธ์ธ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.
์ธ์ฆ ์„œ๋ฒ„: ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ํ™•์ธํ•˜๊ณ , ์ธ์ฆ์ด ์„ฑ๊ณตํ•˜๋ฉด Access Token์„ ๋ฐœ๊ธ‰
๋ฆฌ์†Œ์Šค ์„œ๋ฒ„: ์ธ์ฆ ์„œ๋ฒ„์—์„œ ๋ฐœ๊ธ‰ํ•œ Access Token์„ ๊ฒ€์ฆํ•˜์—ฌ, ๋ณดํ˜ธ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณต
๐Ÿ”ฝ ๊ณผ์ •
์ธ์ฆ ์„œ๋ฒ„ ๋ฐ ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„๋Š” ๋ชจ๋‘ OAuth ์„œ๋ฒ„์ด๋‹ค. ์ธ์ฆ ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž์˜ ์ž๊ฒฉ ์ฆ๋ช…์„ ํ™•์ธํ•œ ๋’ค Authorization Code๋ฅผ ๋ฐœ๊ธ‰ํ•œ๋‹ค. ์ด Authorization Code๋Š” ์ผํšŒ์šฉ์œผ๋กœ, Access Token์„ ๋ฐœ๊ธ‰๋ฐ›๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค. ์ดํ›„ ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„๋Š” Access Token์„ ๊ฒ€์ฆํ•˜๊ณ , ์‹ค์ œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

 

โ–ถ Spring Security์˜ OAuth ๋กœ๊ทธ์ธ

ํ•˜์ง€๋งŒ Spring Security๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์œ„ ๊ณผ์ • ์ค‘ 2~5๋ฒˆ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด ์ค€๋‹ค. ๋”ฐ๋ผ์„œ ์„œ๋ฒ„๋Š” ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค.

  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์—์„œ ์†Œ์…œ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•œ๋‹ค.
  2. Spring Security๊ฐ€ ์ด๋ฅผ ๊ฐ€๋กœ์ฑ„๊ณ , ๋‚ด๋ถ€์ ์œผ๋กœ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. (์œ„ ๊ณผ์ • ์ค‘ 2~5๋ฒˆ)
    • ํผ ๋กœ๊ทธ์ธ์˜ ๊ฒฝ์šฐ UsernamePasswordAuthenticationFilter๊ฐ€, ์†Œ์…œ ๋กœ๊ทธ์ธ์˜ ๊ฒฝ์šฐ OAuth2LoginAuthenticationFilter๊ฐ€ ์ธ์ฆ์„ ๋‹ด๋‹นํ•œ๋‹ค.
  3. UserDetailsService ๋˜๋Š” OAuth2UserService๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋กœ๋“œํ•˜๊ณ , ์ž๊ฒฉ ์ฆ๋ช…์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•œ๋‹ค.
    • ์šฐ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” OAuth2UserService์˜ ๊ตฌํ˜„์ฒด์ธ DefaultOAuth2UserService๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.
  4. ์ธ์ฆ์ด ์„ฑ๊ณตํ•˜๋ฉด ์‚ฌ์šฉ์ž ์ •๋ณด๋Š” SecurityContextHolder์— ์ €์žฅ๋˜๊ณ , AuthenticationSuccessHandler๋ฅผ ํ†ตํ•ด ์ดํ›„์˜ ์ฒ˜๋ฆฌ๊ฐ€ ์ด๋ฃจ์–ด์ง„๋‹ค.
  5. ์ธ์ฆ์ด ์‹คํŒจํ•˜๋ฉด AuthenticationFailureHandler๋ฅผ ํ†ตํ•ด ์‹คํŒจ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ(์˜ˆ: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€, ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ)๊ฐ€ ์ด๋ฃจ์–ด์ง„๋‹ค.
SecurityContextHolder: Spring Security์—์„œ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ํด๋ž˜์Šค์ด๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์—์„œ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋„๋ก, SecurityContext๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

 

โ–ถ ๊ตฌํ˜„ ์ฝ”๋“œ

  • DefaultOAuth2UserService๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ–ˆ๊ณ , ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ AuthenticationSuccessHandler์™€ AuthenticationFailureHandler๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.

๐Ÿ”ฝ OAuth ์ธ์ฆ ์š”์ฒญ URL

http://localhost:8080/v1/oauth2/authorization/${provider}

๐Ÿ”ฝ DefaultOAuth2UserService์˜ ๊ตฌํ˜„์ฒด์ธ CustomOAuth2UserService

/**
 * OAuth2 ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋”ฉ ๋ฐ ๊ถŒํ•œ ์„ค์ •์„ ์œ„ํ•œ ์ปค์Šคํ…€ ์„œ๋น„์Šค ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 */
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

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

        // ๊ถŒํ•œ ์ฒ˜๋ฆฌ
        Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        authorities.add(new SimpleGrantedAuthority(RolesType.ROLE_USER.name()));

        // OAuth2 ๋กœ๊ทธ์ธ ์‹œ ๊ธฐ๋ณธ ํ‚ค(PK)๊ฐ€ ๋˜๋Š” ๊ฐ’
        String userNameAttributeName = userRequest.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
        
        Map<String, Object> attributes = oAuth2User.getAttributes();
        String registrationId = userRequest.getClientRegistration().getRegistrationId(); // naver, kakao, apple ๊ตฌ๋ถ„
        OAuth2UserInfo oAuth2UserInfo = getOAuth2UserInfo(registrationId, attributes);

        return new CustomOAuth2User(
                authorities,
                attributes,
                userNameAttributeName,
                oAuth2UserInfo.getId(),
                oAuth2UserInfo.getProvider(),
                oAuth2UserInfo.getName(),
                oAuth2UserInfo.getImageUrl()
        );
    }

    private OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
        return switch (registrationId) {
            case "naver" -> new NaverUserInfo(attributes);
            case "kakao" -> new KakaoUserInfo(attributes);
            case "apple" -> new AppleUserInfo(attributes);
            default -> throw new IllegalArgumentException("Unknown registrationId: " + registrationId);
        };
    }
}

๐Ÿ”ฝ AuthenticationSuccessHandler์˜ ๊ตฌํ˜„์ฒด์ธ CustomOAuth2SuccessHandler

/**
 * CustomOAuth2UserService ์ธ์ฆ ์„ฑ๊ณต ์‹œ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•ธ๋“ค๋Ÿฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 */
@Component
@RequiredArgsConstructor
public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler {

    private final MemberService memberService;
    private final JwtService jwtService;

    /**
     * OAuth2 ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal();

        // ๊ถŒํ•œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        GrantedAuthority authority = customOAuth2User.getAuthorities().stream().findFirst()
                .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND_MEMBER_ROLES));
        RolesType role = RolesType.valueOf(authority.getAuthority());

        // CustomOAuth2User๋ฅผ MemberLoginDto๋กœ ๋ณ€ํ™˜
        MemberLoginDto memberLoginDto = MemberLoginDto.builder()
                .oauthId(customOAuth2User.getOauthId())
                .provider(customOAuth2User.getProvider())
                .name(customOAuth2User.getName())
                .profileImageUrl(customOAuth2User.getProfileImageUrl())
                .role(role)
                .build();

        // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๋ฐ JWT ํ† ํฐ ์ƒ์„ฑ
        JwtTokenDto jwtTokens = memberService.login(memberLoginDto);

        // JWT ํ† ํฐ์„ ์‘๋‹ต ํ—ค๋”์— ์ถ”๊ฐ€
        jwtService.setAccessTokenToHeader(response, jwtTokens.getAccessToken());
        jwtService.setRefreshTokenToHeader(response, jwtTokens.getRefreshToken());
    }
}

๐Ÿ”ฝ AuthenticationFailureHandler์˜ ๊ตฌํ˜„์ฒด์ธ CustomOAuth2FailureHandler

/**
 * CustomOAuth2UserService ์ธ์ฆ ์‹คํŒจ ์‹œ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•ธ๋“ค๋Ÿฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 */
@Component
public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");

        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("error", "Authentication failed");
        errorResponse.put("message", exception.getMessage());

        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}

๐Ÿ”ฝ Spring Security ์„ค์ • ํด๋ž˜์Šค์ธ SecurityConfig์˜ filterChain์— ๋“ค์–ด๊ฐˆ ๋ฉ”์„œ๋“œ

/**
 * Spring Security ๊ธฐ๋ณธ ์„ค์ • ๋ฉ”์„œ๋“œ
 */
private void defaultSecuritySetting(HttpSecurity http) throws Exception {
    http
            ...
            
            // OAuth2 ๋กœ๊ทธ์ธ ์„ค์ •
            .oauth2Login(oauth -> oauth
                    .successHandler(customOAuth2SuccessHandler)
                    .failureHandler(customOAuth2FailureHandler)
                    .userInfoEndpoint(userinfo -> userinfo
                            .userService(customOAuth2UserService))
            );
}

 

 

๐Ÿšจ ๋ฌธ์ œ ์ƒํ™ฉ

๊ธฐํš ๋‹จ๊ณ„์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‚ฌ์žฅ๋‹˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ์†Œ๋น„์ž ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ๋กœ ํ–ˆ๋‹ค๋Š” ๋‚ด์šฉ์„ ์ „๋‹ฌ๋ฐ›์•˜๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ํ˜„์žฌ ๊ตฌํ˜„๋œ ๋ฐฉ์‹์—์„œ๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ์œผ๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ๋งŒ ์ˆ˜ํ–‰๋˜์–ด ์„œ๋ฒ„์— ์ €์žฅ๋˜๊ณ , ํด๋ผ์ด์–ธํŠธ ์•ฑ์—์„œ ๋กœ๊ทธ์ธ ์ด์ „์— ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•  ๊ฒฝ๋กœ๊ฐ€ ์—†๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‚ฌ์žฅ๋‹˜ ๋ฒ„์ „ ๋˜๋Š” ์†Œ๋น„์ž ๋ฒ„์ „ ์ค‘ ์–ด๋–ค ๊ฒƒ์„ ์‚ฌ์šฉํ•˜๋Š”์ง€๋ฅผ ํŒ๋ณ„ํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ด๋‹ค.

 
 

โœ๏ธ ์›์ธ ๋ถ„์„

  • ์œ„์— ์„ค๋ช…ํ•œ OAuth ๋กœ๊ทธ์ธ ์ธ์ฆ ํ๋ฆ„์„ ์‚ดํŽด๋ณด๋ฉด, ๋‹ค์–‘ํ•œ ๋กœ์ง๋“ค์ด ์‹คํ–‰๋˜๋ฉฐ ๊ทธ ๊ณผ์ •์—์„œ ์—ฌ๋Ÿฌ ๋ฒˆ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋ฐ˜ํ™˜๋˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์ด ๊ณ„์† ๋ณ€๊ฒฝ๋œ๋‹ค. ์ด๋กœ ์ธํ•ด ํด๋ผ์ด์–ธํŠธ ์•ฑ์—์„œ ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ „์†กํ•˜๋”๋ผ๋„, ์ธ์ฆ ํ›„์—๋Š” ํ•ด๋‹น ๊ฐ’๋“ค์ด ์†Œ์‹ค๋˜์–ด ์ „๋‹ฌ๋˜์ง€ ์•Š๋Š”๋‹ค.
  • ๋˜ํ•œ, ๊ธฐ๋ณธ์ ์œผ๋กœ DefaultOAuth2UserService๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ ํ›„ ์‚ฌ์šฉ์ž ์ธ์ฆ๊ณผ ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋”ฉ๋งŒ์„ ๋‹ด๋‹นํ•˜๋ฉฐ, ๋กœ๊ทธ์ธ ์ด์ „์— ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌ๋œ ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์ „๋‹ฌํ•˜๋Š” ๋กœ์ง์€ ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค.

 

 

๐Ÿ”จ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

OAuth ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์‚ดํŽด๋ณด๋‹ˆ, state ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ†ตํ•ด์„œ OAuth ๋กœ๊ทธ์ธ ์ด์ „์˜ ๊ฐ’์„ ์„œ๋ฒ„๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค. ๋ณธ๋ž˜ ๋ชฉ์ ์€ CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง€์™€ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ ํ™•์ธ์„ ์œ„ํ•ด ์‚ฌ์šฉ๋˜์ง€๋งŒ, ํ˜„์žฌ ์šฐ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํŠน์ • ๊ถŒํ•œ ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ด ๋ดค๋‹ค.

๐Ÿ”ฝ ์ƒ๊ฐํ•œ ๋ฐฉ๋ฒ•

๐Ÿ”ฝ ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ถ€๋ถ„

  1. state ๊ฐ’์— ๋”ฐ๋ฅธ ๊ถŒํ•œ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.
  2. ๋˜ํ•œ, ์ค‘์š”ํ•œ ๋ถ€๋ถ„์€ OAuth2AuthorizationRequestResolver๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
    • OAuth2AuthorizationRequestResolver๋Š” OAuth2 ์ธ์ฆ ์š”์ฒญ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์ค€๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ OAuth2 ์ธ์ฆ ์š”์ฒญ์—๋Š” ํด๋ผ์ด์–ธํŠธ ID, ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URI, scope ๋“ฑ์ด ํฌํ•จ๋˜๋Š”๋ฐ, ์ถ”๊ฐ€์ ์ธ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•  ๊ฒฝ์šฐ ์ด๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค.

 

โ–ถ ๊ตฌํ˜„ ์ฝ”๋“œ

๐Ÿ”ฝ OAuth ์ธ์ฆ ์š”์ฒญ URL

http://localhost:8080/v1/oauth2/authorization/${provider}?state=${์—ญํ• }

๐Ÿ”ฝ DefaultOAuth2UserService์˜ ๊ตฌํ˜„์ฒด์ธ CustomOAuth2UserService

  • state ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”์ถœํ•˜์—ฌ ๊ถŒํ•œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€
  • state ๊ฐ’์— ๋”ฐ๋ผ ๊ถŒํ•œ์„ ๊ฒฐ์ •ํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
/**
 * OAuth2 ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋”ฉ ๋ฐ ๊ถŒํ•œ ์„ค์ •์„ ์œ„ํ•œ ์ปค์Šคํ…€ ์„œ๋น„์Šค ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 */
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

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

        // ๊ถŒํ•œ ์ฒ˜๋ฆฌ
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String state = request.getParameter("state");
        Set<SimpleGrantedAuthority> authorities = determineAuthorities(state);
        // ๊ฐ’ ์ถœ๋ ฅ
        System.out.println("state = " + state);


        // OAuth2 ๋กœ๊ทธ์ธ ์‹œ ๊ธฐ๋ณธ ํ‚ค(PK)๊ฐ€ ๋˜๋Š” ๊ฐ’
        String userNameAttributeName = userRequest.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
        
        Map<String, Object> attributes = oAuth2User.getAttributes();
        String registrationId = userRequest.getClientRegistration().getRegistrationId(); // naver, kakao, apple ๊ตฌ๋ถ„
        OAuth2UserInfo oAuth2UserInfo = getOAuth2UserInfo(registrationId, attributes);

        return new CustomOAuth2User(
                authorities,
                attributes,
                userNameAttributeName,
                oAuth2UserInfo.getId(),
                oAuth2UserInfo.getProvider(),
                oAuth2UserInfo.getName(),
                oAuth2UserInfo.getImageUrl()
        );
    }

    /**
     * state ๊ฐ’์— ๋”ฐ๋ผ ๊ถŒํ•œ์„ ๊ฒฐ์ •
     */
    private Set<SimpleGrantedAuthority> determineAuthorities(String state) {
        Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        if ("USER".equalsIgnoreCase(state)) {
            authorities.add(new SimpleGrantedAuthority(RolesType.ROLE_USER.name()));
        } else if ("STORE_OWNER".equalsIgnoreCase(state)) {
            authorities.add(new SimpleGrantedAuthority(RolesType.ROLE_STORE_OWNER.name()));
        } else {
            throw new MemberException(MemberErrorCode.BAD_REQUEST_STATE_PARAMETER);
        }
        return authorities;
    }

    private OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
        return switch (registrationId) {
            case "naver" -> new NaverUserInfo(attributes);
            case "kakao" -> new KakaoUserInfo(attributes);
            case "apple" -> new AppleUserInfo(attributes);
            default -> throw new IllegalArgumentException("Unknown registrationId: " + registrationId);
        };
    }
}

๐Ÿ”ฝ OAuth2AuthorizationRequestResolver์˜ ๊ตฌํ˜„์ฒด์ธ CustomAuthorizationRequestResolver

/**
 * OAuth2 ์ธ์ฆ ์š”์ฒญ์— ๋Œ€ํ•ด state ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋Š” Resolver ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 */
@Component
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;

    public CustomAuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
        this.defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(
                clientRegistrationRepository, "/oauth2/authorization");
    }

    /**
     * ๊ธฐ๋ณธ Resolver๋ฅผ ํ†ตํ•ด ์š”์ฒญ ์ฒ˜๋ฆฌ
     */
    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request);
        return customizeAuthorizationRequest(request, authorizationRequest);
    }

    /**
     * ๊ธฐ๋ณธ Resolver๋ฅผ ํ†ตํ•ด ์š”์ฒญ ์ฒ˜๋ฆฌ
     */
    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId);
        return customizeAuthorizationRequest(request, authorizationRequest);
    }

    /**
     * OAuth2 ์ธ์ฆ ์š”์ฒญ์— state ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฉ”์„œ๋“œ
     */
    private OAuth2AuthorizationRequest customizeAuthorizationRequest(HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest) {
        if (authorizationRequest == null) {
            return null;
        }

        String state = request.getParameter("state");

        if (!StringUtils.hasText(state)) {
            throw new MemberException(MemberErrorCode.BAD_REQUEST_STATE_PARAMETER);
        }

        return OAuth2AuthorizationRequest.from(authorizationRequest)
                .state(state)
                .build();
    }
}

๐Ÿ”ฝ Spring Security ์„ค์ • ํด๋ž˜์Šค์ธ SecurityConfig์˜ filterChain์— ๋“ค์–ด๊ฐˆ ๋ฉ”์„œ๋“œ์— Resolver ์ถ”๊ฐ€

/**
 * Spring Security ๊ธฐ๋ณธ ์„ค์ • ๋ฉ”์„œ๋“œ
 */
private void defaultSecuritySetting(HttpSecurity http) throws Exception {
    http
            ...
            
            // OAuth2 ๋กœ๊ทธ์ธ ์„ค์ •
            .oauth2Login(oauth -> oauth
                    .successHandler(customOAuth2SuccessHandler)
                    .failureHandler(customOAuth2FailureHandler)
                    .userInfoEndpoint(userinfo -> userinfo
                            .userService(customOAuth2UserService))
                    // resolver ์ถ”๊ฐ€
                    .authorizationEndpoint(endpoint -> endpoint
                            .authorizationRequestResolver(customAuthorizationRequestResolver()))
           );
}

 

 

๐Ÿ” ๊ฒฐ๊ณผ ๊ด€์ฐฐ

๐Ÿ”ฝ OAuth ์ธ์ฆ ์š”์ฒญ URL

http://localhost:8080/v1/oauth2/authorization/naver?state=USER

๐Ÿ”ฝ ๊ฒฐ๊ณผ

  • state ๊ฐ’์ด ์ •์ƒ์ ์œผ๋กœ ์ „๋‹ฌ๋œ๋‹ค!
  • FilterChain๋„ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค.
  • ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๋˜ํ•œ ์ •์ƒ์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋ฉฐ, ์ฟผ๋ฆฌ๋„ ์ž˜ ์‹คํ–‰๋œ๋‹ค.
 

 

๐Ÿ’ก ๊ณ ์ฐฐ

1. state ๊ฐ’์„ ํ†ตํ•ด ํŠน์ • ๊ฐ’์„ ์ „๋‹ฌํ•˜๋Š” ๊ฒŒ ์ ์ ˆํ•œ๊ฐ€?

  • state์˜ ๋ณธ๋ž˜ ๋ชฉ์ 
    • state์˜ ๋ณธ๋ž˜ ๋ชฉ์ ์€ CSRF ๋ฐฉ์ง€ ๋ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์ด๋‹ค. ์ด๋ฅผ ์‚ฌ์šฉ์ž ์—ญํ• ์ด๋‚˜ ํŠน์ • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ „๋‹ฌํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, CSRF ๋ฐฉ์ง€ ์™ธ์˜ ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๊ฒŒ ๋˜๋ฏ€๋กœ ๋ณธ๋ž˜ ๋ชฉ์ ์— ๋ถ€ํ•ฉํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ์‚ฌ์šฉ์€ ๋ณด์•ˆ์ ์ธ ์œ„ํ—˜๊ณผ ๊ด€๋ฆฌ์˜ ๋ณต์žก์„ฑ์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • state ๋ณ€์งˆ์˜ ์œ„ํ—˜์„ฑ
    • state ๊ฐ’์ด ์ธ์ฆ ๊ณผ์ •์—์„œ ์ค‘๊ฐ„์— ๋ณ€์งˆ๋˜๊ฑฐ๋‚˜ ์กฐ์ž‘๋  ๊ฒฝ์šฐ, ํšŒ์›๊ฐ€์ž…์ด๋‚˜ ๋กœ๊ทธ์ธ ์‹œ ์ •์ƒ์ ์ธ ๊ถŒํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค.

 

2. ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์‹ค React Native์—์„œ๋Š” ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ํ†ตํ•ด ์ •๋ณด๋ฅผ ๋ฐ›๋Š” ๊ฒƒ์ด ์–ด๋ ต๋‹ค.

  • ์›น์—์„œ์˜ OAuth ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
    • DefaultOAuth2UserService๋Š” ์ฃผ๋กœ ์›น์ด๋‚˜ Spring MVC ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ๋œ๋‹ค. ์ด๋Ÿฌํ•œ ํ™˜๊ฒฝ์—์„œ๋Š” ์„œ๋ฒ„๊ฐ€ ์ธ์ฆ ํ๋ฆ„์„ ์ œ์–ดํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋กœ๋”ฉํ•˜๋ฉฐ ์„ธ์…˜์„ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— DefaultOAuth2UserService๊ฐ€ ์ ํ•ฉํ•˜๋‹ค.
  • React Native์—์„œ์˜ OAuth ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
    • ๋ฐ˜๋ฉด, React Native์™€ ๊ฐ™์€ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•  ๋•Œ ๋ณดํ†ต ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ง์ ‘ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜์—ฌ ํ† ํฐ์„ ์ „๋‹ฌํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ์‹์ด ๋” ์ผ๋ฐ˜์ ์ด๋‹ค.
๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ•ด๋‹น ๋กœ์ง์„ ๊ฐˆ๊ณ , ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ง์ ‘ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ƒˆ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค...

 

 

๐Ÿ“ ์ฐธ๊ณ  ์ž๋ฃŒ

soeun2537