BasicAutheticationFilter
SPA 기반의 페이지 같은 경우 클라이언트 브라우저에서는 서버 사이드렌더링과는 다르게 프런트단에서 뷰를 책임진다.
서버에서 페이지를 리다일렉션 시키면서 페이지가 이동되는 것이 아니기 떄문에 SPA기반 페이지에서 스프링 시큐리티 로그인을 구현하
기 위해선 BasicAutheticationFilter를 사용해야한다. 그 밖에도 하이브리드앱 (브라우저 기반의 모바일 앱) 같은 경우에도
BasicAutheticationFilter 혹은 JWT(JSON WEB TOKEN)기반의 인증방식을 사용한다.
스프링 시큐리티 config 파일에서 configure(HttpSecurity http) 메서드에서 httpBasic()을 enable설정하여 BasicAuthenticationFilter을 사용할 수 있다 .
BasicAuthenticationFilter의 특징
- http 에서는 header에 username:password 값이 base64로 인코딩(decode 하기가 쉽다.)되어 전달 되기 떄문에 보안에 매우 취약하다 , 반드시 https 프로토콜에서 사용할 것을 권장한다 .
- 최초 로그인시에만 인증을 처리하고 , 이후에는 session에 의존한다 , RememberMe 를 설정한 경우 , remember-me 쿠키가 브라우저에 저장되기 떄문에 세션이 만료된 이후라도 브라우저 기반의 앱에서는 장시간 서비스를 로그인 페이지를 거치지 않고 이용할 수 있다 .
- 에러가 나면 401(UnAuthorized) 에러를 내려보낸다
Bearer 토큰
- 최초의 로그인 이후로 토큰을 통해서만 서버에 인증을 요청한다. JWT 토큰 로그인에서 사용된다 .
BasicAuthenticationFilter 로직
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
BasicAuthenticationFilter filter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(
User.withDefaultPasswordEncoder()
.username("user1")
.password("1111")
.roles("USER")
.build()
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable()
.httpBasic();
}
프로젝트를 생성하고 시큐리티 설정파일을 만들어줬다. 기본 user1이라는 인메모리 user를 만들었고 ,
BasicAuthenticationFilter를 사용하기위해 httpSecurity에 .httpBasic()을 설정해 줬다 . csrf 필터가 설정되 있으면 포스트 요청시에 csrf 토큰을 요청하기 때문에 일단은 꺼줬다 .
요청은 Test 에서 요청하기 위해 TestRestTemplate를 활용했다 .
TestRestTemplate는 테스트에서 사용하는 RestTemplate며 생성시 생성자 파라미터로 id , password를 넘겨주면 요청헤더에 authorization 헤더가 자동으로 추가되어 편리하다 .
@LocalServerPort
int port;
RestTemplate client = new RestTemplate();
private String greetingUrl(){
return "http://localhost:"+port+"/greeting";
}
@DisplayName("3 testRestTemplate 활용")
@Test
void useTestRestTemplate(){
//테스트용 restTemplate
//파라미터로 username 과 password를 넘기면
//알아서 Authorization 헤더를 만들어준다
TestRestTemplate template = new TestRestTemplate("user1","1111" );
String resp = template.getForObject(greetingUrl(), String.class);
assertEquals(resp , "hello");
}
컨트롤러에는 GET /greeting 에 대한 요청처리("hello"를 리턴)하는 greeting이라는 메서드가 있다 .
TestRestTemplate를 통해 getForObject()로 요청을 하고 응답값을 테스트 했다 .
테스트가 성공한다 설정한 필터가 잘 동작이 된 것이다 .
BasicAuthenticationFilter의 주요 로직을 살펴보기위해 BasicAuthenticationFilter로 들어가 브레이크 포인트를 잡고 디버그 모드로 돌려봤다 .
doFilterInternal()
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
if (authRequest == null) {
this.logger.trace("Did not process authentication request since failed to find "
+ "username and password in Basic Authorization header");
chain.doFilter(request, response);
return;
}
String username = authRequest.getName();
this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
if (authenticationIsRequired(username)) {
Authentication authResult = this.authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException ex) {
SecurityContextHolder.clearContext();
this.logger.debug("Failed to process authentication request", ex);
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, ex);
}
return;
}
chain.doFilter(request, response);
}
동작을 순서대로 나열 해보면
요청이 들어오고 authenticationConverter의 convert(request) 메서드를 거치게된다
convert()
@Override
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return null;
}
header = header.trim();
if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
return null;
}
if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
throw new BadCredentialsException("Empty basic authentication token");
}
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded = decode(base64Token);
String token = new String(decoded, getCredentialsCharset(request));
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim),
token.substring(delim + 1));
result.setDetails(this.authenticationDetailsSource.buildDetails(request));
return result;
}
convert 에서는 요청에서 헤더값을 꺼내 null 체크 , trim ,Basic 이라는 문자열을 없애고 (Basic 토큰임을 알려야하기 때문에 헤더에 Basic이 앞에 붙는다 ) base64를 decode하고 ' : ' 의 위치를 찾아 전후로 하여 username과 password를 구분하고 껍데기 토큰을 발행한 후에 디테일을 체워주고 리턴한다
리턴 후에 doFilterInternal()를 보면 이전에 살펴본 UsernamePasswordFilter와 동작이 비슷하다 다른점은 인증 이전에 유저가 이미 인증되어있는지를 체크하여 인정되 있다면 인증을 스킵하고 아니면 리턴된 토큰을 가지고 authenticationManager에게 넘겨 인증을 위임하고 , 리턴된 Authentication을 SecurityContextHolder에 등록한다
'Spring Security' 카테고리의 다른 글
스프링시큐리티 공부 11 - 로그인 유지 (session과 remberme cookie) (0) | 2021.07.06 |
---|---|
스프링시큐리티 공부 9 - multi filter chain 구현 (0) | 2021.07.03 |
스프링시큐리티 공부 6 - RoleHierarchy, authenticationDetailsSource 설정하기 (1) | 2021.06.29 |
스프링시큐리티 공부 5 - form 로그인 1 (0) | 2021.06.29 |
스프링시큐리티 공부 4 - 스프링 시큐리티에서의 로그인 (0) | 2021.06.27 |