spring

스프링 시큐리티 JWT 간단 정리 -1

kimkim615 2024. 8. 30. 01:34

 

1. 간단한 동작 원리 

 

- 구현 -

인증 : 로그인
인가 : JWT를 통한 경로별 접근 권한
회원가입
 
 
 

회원가입:

세션 방식과 차이가 없음
API 서버라서 POST 요청을 보내고 JOINCONTROLLER에서 요청을 받고 JOINSERVICE에서 처리 진행하고 USERENTITY에 담아서 REPOSITORY로 데이터베이스에 집어 넣는다
 

로그인:

로그인 경로로 요청이 오면 1. USERNAME 어쩌구 필터를 통해 회원 검증을 한다
MANAGER로 아이디 비밀번호를 던지고 내부적으로 로그인 검증을 한다
로그인 성공하면 세션과 다르게 JWT는 세션에다가 회원정보를 남기지 않고 메서드를 통해
JWT UTIL에서 토큰을 만들어서 응답해줌
 

경로 접근(인가):

토큰을 헤더에 넣어서 요청을 진행해야 한다 특정 경로로 요청이 오면
필터를 통해 검증을 진행하고 jwt 필터라는걸 만들어서 필터 검증을 진행한다
만약에 토큰이 알맞게 존재하고 정보가 일치하면, jwt 필터에서 강제로 일시적인 세션을 만든다
특정 경로로 들어가면 세션이 있기때문에 admin경로에 들어갈수 있고 작업 진행 ㄱㄴ
이렇게 생성된 세션은 요청이 끝나면 소멸됨
 

2. 프로젝트 생성 및 JWT 의존성 추가

 
SpringJWT 프로젝트에서 의존성은 이정도

 
여기서 데이터베이스 의존성(JPA랑 MySQL)  잠시 주석 처리 -> 연결 시켜주지 않으면 에러 뜸
 
JWT 토큰 생성과 관리를 위해 JWT 의존성을 설정해줘야되는데
보통 JWT 0.11.5 버전으로 많이 구현하는데, 최신버전은 0.12.3라서 최신 버전으로 한다
그러나 0.11.5도 중요하기 때문에 이거에 대한 구현도 할 것이다 (버전이 다르면 메서드가 많이 다름)
 

기본 Controller - MainController, AdminController

 
웹서버 말고 api 서버로 동작함 그래서 웹페이지가 아니라 데이터를 응답해주는 거임 -> @ResponseBody 선언
getmapping으로 admin경로에 요청이 오면 문자열 "admin Controller" 리턴해줄 것임 

 
루트 경로로 요청이 오면 main Controller 문자열 리턴해줌

 
여기서 이대로 실행하면 시큐리티에서 로그인 하라는 화면이 나옴
 

3. Security Config 클래스 (STATELESS)

시큐링 시큐리티의 인가 및 설정을 담당하는 클래스다 Config 구현은 시큐리티 버전별로 많이 다름
여기서는 6.2.1 버전으로 구현할 것임

 
버전은 gradle에서 확인할 수 있음 (나는 6.3.3)
https://github.com/spring-projects/spring-security/releases

 

Releases · spring-projects/spring-security

Spring Security. Contribute to spring-projects/spring-security development by creating an account on GitHub.

github.com

 
여기서 버전마다 달라진 점 알 수 있다
 

Security Config 클래스 기본 요소 작성 - SecurityConfig

 
일단 먼저 기본적인 설정만 진행하고 나중에 추가 구현한다.
클래스가 스프링부트에 configuration 클래스로 관리되기 위해 @Configuration 추가
이는 시큐리티를 위한 것이기 때문에 @EnableWebSecurity도 추가 
 
클래스 내부에 특정 메서드들을 빈으로 등록해서 시큐리티를 설정하기 위해 @Bean 추가
 
HttpSecurity를 인자로 받고 예외처리를 해줬다 그리고 받은 인자를 데이터 타입으로 만들어서 리턴한다
 
JWT 방식은 세션을 STATELESS로 관리하기 때문에 csrf에 대한 공격을 방어하지 않아도 되어서
기본적으로 disable 상태로 둠 + form 로그인 방식이랑 http basic 인증 방식도 disable 상태로 둠
 
저번에 만든 기본 컨트롤러들에 대한 어떠한 권한을 가져야되는지 인가 작업을 진행함
authorizeHttpRequests 메서드를 통해 진행하고, 람다식 형태로 만듦
permitAll 다 ㄱㄴ  hasRole 권한설정  authenticated 로그인 한 사람만 ㄱㄴ
 
세션 설정 할 때 STATELESS로 설정하는 코드 중요
 
비밀번호를 캐시로 암호화해서 진행하므로 BCryptPasswordEncoder 메서드 구현해서 bean으로 등록
 

4. POSTMAN 설치

이전에 설치해서 스킵
 

5. DB연결 및 Entity 작성

JWT 발급 전에 회원 정보 검증해야 함 -> 사용자한테 아이디 비번 받고 내부적으로 검증해야됨
그래서 회원 정보를 저장하는 DB MySQL 쓸 것임
 
application.properties에서 MySQL 연결 설정하는 코드 추가할 것임
 
< DB 연결 >
 
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 
spring.datasource.url=jdbc:mysql://localhost:3306/데이터베이스명?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true
 
spring.datasource.username=아이디
 
spring.datasource.password=비밀번호
 
< Hibernate ddl 설정 >
 
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

 

여기서 mysql workbench에서 데이터베이스, 여기선 이름이 data1이므로.... data1 창을 만들고나서

 
이러한 명령문을 실행해줘야 데이터베이스가 생성된다
원래는 아래와 같이 테이블을 생성하는걸 해야하는데 application.properties에서 ddl-auto=create 설정을 해주기 때문에
할 필요 없음

 
 
회원 테이블 Entity 작성: UserEntity

 
회원을 저장할 객체를 만드는 것이다
엔티티의 요소 id, username, password, role (유저 권한을 나타냄) 
id가 생성되는 방식인 GeneratedValue 자동으로 생성되고 관리될수 있도록 identity 설정을 해야지,
id 값이 겹치지않고 잘 생성됨
 
getter setter 어노테이션 방식으로 선언
 
이 엔티티를 가지고 데이터베이스에서 접근해서 테이블에 데이터를 담아오고 다시 집어넣어줄
대리인인 repository를 이제 생성해야한다
 
 

회원 테이블 Repository 작성: UserRepository

 

 
repository는 JpaRepository를 상속받아야 하기 때문에  interface 형식이여야 함
JpaRepository는 두가지 인자를 받는데 첫번째는 repository가 해당하는 entity를 넣어주고
두번째는 엔티티의 타입 즉 id의 타입인 integer를 넣어준다
 
마지막으로 데이터베이스 테이블을 만들어야되는데 테이블을 스프링 기반 엔티티를 기준으로 만들 수 있다
그건 application.properties에서 ddl-auto 설정을 통해 가능하다
아까는 none이였는데 ddl-auto=create으로 설정을 바꿔주면 된다
실행이 완료가 되면 잘 생성이 된 것이므로 다시 auto값을 none으로 바꿔준다.
create로 계속 있으면 기존 데이터가 있을 시 삭제하고 재생성하므로 바꿔줘야됨
-> 그래서 기존 테이블을 유지하면서 변경 사항에 따라 테이블을 수정하는 update 설정을 사용하는게 권장됨
근데 이 강의에서는 create 쓰고 none(변경 작업 하지 않는 설정)으로 바꾸라 했으니까 그대로 따라갈 것이다 ...
 
UserEntity와 같은 엔티티 클래스를 기반으로 테이블을 생성한다
 

6. 회원가입 로직 구현

 

출처: https://www.devyummi.com/page?id=668d525886d3d643f4c18ba0

 
POST 요청으로 join 경로에다가 username이랑 password 를 담아서 회원가입을 위해 던져준다
그 데이터를 dto로 받아서 joincontroller에서 경로에 대한 매핑을 진행해서 dto 데이터를 받는다
받아서 joinservice단으로 넘겨서 dto 데이터를 userentity로 옮겨담아서
최종적으로 userrepository에 넘겨서 데이터베이스 내부에 userentity라는 테이블에 회원을 저장한다
 
DB 테이블을 확인하는 쿼리를 날리면 이렇게 확인 가능하다 그리고 방금 생성해서 데이터가 없는걸 확인할 수 있다

 
이제 회원가입 경로로 아이디와 비밀번호를 던져서 내부에 데이터가 차는지 확인해볼 것이다
그 전에 회원가입과 관련된 dto,controller,service를 만들어볼 것임
 

JoinDTO
 

 
회원가입 데이터를 받을 클래스를 만든다.
username이랑 password 이 두 가지 데이터를 받을 것이므로 선언해준다
여기도 getter setter 어노테이션을 선언해준다
 
 

JoinController
 

 
 

특정한 경로에서 데이터를 받을 클래스를 만든다
api 서버를 만들어줄 것이므로 응답을 responsebody 형태로 진행할 것이다
회원가입이 완료되었으면 ok 글자를 응답한다
데이터를 인자로 받아야하므로 dto를 받는다 - > JoinDTO joinDTO 선언
 
강의에서는 아래처럼 필드에 대한 생성자를 선언해줬으나 @RequiredArgsConstructor 어노테이션으로 대체해줬다..
@RequiredArgsConstructor는 final 필드를 파라미터로 받는 생성자를 자동으로 생성해주는 어노테이션임
 

 
 

JoinService
 

 

직접 회원가입을 처리하는 서비스단을 관리할 서비스 클래스를 만든다
서비스를 등록하기 위해서는 @Service 어노테이션을 선언해준다
내부에 회원가입 진행하는 메서드를 만든다
 
원래는 return값을 회원가입이 되었는지 안되었는지 boolean 값으로 리턴해주지만 이번 강의는
JWT가 주제이므로 그냥 간단하게 void값으로 리턴해줌
앞단에서 오는 DTO를 받아야하므로 JoinDTO라는 인자를 받고 진행한다
 
username이랑 password값을 꺼내야하므로 joinDTO에서 get 메서드를 통해서 꺼낸다
 그리고 이미 존재하는 username인지 확인하는 과정을 거쳐야 한다 
그러므로 userRepository에 아래 코드를 추가한다 username이 존재하는 jpa 구문이다

 

이를 사용하기 위해서 service에서 사용하기 위해서 주입받는다 -> UserRepository 선언

여기서 강의의 아래 코드는 @RequiredArgsConstructor 어노테이션으로 대체한다

 

그리고 entity에 dto에서 받은 데이터를 그대로 옮겨주기 위해서 entity의 특정 필드 값에 대한 설정을 진행한다

password의 경우 암호화를 위해 encoder 주입받고 사용한다

role 같은 경우 우리가 지정해줘야 하는데 여기서는 일단 admin 권한을 다 준다

다하면 repository에 save한다

 

이제 controller에서 JoinService 주입 받고 사용한다

그리고 joinProcess 메서드에다가 joinDTO 객체를 그대로 전달한다

 

이렇게 되면 회원가입 로직을 모두 작성했다

회원가입을 테스트하기 위해서 POSTMAN 실행한다

 

위처럼 form-data를 전송해보면 controller에서 service단으로 넘어간 뒤에 데이터베이스에 데이터를 저장하게 됨

그래서 mysql workbench에서 확인해보면 아래와 같이 데이터가 들어간 것을 알 수 있다

 

 

 

7. 로그인 필터 구현

로그인 모식도

 

이제까지 구현한 것은 /join 부분을 다 한거다 데이터베이스에 회원정보를 다 집어넣은 것

이번엔 로그인 요청을 받아서 처리하는 것을 진행하고 검증을 할 것이다

 

사용자의 요청이 필터를 통해서 id와 비번을 가지고 들어오면, 필터가 그걸 꺼내서 로그인을 진행한다고

AuthenticationManger 한테 넘겨줌. 그러면 AuthenticationManger가 데이터베이스로부터 회원정보를 가지고 와서 검증함.

검증이 확인이 되면 successfulAuth가 동작이 된다. 동작을 할 때 JWT를 생성해서 사용자한테 응답해주면 된다

검증이 실패가 되면 JWT를 만들지 않고 401 응답코드를 리턴해줌

 

 

스프링 시큐리티 필터 동작 원리

 

클라이언트의 요청이 여러 개의 필터를 거쳐서 컨트롤러를 향하게 됨 

스프링 부트 어플리케이션은 전반적으로 톰캣이라는 서블릿 컨테이너 위에서 동작하게 된다

클라이언트의 요청이 오면 톰캣의 서블릿 필터들을 다 통과해서 최종적으로 스프링 부트의 컨트롤러로 전달이 된다

필터에서 가로채서 회원 정보를 검증하게 될건데, 필터들중에 하나인 DelegatingFilter를 등록해서 모든 요청을 가로챈다

 

이제 서블릿 필터 체인의 DelegatingFilter에서 SecurityFilter로 넘어간다. 두 가지가 다르다는 것을 확실히 알아야 함

 

왼쪽 부분은 서블릿 필터고, 오른쪽 부분은 시큐리티 필터

 

요청을 가로채와서 그 요청에 대해서 검증한다 그리고 그 상황에 따라 거부하거나 리디렉션시키거나

서블릿으로 요청을 전달하던지... 한다.

 

SecurityFilterChain의 필터 목록과 순서가 존재한다.

 

JWT 방식의 Form 로그인 방식을 disable 했으므로 위에 있는 UsernamePasswordAuthenticationFilter가 적용이 안된다

그래서 이 필터를 강제로 커스텀 해서 JWT에서 동작이 되도록 만들어서 진행해줄 것이다

 

 

로그인 요청 받기 : 커스텀 UsernamePasswordAuthentication 필터 작성

 

기존의 UsernamePasswordAuthentication 필터는 폼 로그인에서는 활성화 되어있었는데

폼 로그인을 disable 시켰기 때문에 우리가 강제로 만들어줘야함 

로그인 검증을 위한 커스텀된 UsernamePasswordAuthentication 필터를 작성한다

LoginFilter 클래스는 UsernamePasswordAuthenticationFilter 라는 클래스를 상속받아서 사용할 것이다

 

리턴타입은 인증을 하는 Authentication을 리턴하고 이름은 attemptAuthentication이라는 메서드를 작성하는데,

인자는 응답과 요청을 받는다. 예외처리도 해준다

요청을 가로채서 요청에 담겨있는 username과 password 값을 string으로 받는다

꺼낸 값으로 인증을 진행한다. AuthenticationManager한테 값을 던져줘야 하는데 그 바구니는

UsernamePasswordAuthenticationToken이라는 애다. 그래서 최종적으로 manager한테 전달이 된다

그래서 이제 검증을 받는다. 

 

 

 이렇게 만든 필터를 등록해줘야 하는데, 등록하는 방법은 SecurityConfig에서 등록해서 사용하면 된다

 

 

SecurityConfig 설정

 

Bean으로 등록한 filterChain에다가 필터를 등록하는 코드를 작성해준다

.addFilterAt은 원하는 자리에 등록하는거고 .addFilterBefore은 해당 필터 전, .addFilterAfter는 그 후에 등록하는 거다 

우리는 기존 필터 UsernamePasswordAuthentication를 대체하는 것이므로 .addFilterAt 메서드를 사용한다

그 필터는 new를 통해 LoginFilter를 등록해준다 두번째 인자는 위치를 어디로 할 것인지 세팅하는것이므로

UsernamePasswordAuthentication을 작성해준다.

LoginFilter의 경우 특정한 인자를 받아준다. 우리가 클래스를 만들 때 생성자 방식으로 authenticationManger 객체를

주입받았는데, 여기서 주입을 시켜주지 않으면 동작이 안되므로 LoginFilter에 주입시켜줘야한다

그래서 AuthenticationManger Bean 등록하는 코드를 작성해줬다

그러니까! .addFilterAt(new LoginFilter() 이 코드 인자에다가 authenticationManager() 호출을 한번 진행한다

그런데, authenticationManager 또한 AuthenticationConfiguration이라는 인자를 갖는다.  그러니깐 Manager()안에

authenticationConfiguration을 인자로 넣어준다

글고 그 인자는 맨 상단에 private final로 생성자 주입 시켜줬다...

 

 

 

기본적인 세팅은 끝났고, 이제 로그인 성공시 succesfulAuthentication에서 특정한 JWT를 반환하는 코드를 작성하면 된다

아직 다 작성한건 아님! 그치만 필터 등록 확인을 위한 테스트를 진행할 것이다

 

 

이렇게 보내면 오류는 뜬다. 왜냐면 이후의 구현을 다 진행하지 않았기 때문이다

그러나 오류 구문에서 admin이 찍힌 것으로 필터 등록을 확인할 수 있다

-> LoginFilter 코드에서 System.out.println(username); 으로 찍어줬기 때문에