[웹 스터딩]4.3 스프링 시큐리티 통합

박준수·2022년 12월 24일
0

[웹 스터디]

목록 보기
19/19

스프링 시큐리티를 쓰는 이유

예를 들어보자)
모든 API 요청에 토큰을 보네는 것으로 사용자를 확인한다. 그러면 각 API는 맨 처음 토큰을 확인함으로써 접근을 허용 또는 거부하는 코드를 실행할 것이다. 문제는 모든 API가 이 작업을 해야한다는 것이다. 50개가 넘는 API가 있으면 요청을 인증하는 코드를 50번 반복해야 한다는 것이다. 하지만 스프링 시큐리트를 이용해 코드를 한 번만 짜고, 이 코드가 모든 API를 수행하기 바로 전에 실행되도록 설정하고 구현 하면 된다.

JWT 생성 및 반환 구현

build.gradle에 jjwt 라이브러리 추가

TokenProvider 클래스

  • create() : JWT 라이브러리를 이용해 JWT 토큰을 생성한다.(SECRET_KEY를 개인키로 사용한다.) 유저의 정보를 받아 JWT를 생성한다.
  • validateAndGetUserId() : 토큰을 디코딩, 파싱 및 위조여부를 확인한다.

계정 생성 후 token필드 반환 확인

성공!

스프링 시큐리티와 서블릿 필터

스프링 시큐리티는 아주 간단히 말하면 서블릿 필터의 집합이다. 그렇다면 서블릿 필터는 무엇인가?
서블릿 필터는 서블릿 실행 전에 실행되는 클래스들이다.

  • 구현된 로직에 따라 원하지 않는 HTTP 요청을 걸러 낼 수 있다.
  • 걸러낸 HTTP는 거절되는 것이고, 서블릿 필터에서 전부 살아남은 HTTP 요청은 마침내 디스패쳐 서블릿으로 넘어와 우리 컨트롤러에서 실행된다.

서블릿 필터가 꼭 한 개일 필요는 없다. 기능에 따라 다른 서블릿 필터를 작성할 수 있고, 이 필터들을 FilterChain을 이용해 연쇄적으로 순서대로 실행 할 수 있다.

그렇다면 이 서블릿 필터에서 스프링 시큐리티의 위치와 우리가 구현할 필터의 위치는 어디일까?
바로 JwtAuthenticationFilter부분이다!!!!

JWT를 이용한 인증 구현

스프링 시큐리티 gradle에 추가

JwtAuthenticationFilter

코드 설명

  1. 요청의 헤더에서 Bearer토큰을 가져온다. 이 작업은 parseBearerToken() 메서드에서 이뤄진다.
  2. TokenProvider를 이용해 토큰을 인정하고 UsernamePasswordAuthenticationToken을 작성한다. 이 오브젝트에 사용자의 인증 정보를 저장하고 SecurityContext에 인증된 사용자를 등록한다. 서버가 요청이 끝나기 전까지 방금 인증한 사용자의 정보를 갖고 있어야 하기 때문이다.
  • 왜 서버가 이와 같은 정보를 가지고 있어야 할까?
    요청을 처리하는 과정에서 사용자가 인증됐는지 여부와 인증된 사용자가 누구인지 알아야 할 때가 있기 떄문이다.
  1. 스프링 시큐리티의 SecurityContext는 SecurityContextHolder의 createEmptyContext() 메서드를 이용해 생성할 수 있다.
  2. 생성한 컨텍스트에 인증정보인 authentication을 넣고 다시 SecurityContextHolder에 컨텍스트로 등록한다.
  • SecurityContextHolder는 기본적으로 ThreadLocal에 저장된다.(각 스레드 마다 하나의 컨텍스트를 관리 할 수 있으며 같은 스레드 내라면 어디에서든 접근할 수 있다.)

스프링 시큐리티 설정

서블릿 필터를 구현하는 일을 하였으므로 서블릿 필터를 사용하라고 알려주는, 설정 작업을 해준다.

WebSecurityConfig

  • HttpSecurity는 시큐리티 설정을 위한 오브젝트이다. 이 오브젝트는 빌더를 제공하는데, 빌데럴 이용해 cors, csrf, httpbasic, session, authorizeRequest 등 다양한 설정을 할 수 있다.즉 web.xml대식 HttpSecurity를 이용해 시큐리티 관련 설정을 하는 것이다.

  • addFilterAfter() : JwtAuthenticationFilter를 CorsFilter 이후에 실행하라고 설정하는 것이다.

테스팅
로그인 후 GET요청에 Bearer토큰 함께 보내기
정상적인 인증 성공!

토큰이 다르면 403 Forbidden!

TodoController에서 인증된 유저 사용하기

그 전에는 String temporaryUserId = "temporary-user"; 로 임시로 지정해 놓았던 유저 아이디를 사용하고 있었다. 이제 인증된 유저 아이디를 사용할 수 있도록 각 메서드에 userId 매개변수를 추가한다.

TodoController

userId는 도대체 누가 어떻게 String인 것을 알고 넘겨주는 것일까?

@AuthenticationPrincipal이 userId를 찾아낸다. 그렇다면 @AuthenticationPrincipal은 무엇인가? 답을 찾기 위해 JwtAuthenticationFilter 클래스를 봐보자.

JwtAuthenticationFilter에서 UsernamePasswordAuthenticationToken을 생성했다. 이때 생성자의 첫 매개변수로 넣은 것이 AuthenticationPrincipal이다. 이 AuthenticationPrincipal에 String형의 userId를 넣었다.

따라서

  • 스프링은 컨트롤러 메서드를 부를 떄 @AuthenticationPrincipal 어노테이션이 있다는 것을 안다.
  • SecurityContextHolder에서 SecurityContext::Authentication, 즉 UsernamePasswordAuthenticationToken을 가져와 컨트롤러 메서드에 넘겨준다.

테스팅

두 계정을 생성하고 각각 Todo를 추가해보았다 첫번째 사용자가 추가한 Todo는 보이지 않고 오직 두 번째 사용자가 추가한 Todo만 반환된다.

패스워드 암호화

UserService.class

보통 암호화된 패스워드를 비교해야 하는 경우, 사용자에게 받은 패스워드를 같은 방법으로 암호화한후, 그 결과를 데이터베이스의 값과 비교하는 것이 자연스러운 흐름이다.

우리는 matches() 메서드를 사용했는데 BCryptPasswordEncoder는 같은 값을 인코딩하더라도 값이 다르기 때문이다. (패스워드에 랜덤하게 의미 없는 값(salt)을 붙여 결과를 생성한다.)
따라서 사용자에게 받은 패스워드를 인코딩해도 데이터베이스에 저장된 패스워드와는 다를 확률이 높다. 대신 mathches() 메서드가 Salt를 고려해 두 값을 비교해준다.

UserController.class

회원가입 시 패스워드를 인코딩해 저장하는 부분과 로그인 시 수정한 getByCredentials에 BCryptPasswordEncorder를 넘겨주는 것이다.

profile
방구석개발자

0개의 댓글