1. @AuthenticationPrincipal 어노테이션

이 Spring Security 어노테이션은 SecurityContextHolder의 현재 Authentication 객체에서 "principal" 객체를 주입합니다.

2. JwtFilter에서 TokenInfo를 Principal로 설정

JwtFilter 클래스에서 JWT 토큰이 검증될 때:

// 토큰을 파싱하여 TokenInfo 객체 가져오기
TokenInfo tokenInfo = jwtUtil.parseToken(accessToken);

// TokenInfo의 역할에서 권한 생성
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
tokenInfo.getRoleList()
        .forEach(role -> {
            authorities.add(new SimpleGrantedAuthority(role));
        });

// TokenInfo를 principal로 하는 Authentication 생성
Authentication authentication = new UsernamePasswordAuthenticationToken(tokenInfo, accessToken, authorities);

// SecurityContextHolder에 Authentication 설정
SecurityContextHolder.getContext().setAuthentication(authentication);

JwtFilter에서 Authentication의 principal을 TokenInfo 객체로 설정되는 객체를 잡아온다.

중요한 부분은 UsernamePasswordAuthenticationToken을 생성할 때 첫 번째 파라미터로 tokenInfo를 전달하는 것입니다. 이 파라미터가 바로 principal입니다.

3. TokenInfoProvider로 확인

TokenInfoProvider 클래스는 이를 더 확인해줍니다. Authentication에서 principal을 가져와 TokenInfo로 캐스팅합니다:

public TokenInfo getTokenInfo() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if(authentication == null || !authentication.isAuthenticated() || 
       !(authentication.getPrincipal() instanceof TokenInfo tokenInfo)) {
        return null;
    }

    return tokenInfo;
}

결론

@GetMapping("/test")
public ResponseEntity<?> getTaxEmployee(@AuthenticaionPrincipal TokenInfo tokenInfo){
    // TokenInfo 객체 사용
    return ResponseEntity.ok().body("success");
}

1. 

현재 프로젝트에서 사용자가 특정 어플리케이션에 접근을 하면  jwt 토큰을 검증과정을 수행한다,

 

SecurityFilterChain

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .csrf(csrf -> csrf.disable())  // CSRF 비활성화
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) // CORS 필터 추가
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)// JWT 필터 추가
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/oauth2/**", "/login/**").permitAll() // ✅ 추가
                    .requestMatchers("/api/auth/**").permitAll()
                    .requestMatchers("/api/user/**").permitAll()
                    .anyRequest().authenticated()
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .exceptionHandling(exception -> exception.authenticationEntryPoint(customAuthenticationEntryPoint));


    return http.build();

 

사용자가 어플리케이션에 접근을 하게되면 addFilterBefore로 등록된 jwtAuthentictionFilter가 동작을 하는 구조이다.

 

JwtAuthenticationFilter

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);


        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUsername(token);
            String role = jwtTokenProvider.getRole(token);

            // GrantedAuthority를 사용하여 권한 설정
            List<GrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(role));

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return null;
        }
        return Arrays.stream(cookies).filter(s -> s.getName().equals("jwt"))
                .map(Cookie::getValue)
                .findFirst()
                .orElse(null);
    }
}

 

jwtAuthentictionFilter에서 doFilterinternal이 동작을 하게되는데 여기서 검증에 통과한 사용자의 경우엔 SecurityContextHolder에 UsernamePasswordAuthenticationmToken을 담아주는 역할을 수행한다. 그다음 authorizedHttpRequest에 설정해준 내용대로 requestMatchers에 등록된 경로의 경우 필터 통과 여부와 상관없이 항상 접근이 가능하도록 허용을 해준다.

 

 

2. 모두 접근이 가능한데 필터는 왜 동작하나

 

  requestMatchers()에 등록된 경로는 모두 로그인과 관련한 경로이다. 토큰이 없는 사용자라도 접근하여서 로그인이 가능하게 열어둔 것이다. 근데 모두에게 열린 경로인데 필터가 동작해서 우선 토큰을 검증하는 상황이 그닥 효율적이지 않다고 생각을 했고,  특정 경로로 접근시엔 해당 필터가 동작하지 않고 바로  authorizedhttpRequests()동작하게 하고자 한다.

 

 

 

3. 필터 동작 멈춰!

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String path = request.getRequestURI();
        // JWT 토큰이 필요 없는 경로들에 대해 필터를 건너뜁니다
        return path.startsWith("/api/auth/login") ||
                path.startsWith("/api/user") ||
                path.startsWith("/oauth2") ||
                path.startsWith("/login");
    }
... doFilterInternal()

 

해결방법은 정말 간단하다. 아까 토큰 검증을 진행했던 JwtAuthenticationFilter에서 shouldNotFilter()메서드만 오버라이드 해주면 된다. 해당 필터가 동작할 때 여기로 접근하면 그냥 필터링 하지않갰다는 기능과 역할에 충실한 이름이다. ServletReqeust에서 접근 경로를 받아오고 그 경로들 중 토큰검증이 필요없는 경로를 등록해주면 끝이다.

 

4. permitAll()이랑 뭐가 다른가요?

  SecurityFilterChain에서의 permitAll()이랑 같아보이는데 뭐가 다른지 처음엔 궁금했다. 이걸 이해하려면 SecurifyFilterChain의 동작 순서를 먼저 알아야 한다.

 

 

상단 그림에서 permitAll()은 authorizedHttpRequest 영역에서 실행이 된다. 즉 addFilterBefore()에서 각 필터들이 실행이 되고 그 필터링을 모두 한 뒤 authorizedHttpReqeust에 내려오면 addFilterBefore()의 결과와 상관 없이 모두 허용을 해주겠다는 의미인데 여기서 사용하는 shouldNotFilter의 경우 JwtAuthenticationFilter자체가 동작하지 않게 막는 메서드이다. 해당 경로 접근시 필터링 자체를 막아서 성능적 이점을 가져가겠다는 목적을 가지고 사용을 한다.

 

5. addFilterAfter()쓰면 되지않나요?

  그럼 다음과같은 의문이 든다. AddFilterAfter()로 걸어둬서 permitAll()로 걸린 경로는 다 허용해주면 되는거 아닌가 하는 의문이 든다.

이렇게 되면 물론 동작은 한다. 하지만 인증 로직의 순서 문제가 있다. 여기선 동작하는 필터들이 cors, jwtAuthenticationFilter 이렇게 두개뿐이지만 추후에 혹은 타 프로젝트에선 여러 필터들이 동작할 수 있다. 해당 필터들이 동작해야하는 순서라는게 있는데 jwtAuthenticationFilter만 addAfterFilter()로 등록하게 된다면 순서가 꼬이고 유지보수적인 문제에서 자유롭지 못하다. 

  인증과 관련된 부분들은 addFilterBefore()에다 담고, 그 후 로깅이나 세션관리, 권한확장이나 토큰 갱신과같은 사용자 확인및 인증과 관련된게 아닌 내용들에 대해서 담는것이 일반적인 관례다.

 

addFilterAfter() 예제

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 기본 설정들...
            
            // 인증 이후 로깅을 위한 필터
            .addFilterAfter(new AuditLogFilter(), UsernamePasswordAuthenticationFilter.class)
            
            // 접근 제어 결정 이후 응답 헤더를 수정하는 필터
            .addFilterAfter(new SecurityHeadersFilter(), FilterSecurityInterceptor.class)
            
            // JWT 인증 이후 토큰 갱신 처리를 위한 필터
            .addFilterAfter(new TokenRefreshFilter(), JwtAuthenticationFilter.class);
            
        return http.build();
    }
}

 

 

 

0.

  기존에 아이디/패스워드 방식으로 로그인이 동작하던 로그인단에서 추가로 구글로그인도 가능하게 해달라는 요청이 들어왔다.

먼저 구글로그인을 사용하려면 구글 API에 등록과정이 필요하다. 해당과정은 하단 블로그 참조

https://blogan99.tistory.com/90

 

 

1. 의존성

build.gradle에 다음과 같이 소셜로그인용 의존성을 넣어준다

implementation "org.springframework.boot:spring-boot-starter-oauth2-client" // 소셜로그인 용

 

OAuth2 토큰발급의 주체는 구글서버이고, 우리인가 인증 서버는 구글서버의 클라이언트인 상황이다. 구글로그인 성공시 자체 jwt 토큰을 내려줄 의도이기에 'org.springframework.security:spring-security-oauth2-authorization-server'는 의존성을 추가하지 않기로 했다.

 

 

2.  SecurityFilterChain

기존 아이디/패스워드 로그인 방식을 유지하면서 구글로그인도 가능하게 하는 방법을 필요로 하는 상황인데 SecurityConfig에서 SecurityFilterChain에 약간만 추가해주면 된다.

 

기존 SecurityFilterChain

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, PrincipalOauth2UserService principalOauth2UserService, OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler) throws Exception {
    http
            .csrf(csrf -> csrf.disable())  // CSRF 비활성화
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) // CORS 필터 추가
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/api/auth/**").permitAll()
                    .requestMatchers("/api/user/**").permitAll()
                    .anyRequest().authenticated()
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가

    return http.build();
}

 

 

 

OAuth2 추가 FilterChain

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, PrincipalOauth2UserService principalOauth2UserService, OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler) throws Exception {
    http
            .csrf(csrf -> csrf.disable())  // CSRF 비활성화
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) // CORS 필터 추가
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/oauth2/**", "/login/**").permitAll() // ✅ 소셜로그인 경로는 허용경로로 추가.
                    .requestMatchers("/api/auth/**").permitAll()
                    .requestMatchers("/api/user/**").permitAll()
                    .anyRequest().authenticated()
            )
            // OAuth 소셜로그인 도입후 추가한 부분.
            .oauth2Login(oauth2 -> oauth2
                    .userInfoEndpoint(userInfo -> userInfo.userService(principalOauth2UserService))
                    .defaultSuccessUrl("/", true)
                    .successHandler(oAuth2LoginSuccessHandler)
                    .failureHandler((request, response, e) -> {
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                        response.setContentType("application/json;charset=utf-8");
                        response.getWriter().write("{\"error\": \"" + e.getMessage() + "\"}");
                    })
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가

    return http.build();
}

 

 

소셜 로그인 경로로 접근시에 로그인은 누구나 허용해야 하므로 authorizeHttpRequests()에 해당경로를 추가해주고,

authorizeHttpRequests() 하단에 .oauth2Login()을 추가해주면 된다. oauth2Login()에는 로그인 정책을 지정하는 PrinciapOauth2UserService와, 로그인 성공후 진행할 정책에 대한 내용인 OAuth2LoginSuccessHanler를 등록해주고, 로그인 실패를 컨트롤 할 failureHanlder()를 채워주면 된다.

 

 

2-1 PrincipalOauth2UserService

@Service
@RequiredArgsConstructor
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    private final UserRepository userRepository;

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

        String email = oAuth2User.getAttribute("email");

        OAuth2User oAuth2User2 = super.loadUser(userRequest);
        Optional<Users> users = userRepository.findUsersByEmail(email);

        if (users.isPresent()) {
            // 토큰발급은 OAuth2LoginSuccessHandler에서 진행.
            return new UserDetailsImpl(users.get(),oAuth2User2.getAttributes());
        }
        else{
            throw new GoogleUserException("회원가입이 필요합니다. 관리자에게 문의하세요");
        }

    }
}

 

  우선 UserRepository를 살펴본 뒤, 해당 메일로 가입한 이력이 있는지 확인한 뒤, 가입 이력이 있으면 로그인 성공을, 실패시 예외를 던지도록 구현하였다.

  정책따라 다르긴 하지만, 소셜로그인 이력이 없으면 여기서 바로 회원가입을 진행하록 많이들 구현한다. 하지만 현재 요구사항에선 소셜가입은 관리자를 통해서만 가능하게 해달라는 요청이 있어 가입이 되어있지 않다면 예외를 뱉어내도록 구현하였다.

 

 

2-2 OAuth2LoginSuccessHandler

@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        String token = jwtTokenProvider.generateAuthToken(authentication);

        ResponseCookie jwtCookie = ResponseCookie.from("jwt", token)
                .httpOnly(true)// js에서 접근 불가능(xss 방어)
                .secure(false) // true면 https에서만 사용 가능. 운영은 true로
                .path("/") //모든 경로에서 사용가능
                .maxAge(3600)
                .sameSite("Strict") //Csrf 방어
                .build();
                
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.SET_COOKIE, jwtCookie.toString());

        response.addHeader(jwtCookie.getName(), jwtCookie.getValue());
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"response\":\"SUCCESS\"}");
        response.setHeader("jwt", jwtCookie.toString());
    }
}

 

로그인 성공시엔 기존에 아이디/패스워드 로그인단에서 발급해주던 방식과 동일하게 토큰틀 발급하도록 해주었다.

 

 

3. 

위 과정이 모두 완료되었다면 로그인 테스트를 진행한다. 우선 서버를 구동하고, 브라우저를 열어서 localhost:8080/oauth2/authorization/google 경로로 접근을 해주고 로그인을 진행해준다.

 

 

 

 

미리 등록된 계정으로 로그인을 진행하면 SUCCESS 리스폰스가 잘 떨어지는 모습.

0. 현상황

  현재 Spring Cloud Gateway에 Spring Security를 도입을 하는 과정에서 jwt 토큰 인증중 예외가 발생하면 500에러가 그대로 노출이 되는 상황이 발생을 하였다. 현 상황을 그대로 두면 클라이언트에게 서버 상황을 그대로 노출을 하게되어 불편을 초래하고, 또 보안상 좋지않기 때문에 해당 예외를 400에러로 바꾸고 에러 메시지, 에러코드정도만 담은 간단한 json 형태로 바꾸어 출력을 해주고자 한다.

 

 

 

1. 웹플럭스 기반 Security에서의 필터체인

  Spring Cloud Gateway는 기본적으로 WebFlux 기반으로 동작한다. WebFlux기반에서 돌아가는 SpringSecurity에서는 SecurityFilterChain에서 에러 핸들링 방법을 설정해줘야 한다.

  @Bean
    public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
        return http
                .csrf(csrf -> csrf.disable()) // CSRF 비활성화
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers(whiteListPath.getWhiteList()).permitAll() // 화이트리스트 경로는 허용
                        .anyExchange().authenticated() // 그 외는 인증 필요
                )
                .securityContextRepository(securityContextRepository)
                .authenticationManager(authenticationManager)
                .exceptionHandling(exceptionHandlingSpec ->
                        exceptionHandlingSpec.authenticationEntryPoint(authenticationEntryPoint)
                )
                .build();
    }

여기서 하단에 위치한 코드중에서

.exceptionHandling(exceptionHandlingSpec ->
        exceptionHandlingSpec.authenticationEntryPoint(authenticationEntryPoint)
)

 

.exceptionHandling() 으로 예외를 어떤걸 이용하여 핸들링을 할 지를 결정을 해주면 된다.

 

authenticationEntryPoint는 ServerAuthenticationEntryPoint 인터페이스를 구현한 클래스를 작성해주면 된다.

@Component
public class CustomAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {

        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

        String body = "{\"error\": \"" + ex.getMessage() + "\"}";
        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = exchange.getResponse()
                .bufferFactory()
                .wrap(bytes);

        return exchange.getResponse().writeWith(Mono.just(buffer));
    }
}

 

이런식으로 에러를 잡으면 에러코드와 형식, 그리고 예외 메시지를 함께 포함하여 클라이언트단에 전달하고자 한다.

 

2. 원하는 지점에서 예외발생시키기.

 

1.에서 작성한 SecurityFilterChain동작 중  jwt 토큰 추출과 검증방법을 지정해주는 AuthenticationManager 에서 토큰 검증 중 검증에 실패하거나, 로그아웃을 하여 BlackList에 등록이 된 jwt는 예외를 발생하도록 정해주었다.

 

@RequiredArgsConstructor
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
    private final JwtTokenProvider tokenProvider;

    @Qualifier("blacklistRedisTemplate")
    private final RedisTemplate<String, Object> authRedisTemplate;


    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String token = authentication.getCredentials().toString();

        if (!tokenProvider.validateToken(token) || authRedisTemplate.hasKey(token)) {
            return Mono.error(new CustomTokenException("토큰이 유효하지않습니다."));
        }

        String username = tokenProvider.getUsername(token);
        String role = tokenProvider.getRole(token);

        List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(role));
        return Mono.just(new UsernamePasswordAuthenticationToken(username, null, authorities));
    }
}

 

여기까지만 하면 이제 원하는대로 동작을 할 것 같다.

 

3. 의도치 않은 예외발생

  이제 에러가 잡혔다고 생각하고 다시 테스트를 해보면

 

아까 봤던 에러가 그대로 올라온다. 무언가 이상하다. AuthenticationEntryPoint가 동작을 안한다는 의미이다.

 

사실 AuthenticationEntryPoint는 문제가 없다. 우선 예외가 터지는 구조는 다음과 같다.

 

 SecurityFilterChain이 실행이 되고, JwtAuthenticationManager에서 Jwt 검증을 하다가 검증을 통과하지 못하면 Exception을 던지게 되는데 이 Exception을 ExceptionTranslationWebFilter 가 감지한다.

 

이때 Exception이 AuthenticationException이나 AccessDeniedException을 감지하게 되면 ServerAuthenticationEntryPoint를 실행을 하게 되는데, 만약에 다른 예외이거나 SeucurityFilterChain 밖에서 예외가 터져버리면 ServerAuthenticationEntryPoint에서 예외를 잡을수가 없다.

 

JwtAuthenticationManager에서 작성한 코드를 다시 보자.

@Component
@RequiredArgsConstructor
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
    private final JwtTokenProvider tokenProvider;

    @Qualifier("blacklistRedisTemplate")
    private final RedisTemplate<String, Object> authRedisTemplate;


    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String token = authentication.getCredentials().toString();

        if (!tokenProvider.validateToken(token) || authRedisTemplate.hasKey(token)) {
            return Mono.error(new CustomTokenException("토큰이 유효하지않습니다."));
        }

        String username = tokenProvider.getUsername(token);
        String role = tokenProvider.getRole(token);

        List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(role));
        return Mono.just(new UsernamePasswordAuthenticationToken(username, null, authorities));
    }
}

 

 

여기서 이 부분이 문제가 되는 부분이다.

return Mono.error(new CustomTokenException("토큰이 유효하지않습니다."));

 

 Mono.error()로 주면 SecurityContextRepository의 load에서 예외가 발생 -> 정상 흐름이 아니라서 securityFilterChain에서 벗어나버려서 예외를 핸들링할 방법이 없다.

 

Mono.empty()로 주면 정상적인 SecurityFilterChain의 흐름이고 CustomAuthenticationEntryPoint까지 정상적으로 흘러는 가지만 empty()엔 파라미터를 받지않아서 우리가 주고싶었던 CustomException의 메시지를 client단에 넘겨줄 수 없고 AuthenticationException의 기본 메시지만 출력이 가능하다. 

 

 

4. 의도한 메시지 보내주기 -> SecurityExceptionHandler 구현

return Mono.error()로 그대로 두고,   ErrorWebExceptionhandler를 구현을 하게되면 SecurityFilterChain에서 벗어난 에러처리 + 지정한 메시지 클라이언트에게 전달 이 두가지를 다 성공할 수 있다. 코드는 다음과 같다.

@Component
public class SecurityExceptionHandler implements ErrorWebExceptionHandler {
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 여기서 필터 단계에서 발생한 예외를 캐치
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        String message = "예상치 못한 에러가 발생했습니다.";

        if (ex instanceof CustomTokenException) {
            status = HttpStatus.UNAUTHORIZED;
            message = ex.getMessage(); // "토큰이 유효하지 않습니다."
        }

        exchange.getResponse().setStatusCode(status);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        String body = "{\"error\": \"" + message + "\"}";

        DataBuffer buffer = exchange.getResponse()
                .bufferFactory()
                .wrap(body.getBytes(StandardCharsets.UTF_8));
        return exchange.getResponse().writeWith(Mono.just(buffer));
    }
}

 

ErrorWebExceptionHandler는 Webflux 애플리케이션에서 최상위 예외처리자이다. 이 핸들러를 구현을 해주게 된다면 모든 예외처리가 가능하다. 

 

이렇게 SecurityExceptionHandler를 구현을 하고 다시 테스트를 진행해보면

 

CustomException에서 지정한 메시지를 넘겨받는데 성공하였다.

1편에서는 공통으로 사용할 UsersEntity를 만들고 Member-Server에 적용을 하였고, 이번편에서는 Auth-Server에 UsersEntity를 적용을 하려고 한다.

 

 서브모듈 등록하기

우선 1편에서와 마찬가지로, Auth-server의 root에서 깃에 올려둔 공통모듈을 서브모듈로 등록해준다.

git submodule add https://github.com/깃헙아이디/레파지토리명.git 원하는폴더명

 

 

이렇게 src폴더상위에 등록을 해주고, 기존에 사용하던 UserDetails를 상속받는 UsersEntity는 삭제해주고, 로그인 서비스에 사용될 UserDetailsImpl을 새로 만들어준다. 

 

public class UserDetailsImpl implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String nickname;
    private Role role;

    public UserDetailsImpl(Users usersByUsername) {
            this.id = usersByUsername.getId();
            this.username = usersByUsername.getUsername();
            this.password = usersByUsername.getPassword();
            this.email = usersByUsername.getEmail();
            this.nickname = usersByUsername.getNickname();
            this.role = usersByUsername.getRole();
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + this.role));
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    public String getRole(){
        return this.role.toString();
    }

    @Override
    public boolean isAccountNonExpired() { return true; }

    @Override
    public boolean isAccountNonLocked() { return true; }

    @Override
    public boolean isCredentialsNonExpired() { return true; }

    @Override
    public boolean isEnabled() { return true; }
}

 

 

UsersEntity를 그대로 사용하던 Security의 userDetailsService() 로직을 수정을 해줘야 한다.

 

securityConfig단에 기존 로직은 다음과 같다.

    @Bean
    public UserDetailsService userDetailsService() { // ✅ UsersRepository 주입
        return username -> repository.findUsersByUsername(username)
                .orElseThrow(() -> new UserException(username + "에 로그인 할 수 없습니다. 아이디나 비밀번호를 확인해주세요."));

 

 

이제 위 코드를 아래와 같이 UsersEntity로 받아온 유저 정보를, UserDetailsImpl로 리턴하게 변경해준다.

  public UserDetailsService userDetailsService() { // ✅ UsersRepository 주입
        return username -> {
            Users user = repository.findUsersByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("해당 사용자가 존재하지 않습니다: " + username);
            }
            return new UserDetailsImpl(user);
        };
    }

 

 

이렇게 되면 MemberServer와 AuthServer의 분리가 완전히 끝났다. 

+ Recent posts