두둥 프로젝트 진행도중 스웨거도 올려서 배포하다보니, 보안도 중요하니 비밀번호를 걸고 싶어졌다.
nest js 에서는 스웨거에 비밀번호 거는게 설정만 해주면 자동으로 나오는 기능인데,
spring은 검색을 해봐도 스웨거 쪽에서 세팅해주는게 안나와서
spring security로 basic auth 를 설정하는 방법을 포스팅 하려한다.
참고로 두둥 프로젝트에서는 jwt 도 인증용으로 같이 사용중이다.
목차
1. 문제점
2. 스프링에서 basic auth는 어떻게 동작할까?
3. basic auth 설정하기
4. AccessDeniedFilter
1. 문제점
http.authorizeRequests().mvcMatchers(SwaggerPatterns).authenticated().and().httpBasic();
위처럼 SecurityConfig에 한줄만 넣으면 되는거 아니야? 라고 맨처음 생각했었다.
맞다. 아마 사실 특별히? 건든건 게 없는 프로젝트라면.. 잘될꺼다.
유저를 디비로 새로만들긴 그러하니
메모리로 유저만들어서 적당히 비밀번호 환경변수로 생성하면 그만이다.
두둥에서 문제 되었던점은
builder.addFilterBefore(accessDeniedFilter, FilterSecurityInterceptor.class);
AccessDeniedFilter 로 인한 문제가 컸다.
AccessDeniedFilter 를 커스텀해서 사용중인 이유는 인증이 필요한 api 에 인증을 허가하지않은 상태에서 접근할 경우
필터간에 AccessDenied Exception 이 발생하게 되는데, 흘러가게 냅두면 클라이언트한테 403 코드 응답만 오게 된다.
정형화된 형식이 다있으므로 이를 처리를 해줘야한다.
basic auth 가 필요한 요청이오게되면, 스프링 시큐리티 필터간에 AccessDeniedException이 발생하며,
자동으로 적절하게 처리되어야하는데,
위에 403을 잡자고 필자가 중간에 껴넣었으니 제대로된 basic auth가 수행되지 못하던 문제였다.
2. 스프링에서 basic auth는 어떻게 동작할까?
basic auth는 인증 안된사용자면 401 로 WWW-Authenticate 헤더 내려주고, 브라우저가 알아서 폼형태로 띄워서
Authorizatioin : Basic <Base64인코딩> 한 형식으로 적절하게 보내주는 형태이다.
여기서 볼 부분은 401을 어느포인트에서 내려주는가 이다.
13번째 ExceptionTranslationFilter를 보면 될것 같다.
hanleAccessDeniedException 메서드 안에서 두번정도 타고 넘어가면,
익명유저일 때 AccessDeniedException 이 발생한 상태라면
commence 메소드를 실행시켜 401 응답과 함께 WWW-Authenticate 헤더를 전송하게 되어있다.
그럼, AccessDeniedException 은 어디서 발생하나요?
필터 통과하다가 마지막에 vote 를 하는 시점이있다.
투표에서 다 떨어지면 마지막에 AccessDeniedException 이 뜨는 것이다.
정수원님의 스프링 시큐리티 강의를 들으면, 디버깅하시면서 세세하게 알려주신다.
잠만..필터를 지났는데..? 어떻게 다시 ExceptionTranslationFilter로 돌아왔죠?
간단하다 필터를 진행시킬때 try catch로 감싸서 그렇다.
진행하다가 에러가 터지면 에러처리하는 방식이다.
한번 통과했다고 끝인게 아니라, 갔다가 돌아온다.
마치 블로그를 적어야지 적어야지하다가, 쌓이고나서 부랴부랴 적고있는 필자의 모습과 같다.
정리하자면 최종 필터가 끝날때 basic auth 가 필요한 api 이면 인증이 안되어있는 익명 유저일경우
AccessDeniedException이 뜰거고. 그것에대한처리는 ExceptionTranslationFilter 에서 한다.
BasicAuthenticationEntryPoint Class 의 commenc 메소드에서 401로 리턴한다.
3. basic auth 설정하기
스프링에서 basic auth를 처리하는 방법을 알았으니, 적용해볼 차례이다.
/** 스웨거용 인메모리 유저 설정 */
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user =
User.withUsername(swaggerUser)
.password(passwordEncoder().encode(swaggerPassword))
.roles("SWAGGER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(8);
}
인메모리로 유저한명을 세팅한다.
중요포인트는 roles를 SWAGGER로 부여한 점이다.
두둥 프로젝트는 기본 유저는 USER 로 기본권한이 세팅되어있다.
basic auth로 로그인 한 사용자는 스웨거만 사용할수 있어야한다.
api를 basic auth 인증한채로 쏘면 안되기때문이다.
.anyRequest()
.hasRole("USER");
api 같은경우 USER 권한 이상인 사용자만 사용가능하도록 해놨다. ( 롤 계층 적용도되어있음 )
if (springEnvironmentHelper.isProdAndStagingProfile()) {
http.authorizeRequests().mvcMatchers(SwaggerPatterns).authenticated().and().httpBasic();
}
// SWaggerPatterns
public static final String[] SwaggerPatterns = {
"/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs",
};
그다음 SecurityConfig의 filterChain 메소드안에 스웨거 패턴 url 만 httpBasic을 설정해줬다.
스웨거 인증거는건 개발환경에서는 필요없으니, 동적으로 profile 가져와서 staging,prod 환경에만 세팅하는것으로 했다.
이렇게 하면 basic auth는 적용되어지게 된다.
소스한번 보는게 좀더 이해가 쉬우실것 같다.
4. AccessDeniedFilter
스프링에서 기본적으로 403에대한 처리는 ExceptionTranslationFilter 내부에서 행해지고 있다.
구조는 똑같다. try catch 로 감싸서 다음 필터로 넘어가게되고,
필터 마지막에서 vote를 하면서 AccessDenied Exception을 발생시키는데,
basic auth를 적용해야할 api가 아닌경우 403을 리턴하게 되어있다.
즉 403을 우리가 원하는 형식으로 내려주기 위해서는
AccessDenied Exception 을 ExceptionTranslationFilter가 처리하게 둬선 안된다.
ExceptionTranslationFilter -> < 이사이 에 필터를 집어넣어야한다. > -> vote
위와같은 흐름을 맞추기 위해
//FilterConfig
@Override
public void configure(HttpSecurity builder) {
builder.addFilterBefore(jwtTokenFilter, BasicAuthenticationFilter.class);
builder.addFilterBefore(jwtExceptionFilter, JwtTokenFilter.class);
// ExceptionTranslationFilter 보다 뒤에 있도록 한다.
builder.addFilterBefore(accessDeniedFilter, FilterSecurityInterceptor.class);
}
필터 컨피그에서 ExceptionTranslationFilter 보다 더 앞쪽으로 커스텀하게 만든 AccessDeniedFilter를 위치시킨다.
위와같은 순서가 되게되는것이다.
이로써 403으로 리턴해주는건 형식을 맞출수 있지만, 하나 더 문제가있다.
AccessDeniedFilter 가 spring security 의 basic auth 기본 처리 ( 401 리턴 ) 를 막아버리기 때문이다.
이 문제가 두둥 프로젝트에서 가장 필자를 어렵게 만들었던 문제였다.
따라서 AccessDeniedFilter doFilter 메소드에서 필터를 적용시켜야할 필터를 정해야한다.
// AccessDeniedFilter custom
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String servletPath = request.getServletPath();
return PatternMatchUtils.simpleMatch(SwaggerPatterns, servletPath);
}
즉 위처럼 필터를 하지말아야할 패스를 지정해서 넘겨줄 수 있다.
이렇게 설정하게되면 basic auth에 대한 처리는 ExceptionTranslationFilter 에게,
기존 jwt 수행과정속에서 인증이 안되면 403으로 원하는 응답값 형식을 내려줄 수 있게 된다.
basic auth 와 jwt 인증을 같이 쓸려고 하니, 맨처음엔 막막했었다.
적용해보고, 디버거를 하루 왼종일 돌려보면서 구조를 파악하고 , 올바르게 적용하고나니
한층더 스프링 시큐리티에 이해도가 생긴것 같다.
구글에 위내용으로 검색해보면, 아예 없는 내용인것같은데, 이글을 보고 다들 쉽게적용할 수 있으면 좋을것 같다.
이글은 두둥 프로젝트를 진행하면서 작성한 글이다. 참고하시길 바란다.
'스프링' 카테고리의 다른 글
[스프링] spring swagger 같은 코드 여러 에러 응답 예시 만들기 (3) | 2023.03.06 |
---|---|
[스프링] 공통 응답 형식 만들기 ResponseBodyAdvice (0) | 2023.03.05 |
[스프링] error code 도메인 별 분리하기 (0) | 2023.03.05 |
[스프링] spring swagger api 하나만 인증 풀기 (2) | 2023.03.05 |
[스프링] Spring disable Aop in test (1) | 2023.01.23 |