๊ฐœ๋ฐœ์ž HOON
๐Ÿ› HOON DEVLog
๊ฐœ๋ฐœ์ž HOON
์ „์ฒด ๋ฐฉ๋ฌธ์ž
์˜ค๋Š˜
์–ด์ œ
  • ๐Ÿ˜Ž ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ (137)
    • ๐Ÿ“ ์‹ ์ž… ์ธํ„ฐ๋ทฐ ์ค€๋น„ (7)
    • ๐Ÿฆ” ์ทจ์—…์ค€๋น„ ๊ธฐ๋ก (7)
    • โ˜• ์ž๋ฐ” : JAVA (5)
    • ๐Ÿ ์ฝ”๋”ฉํ…Œ์ŠคํŠธ ๋Œ€๋น„ : PS (80)
    • ๐ŸŒฑ ๋ฐฑ์—”๋“œ : Backend (13)
    • ๐Ÿงช ์ปดํ“จํ„ฐ๊ณผํ•™ : CS (11)
    • ๐Ÿ—‚ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค : DB (1)
    • ๐Ÿƒ‍โ™‚๏ธ DEVLOG (8)
    • โš™๏ธ Trouble Shooting (5)

๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

  • ํ™ˆ
  • GitHub
  • Resume

๊ณต์ง€์‚ฌํ•ญ

์ธ๊ธฐ ๊ธ€

์ตœ๊ทผ ๊ธ€

ํ‹ฐ์Šคํ† ๋ฆฌ

hELLO ยท Designed By ์ •์ƒ์šฐ.
๊ฐœ๋ฐœ์ž HOON

๐Ÿ› HOON DEVLog

[TS] Spring Security + Spring OAuth2 Client ์ ์šฉ ๊ณผ์ • ์ค‘ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…
โš™๏ธ Trouble Shooting

[TS] Spring Security + Spring OAuth2 Client ์ ์šฉ ๊ณผ์ • ์ค‘ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

2023. 9. 19. 09:51

๐Ÿค” ์˜ค๋ฅ˜ ๋ฐœ์ƒ

์ƒˆ๋กœ์šด ํ† ์ด ํ”„๋กœ์ ํŠธ, NEO๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ์œ„ํ•ด OAuth2 Client ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

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

 

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ฌธ์ œ๋ผ๋ฉด, ๋ถ„๋ช… ๋กœ๊ทธ๊ฐ€ ์ž‘์„ฑ๋˜์—ˆ์„ํ…๋ฐ, ํ•ด๋‹น ๋กœ๊ทธ๊ฐ€ ์—†๋Š” ๊ฒƒ๋„ ์ด์ƒํ•ด ๋ฌธ์ œ์˜ ์›์ธ์„ ํŠธ๋ž˜ํ‚นํ–ˆ์Šต๋‹ˆ๋‹ค.

 

OAuth2 ์ธ์ฆ ๊ณผ์ • ์ค‘, ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์–ป์–ด์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๋Š” ์ฝ”๋“œ๋Š”, OAuth2UserService<OAuth2UserRequest, OAuth2User>๋ฅผ implementํ•œ NEOOAuth2UserService์— ์žˆ์Šต๋‹ˆ๋‹ค.

@Slf4j
@Service
@RequiredArgsConstructor
public class NEOOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private final NEOUserRepository userRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        log.info("CustomOAuth2UserService.loadUser() ์‹คํ–‰ - OAuth2 ๋กœ๊ทธ์ธ ์š”์ฒญ ์ง„์ž…");

        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        NEOOAuth2ProviderType providerType = NEOOAuth2ProviderType.ofRegistrationId(registrationId);
        String userNameAttributeName = userRequest.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); // OAuth2 ๋กœ๊ทธ์ธ ์‹œ ํ‚ค(PK)๊ฐ€ ๋˜๋Š” ๊ฐ’
        Map<String, Object> attributes = oAuth2User.getAttributes(); // ์†Œ์…œ ๋กœ๊ทธ์ธ์—์„œ API๊ฐ€ ์ œ๊ณตํ•˜๋Š” userInfo์˜ Json ๊ฐ’(์œ ์ € ์ •๋ณด๋“ค)

        NEOOAuth2AttributesDTO extractAttributes = NEOOAuth2AttributesDTO.of(providerType, userNameAttributeName, attributes);

        NEOUserEntity createdUser = getUser(extractAttributes, providerType); // getUser() ๋ฉ”์†Œ๋“œ๋กœ User ๊ฐ์ฒด ์ƒ์„ฑ ํ›„ ๋ฐ˜ํ™˜

        return new NEOCustomOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(createdUser.getUserType().getKey())),
                attributes,
                extractAttributes.getNameAttributeKey(),
                createdUser.getEmail(),
                createdUser.getUserType(),
                createdUser.getUserName(),
                createdUser.getPhoneNumber(),
                createdUser.getGender()
        );
    }

    private NEOUserEntity getUser(NEOOAuth2AttributesDTO attributes, NEOOAuth2ProviderType providerType) {
        NEOUserEntity foundUser;

        try {
            foundUser = userRepository.findByProviderTypeAndSocialID(providerType,
                            attributes.getOauth2UserInfo().getId())
                    .orElseGet(() -> saveUser(attributes, providerType));
        } catch (Exception e){
            throw new NEOUnexpectedException("OAuth ์ง„ํ–‰ ๊ณผ์ • ์ค‘, DB ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ๊ด€๋ฆฐ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.");
        }

        return foundUser;
    }

    private NEOUserEntity saveUser(NEOOAuth2AttributesDTO attributes, NEOOAuth2ProviderType socialType) {
        NEOUserEntity createdUser = attributes.toEntity(socialType, attributes.getOauth2UserInfo());
        return userRepository.save(createdUser);
    }

}

 

ํ•ด๋‹น ํด๋ž˜์Šค์˜ loadUser()์—์„œ, getUser()์™€ saveUser()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

DB์— ์ €์žฅ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์€, getUser() ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์ด๊ณ , ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ loadUser() ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•˜์Œ์„ ๋œปํ•ฉ๋‹ˆ๋‹ค. ๋””๋ฒ„๊ฑฐ ํฌ์ธํŠธ๋ฅผ ํ†ตํ•ด์„œ๋„ ํ™•์ธํ•œ ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค.

 

โ˜•๏ธ Security Config Code

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class NEOSecurityConfig {

    private final NEOOAuth2SuccessHandler oAuth2LoginSuccessHandler;
    private final NEOOAuth2FailureHandler oAuth2LoginFailureHandler;
    private final NEOOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // ํผ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ X
        http.formLogin(AbstractHttpConfigurer::disable)
                // httpBasic ์ธ์ฆ ๋ฐฉ์‹ ์‚ฌ์šฉ X
                .httpBasic(AbstractHttpConfigurer::disable)
                // ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ „์ œ ํ•˜์— csrf X (NEO๋Š” ์•ฑ)
                .csrf(AbstractHttpConfigurer::disable)
                // ์„ธ์…˜ ๋ฏธ์‚ฌ์šฉ, JWT Token ๋ฐฉ์‹ ์‚ฌ์šฉ
                .sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // OAuth2 ์†Œ์…œ ๋กœ๊ทธ์ธ
                .oauth2Login((oauth2Login) -> oauth2Login
                        .successHandler(oAuth2LoginSuccessHandler)
                        .failureHandler(oAuth2LoginFailureHandler)
                        .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2UserService)))
                // URL๋ณ„ ๊ถŒํ•œ ๊ด€๋ฆฌ
                .authorizeHttpRequests(requests -> requests
                        // ๋กœ๊ทธ์ธ ๊ด€๋ จ URL ๋ชจ๋‘ ํ—ˆ๊ฐ€
                        .requestMatchers("/login", "/oauth2/authorization/**", "/api/v1/oauth2/**").permitAll()
                        // API ๊ฐœ๋ฐœ ๋ฌธ์„œ URL ๋ชจ๋‘ ํ—ˆ๊ฐ€
                        .requestMatchers("/docs/**").permitAll()
                        // ๋‚˜๋จธ์ง€ URL
                        .anyRequest().authenticated());

        return http.build();
    }

}

 

 

โ˜•๏ธ application-oauth.yml

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: {google-client-id}
            client-secret: {google-client-secret}
            redirect-uri: http://localhost:8080/api/v1/oauth2/google
            scope: profile, email

          naver:
            client-id: {naver-client-id}
            client-secret: {naver-client-secret}
            redirect-uri: http://localhost:8080/api/v1/oauth2/naver
            authorization-grant-type: authorization_code
            scope: name, email, gender, mobile
            client-name: Naver

          kakao:
            client-id: {kakao-client-id}
            client-secret: {kakao-client-secret}
            redirect-uri: http://localhost:8080/api/v1/oauth2/kakao
            client-authentication-method: POST
            authorization-grant-type: authorization_code
            scope: account_email, gender
            client-name: Kakao

        provider:
          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

          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

 

์œ„์˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด NEOSecurityConfig์˜ oauth2Login.userInfoEndpoint()์— ์šฐ๋ฆฌ๊ฐ€ ๊ฐœ๋ฐœํ•œ OAuth2UserService๋ฅผ ๋“ฑ๋กํ–ˆ๋Š”๋ฐ ์™œ loadUser()๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•˜๋˜ ๊ฒƒ์ผ๊นŒ์š”?

ํ•ด๋‹น ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ณผ์ •์—์„œ OAuth2 Client์— ๋Œ€ํ•ด ๊นŠ๊ฒŒ ํŒŒ๊ณ ๋“ค์–ด ๋” ์ž˜ ์ดํ•ดํ•˜๋Š” ๊ณผ์ •์ด ๋˜์—ˆ๊ธฐ์— ํ•ด๋‹น ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๊ธฐ๋กœ ๋งˆ์Œ๋จน์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

๐Ÿค” 1. OAuth2UserService๋Š” ์ž˜ ๋“ฑ๋ก๋˜์—ˆ๋Š”๊ฐ€?

 

oauth2Login.userInfoEndpoint ๋ฉ”์†Œ๋“œ๋Š” OAuth2์˜ Authorization Server์˜ ์‚ฌ์šฉ์ž ์ •๋ณด ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ทœ์ •ํ•˜๋Š” ๋ฉ”์†Œ๋“œ์ž…๋‹ˆ๋‹ค.

 

 

๋”ฐ๋ผ์„œ UserInfoEndpointConfig.userSerivce()๋ฅผ ํ†ตํ•ด์„œ Resource Owner์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์–ป์–ด์˜ฌ ์ˆ˜ ์žˆ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด ์—”๋“œํฌ์ธํŠธ๋ฅผ ์„ค์ •ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ OAuth2UserService๋ฅผ ์š”๊ตฌํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ํ•ด๋‹น ํƒ€์ž…์€ ์šฐ๋ฆฌ๊ฐ€ ์ฝ”๋“œ์—์„œ ๊ตฌํ˜„ํ•œ NEOOAuth2UserService์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

 

OAuth2UserService๋ฅผ ์ž ์‹œ ์‚ดํŽด๋ณด๋ฉด, Resource Owner์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋Š” ์ฑ…์ž„์ด ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋กœ์จ,

Authorization Server๋กœ๋ถ€ํ„ฐ ์–ป์–ด์˜จ Access Token์„ ํ†ตํ•ด OAuth2User์˜ ํ˜•์‹์„ ๊ฐ€์ง„ AuthenticatedPrincipal์„ ์–ป์–ด์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰ Resource Server๋กœ๋ถ€ํ„ฐ Access Token์„ ํ†ตํ•ด Resource Owner์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.

 

์šฐ์„ , OAuth2UserService๊ฐ€ ์ž˜ ๋“ฑ๋ก๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” OAuth2LoginConfigurer์˜ init ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ดํŽด๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

OAuth2LoginConfigurer์˜ init() ๋ฉ”์†Œ๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ผ์„ ํ•ฉ๋‹ˆ๋‹ค.

@Override
    public void init(B http) throws Exception {
        OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter(
                OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
                OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()), this.loginProcessingUrl);
        authenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
        this.setAuthenticationFilter(authenticationFilter);
        super.loginProcessingUrl(this.loginProcessingUrl);
        if (this.loginPage != null) {
            // Set custom login page
            super.loginPage(this.loginPage);
            super.init(http);
        }
        else {
            Map<String, String> loginUrlToClientName = this.getLoginLinks();
            if (loginUrlToClientName.size() == 1) {
                // Setup auto-redirect to provider login page
                // when only 1 client is configured
                this.updateAuthenticationDefaults();
                this.updateAccessDefaults(http);
                String providerLoginPage = loginUrlToClientName.keySet().iterator().next();
                this.registerAuthenticationEntryPoint(http, this.getLoginEntryPoint(http, providerLoginPage));
            }
            else {
                super.init(http);
            }
        }
        OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = this.tokenEndpointConfig.accessTokenResponseClient;
        if (accessTokenResponseClient == null) {
            accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
        }
        OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = getOAuth2UserService();
        OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider = new OAuth2LoginAuthenticationProvider(
                accessTokenResponseClient, oauth2UserService);
        GrantedAuthoritiesMapper userAuthoritiesMapper = this.getGrantedAuthoritiesMapper();
        if (userAuthoritiesMapper != null) {
            oauth2LoginAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);
        }
        http.authenticationProvider(this.postProcess(oauth2LoginAuthenticationProvider));
        boolean oidcAuthenticationProviderEnabled = ClassUtils
                .isPresent("org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader());
        if (oidcAuthenticationProviderEnabled) {
            OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService = getOidcUserService();
            OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider = new OidcAuthorizationCodeAuthenticationProvider(
                    accessTokenResponseClient, oidcUserService);
            JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = this.getJwtDecoderFactoryBean();
            if (jwtDecoderFactory != null) {
                oidcAuthorizationCodeAuthenticationProvider.setJwtDecoderFactory(jwtDecoderFactory);
            }
            if (userAuthoritiesMapper != null) {
                oidcAuthorizationCodeAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);
            }
            http.authenticationProvider(this.postProcess(oidcAuthorizationCodeAuthenticationProvider));
        }
        else {
            http.authenticationProvider(new OidcAuthenticationRequestChecker());
        }
        this.initDefaultLoginFilter(http);
    }

 

- OAuth2LoginAuthenticationFilter๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
- loginPage๋ฅผ ์„ค์ •ํ•˜๋ฉฐ, ์„ค์ •๋œ ๊ฐ’์ด ์—†๋‹ค๋ฉด ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ loginPage๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
- provider์— ๋งž๋Š” ๋งํฌ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, [kakao, naver, google]์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๊ฐ๊ฐ์˜ authorization์„ ๊ฐ€๋Šฅ์ผ€ ํ•˜๋Š” ๋งํฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
- Authorization grant code๋ฅผ Access Token์œผ๋กœ ๊ตํ™˜ํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.(OAuth2AccessTokenResponseClient๋ฅผ OAuth2LoginAuthenticationProvider์— ๋“ฑ๋ก)
- AccessToken์„ ํ†ตํ•ด Resource ์„œ๋ฒ„์— Resource Owner์˜ ์ •๋ณด๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋Š” OAuth2UserService๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
- ๊ธฐ๋ณธ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ฐ ๊ฐ Provider์˜ ๋กœ๊ทธ์ธ ํผ์œผ๋กœ ์ ‘์†ํ•˜๋Š” URL ์ƒ์„ฑ ๋“ฑ.

 

 

init ๋ฉ”์†Œ๋“œ ๋‚ด์˜ getOAuth2UserService() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด, ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  NEOOAuth2UserService ๊ฐ์ฒด๊ฐ€ ์ž˜ ๋“ค์–ด๊ฐ„ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

๊ฐ์ฒด๊ฐ€ ์ž˜ ๋“ฑ๋ก๋˜์—ˆ์Œ์—๋„, loadUser()๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•„ ๋”์šฑ ๊ถ๊ธˆํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

 

๐Ÿค” 2. ๋ฌธ์ œ ์ถ”์ ํ•˜๊ธฐ

๊ทธ๋ž˜์„œ ๊ณง๋ฐ”๋กœ loadUser()๋Š” ๋ˆ„๊ฐ€ ํ˜ธ์ถœํ•˜๋Š”์ง€, ํŠธ๋ž˜ํ‚น ํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

loadUser() ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์€ ์ด 6๊ฐœ์ด์ง€๋งŒ OpenID๋ฅผ ๋‹ค๋ฃจ๋Š” Oidc๋‚˜, OAuth2UserService๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์œ„์ž„ํ•  ์ˆ˜ ์žˆ๋Š” DelegatingOAuth2UserService, ์šฐ๋ฆฌ๊ฐ€ ๊ตฌํ˜„ํ•œ NEOOAuth2UserService๋ฅผ ์ œ์™ธํ•˜๋ฉด

OAuth2LoginAuthenticationProvider๊ฐ€ ์‹ค์งˆ์ ์œผ๋กœ loadUser()๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

ํ•ด๋‹น ๋ถ€๋ถ„์„ ๋”ฐ๋ผ๊ฐ€๋ฉด, OAuth2LoginAuthenticationProvider์˜ authenticate ๋ฉ”์†Œ๋“œ์— ๋„์ฐฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น authenticate ๋ฉ”์†Œ๋“œ๋Š” ProviderManager์˜ authenticate์—์„œ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋Š” ์ „๋‹ฌ๋ฐ›์€ Authentication ๊ฐ์ฒด๋กœ ํ•˜์—ฌ๊ธˆ ์ธ์ฆ์„ ์‹œ๋„ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋กœ, AuthenticationProvider์— ์˜ํ•ด ์‹œ๋„๋ฉ๋‹ˆ๋‹ค.

 

์ฆ‰, ์ •์ƒ์ ์œผ๋กœ OAuth2LoginAuthenticationProvider๊ฐ€ ์ž˜ ๋“ฑ๋ก๋˜์—ˆ๋‹ค๋ฉด, ProviderManager์˜ authenticate ๋ฉ”์†Œ๋“œ์—์„œ, getProviders()๋ฅผ ํ†ตํ•ด for๋ฌธ์„ ๋Œ๋ฉฐ ํ•ด๋‹น ํ”„๋กœ๋ฐ”์ด๋”์— ๋Œ€ํ•ด authenticate ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด์„œ ์ธ์ฆ ๊ณผ์ •์„ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฌ๋‚˜, ์ •์ƒ์ ์œผ๋กœ ๋“ฑ๋ก์ด ๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋Š” ํ˜ธ์ถœ๋˜์ง€ ์•Š์•˜๋˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ •์ƒ์ ์œผ๋กœ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์›์ธ์„ ์ข€ ๋” ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ•ด๋‹น authenticate ๋ฉ”์†Œ๋“œ๋ฅผ ๋ˆ„๊ฐ€ ํ˜ธ์ถœํ–ˆ๋Š”์ง€ ์‚ดํŽด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

ํ•œ ๋ฒˆ ๋” ํƒ€๊ณ  ์˜ฌ๋ผ๊ฐ€๋ฉด, OAuth2LoginAuthenticationFilter์— ๋„์ฐฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

โ˜•๏ธ OAuth2LoginAuthenticationFilter

An implementation of an AbstractAuthenticationProcessingFilter for OAuth 2.0 Login. This authentication Filter handles the processing of an OAuth 2.0 Authorization Response for the authorization code grant flow and delegates an OAuth2LoginAuthenticationToken to the AuthenticationManager to log in the End-User. The OAuth 2.0 Authorization Response is processed as follows: Assuming the End-User (Resource Owner) has granted access to the Client, the Authorization Server will append the code and state parameters to the redirect_uri (provided in the Authorization Request) and redirect the End-User's user-agent back to this Filter (the Client). This Filter will then create an OAuth2LoginAuthenticationToken with the code received and delegate it to the AuthenticationManager to authenticate. Upon a successful authentication, an OAuth2AuthenticationToken is created (representing the End-User Principal) and associated to the Authorized Client using the OAuth2AuthorizedClientRepository. Finally, the OAuth2AuthenticationToken is returned and ultimately stored in the SecurityContextRepository to complete the authentication processing.
[์ธ์šฉ : Javadoc]

โ˜•๏ธ OAuth2LoginAuthenticationFilter ํ•œ๊ธ€ ์š”์•ฝ
- AbstractAuthenticationProcessingFilter์˜ OAuth2.0 ๋กœ๊ทธ์ธ์„ ์œ„ํ•œ ๊ตฌํ˜„์ฒด
- OAuth2์˜ "Authorization code grant" ๋ฐฉ์‹์—์„œ OAuth 2.0 Authorization Response์„ ๋‹ค๋ฃจ๊ณ , "OAuth2LoginAuthenticationToken"์„ AuthenticationManager์—๊ฒŒ ์œ„์ž„ํ•ด์„œ ์ตœ์ข… ์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์ธ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ํ•„ํ„ฐ.

- OAuth 2.0 Authorization Response์˜ ํ”„๋กœ์„ธ์Šค
1. Resource Owner๊ฐ€ Client๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฐ€์ • ํ•˜์—, ์ธ์ฆ ์„œ๋ฒ„๋Š” redirect_uri์— code์™€ state๋ฅผ ์ถ”๊ฐ€
2. ์ตœ์ข… ์‚ฌ์šฉ์ž์˜ ์—์ด์ „ํŠธ๋ฅผ ๋‹ค์‹œ ์ด ํ•„ํ„ฐ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜
3. OAuth2LoginAuthenticationToken์„ ์ƒ์„ฑ
4. AuthenticationManager์—๊ฒŒ ์ธ์ฆ ์œ„์ž„
5. ์„ฑ๊ณต์ ์œผ๋กœ ์ธ์ฆ์ด ์™„๋ฃŒ ๋˜๋ฉด, OAuth2LoginAuthenticationToken์˜ ํ•„๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ OAuth2AuthenticationToken์ด ์ƒ์„ฑ.

 

 

OAuth2LoginAuthenticationFilter์—์„œ ์ค‘์ ์ ์œผ๋กœ ๋ณด์•„์•ผ ํ•  ๊ฒƒ์€ ์ด ๋‘ ๊ฐ€์ง€ ์ž…๋‹ˆ๋‹ค.

1. OAuth2LoginAuthenticationFilter๋Š” OAuth2LoginConfigurer์˜ init ๋ฉ”์†Œ๋“œ์—์„œ ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด ์ƒ์„ฑ๋œ ์ ์ด ์žˆ๋‹ค.
2. attemptAuthentication ๋ฉ”์†Œ๋“œ

 

์ฒซ ๋ฒˆ์งธ ์ค‘์ ์œผ๋กœ OAuth2LoginAuthenticationFilter๋Š” OAuth2LoginConfigurer์˜ init ๋ฉ”์†Œ๋“œ์—์„œ ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด ์ƒ์„ฑ๋œ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ€์žฅ ์ฒ˜์Œ ๋‚˜์˜ค๊ฒŒ ๋˜๋Š” ์ฝ”๋“œ์ฃ .

OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter(
                OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
                OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()), this.loginProcessingUrl);

 

์ƒ์„ฑ์ž๋ฅผ ์‚ดํŽด๋ณด๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์€ ์ƒ์„ฑ์ž๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

OAuth2LoginConfigurer.loginProcessUrl์ด OAuth2LoginAuthenticationFilter์„ ์ƒ์„ฑํ•  ๋•Œ ์ „๋‹ฌ๋˜๊ณ , ๊ทธ ๊ฐ’์€ ๊ทธ๋Œ€๋กœ ๋ถ€๋ชจ์—๊ฒŒ filterProcessesUrl์œผ๋กœ ์ „๋‹ฌ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

OAuth2LoginAuthenticationFilter์˜ ๋ถ€๋ชจ๋Š”, AbstractAuthenticationProcessingFilter์ž…๋‹ˆ๋‹ค.

 

์œ„์™€ ๊ฐ™์€ ์ฝ”๋“œ๋กœ ์ธํ•ด AbstractAuthenticationProcessingFilter์˜ requiresAuthenticationRequestMatcher๋Š” OAuth2LoginConfigurer.loginProcessUrl์˜ ์˜ํ–ฅ์„ ๊ณ ์Šค๋ž€ํžˆ ๋ฐ›๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด ์ ์„ ๊ธฐ์–ตํ•œ ์ƒํƒœ์—์„œ, ์•„๋ž˜์˜ ๊ธ€์„ ๋ณด๋ฉด ์‹ค๋งˆ๋ฆฌ๊ฐ€ ํ’€๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

๋‘ ๋ฒˆ์งธ ์ค‘์ ์œผ๋กœ OAuth2LoginAuthenticationFilter์˜ attemptAuthentication ๋ฉ”์†Œ๋“œ๋Š”, Resource Owner๊ฐ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ง„ํ–‰ํ•˜๊ณ  ๋‚œ ์ดํ›„์— ๋™์ž‘ํ•˜๋ฉฐ Authorization Server๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋œ code๋ฅผ HttpServletRequest๋กœ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜ค๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ์ด ๋ฉ”์†Œ๋“œ๋Š”, AbstractAuthenticationProcessingFilter์˜ doFilter ๋ฉ”์†Œ๋“œ์—์„œ ์‹คํ–‰๋˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ์ž˜ ๋ณด์•„์•ผ ํ•  ๋ถ€๋ถ„์€, try๋ฌธ์„ ๋“ค์–ด๊ฐ€๊ธฐ ์ „์˜ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค.

requiresAuthentication์˜ boolean ๋ฆฌํ„ด๊ฐ’์— ์˜ํ•ด ์•„๋ž˜์˜ attemptAuthentication์„ ์‹คํ–‰ํ•  ๊ฒƒ์ธ์ง€, ์•„๋‹Œ์ง€๊ฐ€ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.

 

ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋กœ ๊ฐ€์„œ ์–ด๋–ค ๋กœ์ง์ธ์ง€ ํ™•์ธํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋Š” ์ด ํ•„ํ„ฐ๊ฐ€ ๋กœ๊ทธ์ธ ์š”์ฒญ ์‹คํ–‰์„ ์‹œ๋„ํ•ด์•ผ ํ•˜๋Š”์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์—ญํ• ์„ ํ•˜๋Š” ๋ฉ”์†Œ๋“œ์ž…๋‹ˆ๋‹ค.

์ฆ‰, this.requiresAuthenticationRequestMatcher.matches()์˜ ๊ฐ’์ด false๋ผ๋ฉด, ํ•ด๋‹น ํ•„ํ„ฐ๊ฐ€ ๋กœ๊ทธ์ธ ํ”„๋กœ์„ธ์Šค ์‹คํ–‰์„ ์‹œ๋„ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

this.requiresAuthenticationRequestMatcher๋Š” ์œ„์—์„œ ๋ณด์•˜๋“ฏ์ด, OAuth2LoginConfigurer.loginProcessUrl์˜ ์˜ํ–ฅ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.

loginProcessUrl์€ OAuth2LoginAuthenticationFilter์˜ DEFAULT\_FILTER\_PROCESSES\_URI์ด๋ฉฐ, ํ•ด๋‹น ๊ฐ’์€ "/login/oauth2/code/*"๋กœ ๊ณ ์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋‹ค์‹œ ์œ„์˜ application-oauth.yml์„ ์‚ดํŽด๋ณด๋ฉด,

http://localhost:8080/api/v1/oauth2/*

์™€ ๊ฐ™์ด ์„ค์ •๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด๋‹น url๋กœ๋Š” ๋กœ๊ทธ์ธ ํ”„๋กœ์„ธ์Šค๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š์•˜๋˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

"์–ด? ๋‹จ์ˆœํžˆ application.yml์— redirect-url์„ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค๊ณ  ํ•ด์„œ ๋ฐ”๋กœ ๋™์ž‘ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๊ตฌ๋‚˜?"

 

๊ทธ๋ ‡๋‹ค๋ฉด ์œ„์˜ ๋‚ด์šฉ์„ ํ† ๋Œ€๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€, OAuth2LoginConfigurer์˜ loginProcessUrl์„ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋“  ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์˜์™ธ๋กœ ๊ฐ„๋‹จํžˆ Security Config ์ฝ”๋“œ์—์„œ ๋‹จ ํ•œ ์ค„๋งŒ ์ถ”๊ฐ€ ํ•˜๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ์˜€๋„ค์š”.

 

โ˜•๏ธ ๋ณ€๊ฒฝ๋œ Security Config Code

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class NEOSecurityConfig {

    private final NEOOAuth2SuccessHandler oAuth2LoginSuccessHandler;
    private final NEOOAuth2FailureHandler oAuth2LoginFailureHandler;
    private final NEOOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // ํผ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ X
        http.formLogin(AbstractHttpConfigurer::disable)
                // httpBasic ์ธ์ฆ ๋ฐฉ์‹ ์‚ฌ์šฉ X
                .httpBasic(AbstractHttpConfigurer::disable)
                // ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ „์ œ ํ•˜์— csrf X (NEO๋Š” ์•ฑ)
                .csrf(AbstractHttpConfigurer::disable)
                // ์„ธ์…˜ ๋ฏธ์‚ฌ์šฉ, JWT Token ๋ฐฉ์‹ ์‚ฌ์šฉ
                .sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // OAuth2 ์†Œ์…œ ๋กœ๊ทธ์ธ
                .oauth2Login((oauth2Login) -> oauth2Login
                        .loginProcessingUrl("/api/v1/oauth2/*") // ์ถ”๊ฐ€๋จ!
                        .successHandler(oAuth2LoginSuccessHandler)
                        .failureHandler(oAuth2LoginFailureHandler)
                        .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2UserService)))
                // URL๋ณ„ ๊ถŒํ•œ ๊ด€๋ฆฌ
                .authorizeHttpRequests(requests -> requests
                        // ๋กœ๊ทธ์ธ ๊ด€๋ จ URL ๋ชจ๋‘ ํ—ˆ๊ฐ€
                        .requestMatchers("/login", "/oauth2/authorization/**", "/api/v1/oauth2/**").permitAll()
                        // API ๊ฐœ๋ฐœ ๋ฌธ์„œ URL ๋ชจ๋‘ ํ—ˆ๊ฐ€
                        .requestMatchers("/docs/**").permitAll()
                        // ๋‚˜๋จธ์ง€ URL
                        .anyRequest().authenticated());

        return http.build();
    }

}

 

๐Ÿค” 3. ์ด์ •๋ฆฌ ๋ฐ ํ›„๊ธฐ

 

๋ฌธ์ œ๋Š” loginProcessUrl์„ ์„ค์ •ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ˆœํžˆ application.yml์— redirect-url์„ ์„ค์ •ํ•˜๋ฉด ๋‚ด๋ถ€์ ์œผ๋กœ oauth2 client๊ฐ€ ๋ชจ๋“  ๊ฒƒ์„ ์ฒ˜๋ฆฌํ•ด ์ค„ ๊ฒƒ์ด๋ผ๋Š” ์•ˆ์ผํ•œ ์ƒ๊ฐ์ด ํ•ด๋‹น ๋ฌธ์ œ๋ฅผ ๋ฐœ์ƒ์‹œ์ผฐ๋„ค์š”.

 

๊ทธ๋ž˜๋„ ์ด๋Ÿฌํ•œ ์‹ค์ˆ˜ ๋•๋ถ„์— oauth2 client๊ฐ€ ์–ด๋–ป๊ฒŒ authroization code grant ๋ฐฉ์‹์„ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€, ๋‚ด๋ถ€๋ฅผ ๋“ค์—ฌ๋‹ค ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๊ธฐํšŒ๊ฐ€ ๋œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์•ž์œผ๋กœ๋Š” ๋‹จ์ˆœํ•œ ์ง๊ด€๋ณด๋‹ค๋Š” ์ข€ ๋” ๊นŠ์€ ์ดํ•ด๋ฅผ ํ•œ ์ƒํƒœ๋กœ ๊ฐœ๋ฐœํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์—ฌ์•ผ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

์ €์ž‘์žํ‘œ์‹œ ๋น„์˜๋ฆฌ ๋™์ผ์กฐ๊ฑด

'โš™๏ธ Trouble Shooting' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Trouble Shooting] Lombok ์‚ฌ์šฉ์‹œ, ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋˜์ง€ ์•Š๋Š” ๊ฐ„๋‹จํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ  (0) 2023.08.14
[Trouble Shooting] Swagger-ui๋ฅผ ์ ‘์†ํ–ˆ์„ ๋•Œ 404 error๊ฐ€ ๋‚˜๋Š” ๊ฒฝ์šฐ  (0) 2023.08.14
[TS] JPA error - Specified key was too long; max key length is 1000 bytes  (0) 2023.03.23
[TS] ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ฐ Spring WebFlux ์ ์šฉ ๊ณผ์ • ์ค‘ ์ƒ๊ธด ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…  (0) 2023.03.12
    'โš™๏ธ Trouble Shooting' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
    • [Trouble Shooting] Lombok ์‚ฌ์šฉ์‹œ, ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋˜์ง€ ์•Š๋Š” ๊ฐ„๋‹จํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ
    • [Trouble Shooting] Swagger-ui๋ฅผ ์ ‘์†ํ–ˆ์„ ๋•Œ 404 error๊ฐ€ ๋‚˜๋Š” ๊ฒฝ์šฐ
    • [TS] JPA error - Specified key was too long; max key length is 1000 bytes
    • [TS] ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ฐ Spring WebFlux ์ ์šฉ ๊ณผ์ • ์ค‘ ์ƒ๊ธด ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…
    ๊ฐœ๋ฐœ์ž HOON
    ๊ฐœ๋ฐœ์ž HOON
    ์ข‹์€ ๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด๊ฐ€ ๋˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ก์„ ๋ชจ์•˜์Šต๋‹ˆ๋‹ค. # ์ฃผ๋‹ˆ์–ด # ๋ฐฑ์—”๋“œ # ๊ฐœ๋ฐœ์ž

    ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”