spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
배경 지식을 추가하기 위해 jwt.io 에서 멋진 소개를 제공합니다! 살펴보겠습니다:
JWT(JSON 웹 토큰)는 당사자 간에 정보를 JSON 개체로 안전하게 전송하기 위한 간결하고 독립적인 방법을 정의하는 개방형 표준(RFC 7519)입니다. 이 정보는 디지털 서명이 되어 있으므로 확인하고 신뢰할 수 있습니다. JWT는 비밀(HMAC 알고리즘 사용) 또는 RSA를 사용하는 공개/개인 키 쌍을 사용하여 서명할 수 있습니다.
이 정의의 몇 가지 개념을 더 자세히 설명하겠습니다.
컴팩트 : 크기가 작기 때문에 JWT는 URL, POST 매개변수 또는 HTTP 헤더 내부를 통해 전송될 수 있습니다. 또한 크기가 작을수록 전송 속도가 빠릅니다.
자체 포함 : 페이로드에는 사용자에 대해 필요한 모든 정보가 포함되어 있으므로 데이터베이스를 두 번 이상 쿼리할 필요가 없습니다.
JSON 웹 토큰이 유용한 몇 가지 시나리오는 다음과 같습니다.
인증 : JWT를 사용하는 가장 일반적인 시나리오입니다. 사용자가 로그인하면 각 후속 요청에 JWT가 포함되어 사용자가 해당 토큰으로 허용되는 경로, 서비스 및 리소스에 액세스할 수 있습니다. Single Sign On은 오버헤드가 적고 다양한 도메인에서 쉽게 사용할 수 있다는 점 때문에 현재 JWT를 널리 사용하는 기능입니다.
정보 교환 : JSON 웹 토큰은 당사자 간에 정보를 안전하게 전송하는 좋은 방법입니다. 예를 들어 공개/개인 키 쌍을 사용하여 JWT에 서명할 수 있으므로 보낸 사람이 누구인지 확인할 수 있습니다. 또한 헤더와 페이로드를 사용해 서명을 계산하므로 콘텐츠가 변조되지 않았는지 확인할 수도 있습니다.
JSON 웹 토큰은 점 (.) 으로 구분된 세 부분으로 구성됩니다.
따라서 JWT는 일반적으로 다음과 같습니다.
xxxxx
. yyyyy
. zzzzz
다양한 부분을 분해해 보겠습니다.
헤더
헤더는 일반적으로 토큰 유형(JWT)과 사용되는 해싱 알고리즘(예: HMAC SHA256 또는 RSA)의 두 부분으로 구성됩니다.
예를 들어:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
그런 다음 이 JSON은 Base64Url로 인코딩되어 JWT의 첫 번째 부분을 구성합니다.
유효 탑재량
토큰의 두 번째 부분은 클레임을 포함하는 페이로드입니다. 클레임은 엔터티(일반적으로 사용자) 및 추가 메타데이터에 대한 설명입니다. 클레임에는 예약된 클레임, 공개 클레임, 프라이빗 클레임의 세 가지 유형이 있습니다.
JWT가 압축되도록 되어 있으므로 클레임 이름은 단 3자입니다.
공개 청구 : JWT를 사용하는 사람들이 원하는 대로 정의할 수 있습니다. 그러나 충돌을 방지하려면 IANA JSON 웹 토큰 레지스트리에 정의하거나 충돌 방지 네임스페이스를 포함하는 URI로 정의해야 합니다.
비공개 클레임 : 사용에 동의한 당사자 간에 정보를 공유하기 위해 생성된 맞춤 클레임입니다.
페이로드의 예는 다음과 같습니다.
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
그런 다음 페이로드는 Base64Url로 인코딩되어 JSON 웹 토큰의 두 번째 부분을 구성합니다.
서명
서명 부분을 생성하려면 인코딩된 헤더, 인코딩된 페이로드, 비밀, 헤더에 지정된 알고리즘을 가져와서 서명해야 합니다.
예를 들어 HMAC SHA256 알고리즘을 사용하려는 경우 서명은 다음과 같은 방식으로 생성됩니다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
서명은 JWT의 보낸 사람이 누구인지 확인하고 메시지가 도중에 변경되지 않았는지 확인하는 데 사용됩니다. 모두 합치기
출력은 HTML 및 HTTP 환경에서 쉽게 전달할 수 있는 점으로 구분된 3개의 Base64 문자열이며, SAML과 같은 XML 기반 표준과 비교할 때 더 컴팩트합니다.
다음은 이전 헤더와 페이로드가 인코딩되어 있고 보안 비밀로 서명된 JWT를 보여줍니다. 인코딩된 JWT
인증에서 사용자가 자격 증명을 사용하여 성공적으로 로그인하면 JSON 웹 토큰이 반환되며 로컬에서 세션을 생성하는 전통적인 접근 방식 대신 로컬(일반적으로 로컬 저장소에 있지만 쿠키도 사용할 수 있음)에 저장해야 합니다. 서버를 종료하고 쿠키를 반환합니다.
사용자가 보호된 경로나 리소스에 액세스하려고 할 때마다 사용자 에이전트는 일반적으로 Bearer 스키마를 사용하여 Authorization 헤더에 JWT를 보내야 합니다. 헤더의 내용은 다음과 같아야 합니다.
Authorization: Bearer <token>
사용자 상태는 서버 메모리에 저장되지 않으므로 이는 상태 비저장 인증 메커니즘입니다. 서버의 보호된 경로는 Authorization 헤더에서 유효한 JWT를 확인하고, JWT가 있는 경우 사용자는 보호된 리소스에 액세스할 수 있습니다. JWT는 자체 포함되어 있으므로 필요한 모든 정보가 있으므로 데이터베이스를 여러 번 쿼리할 필요성이 줄어듭니다.
이를 통해 상태 비저장 데이터 API에 전적으로 의존하고 다운스트림 서비스에 요청할 수도 있습니다. API를 제공하는 도메인은 중요하지 않으므로 CORS(Cross-Origin Resource Sharing)는 쿠키를 사용하지 않으므로 문제가 되지 않습니다.
다음 다이어그램은 이 프로세스를 보여줍니다.
토큰 기반 인증 스키마는 세션/쿠키와 비교할 때 중요한 이점을 제공하므로 최근 엄청난 인기를 얻었습니다.
이 접근 방식에는 몇 가지 절충안이 필요합니다.
JWT 인증 흐름은 매우 간단합니다.
인증 클레임이 액세스 토큰에 포함된다는 점에 유의하는 것이 중요합니다. 이것이 왜 중요합니까? 글쎄, 인증 요청(예: 데이터베이스의 사용자 권한)이 액세스 토큰의 수명 동안 변경되었다고 가정해 보겠습니다. 이러한 변경 사항은 새 액세스 토큰이 발급될 때까지 적용되지 않습니다. 대부분의 경우 액세스 토큰은 수명이 짧기 때문에 이는 큰 문제가 아닙니다. 그렇지 않으면 불투명 토큰 패턴을 사용하세요.
가능한 경우 Spring 보안 기본 동작을 재사용하면서 Java 및 Spring을 사용하여 JWT 토큰 기반 인증을 구현할 수 있는 방법을 살펴보겠습니다. Spring Security 프레임워크에는 세션 쿠키, HTTP 기본 및 HTTP 다이제스트와 같은 인증 메커니즘을 이미 처리하는 플러그인 클래스가 함께 제공됩니다. 그럼에도 불구하고 JWT에 대한 기본 지원이 부족하므로 이를 작동시키려면 손을 더럽혀야 합니다.
이 데모는 현재 test_db 라는 H2 데이터베이스를 사용하고 있으므로 많은 구성 없이 즉시 실행할 수 있습니다. 다른 데이터베이스에 연결하려면 리소스 디렉터리 내의 application.yml
파일에 연결을 지정해야 합니다. hibernate.hbm2ddl.auto=create-drop
배포할 때마다 깨끗한 데이터베이스를 삭제하고 생성한다는 점에 유의하세요(실제 프로젝트에서 이것을 사용하는 경우 이를 변경하고 싶을 수도 있습니다). 다음은 프로젝트의 예입니다. 자신의 MySQL 데이터베이스를 사용하기 위해 url
및 dialect
속성에 대한 주석을 얼마나 쉽게 바꿀 수 있는지 확인하세요.
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtToken필터
JwtTokenFilter
필터는 로그인 토큰 엔드포인트( /users/signin
) 및 singup 엔드포인트( /users/signup
)를 제외하고 각 API( /**
)에 적용됩니다.
이 필터에는 다음과 같은 책임이 있습니다.
JwtTokenProvider
에 인증을 위임하고 그렇지 않으면 인증 예외를 발생시킵니다. 인증 성공 시 chain.doFilter(request, response)
호출되는지 확인하세요. 가장 마지막 필터인 FilterSecurityInterceptor#doFilter 가 요청된 API 리소스를 처리하는 컨트롤러의 메서드를 실제로 호출하는 역할을 담당하기 때문에 요청 처리를 다음 필터로 진행하려고 합니다.
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilterConfigurer
스프링 부트 보안의 DefaultSecurityFilterChain
에 JwtTokenFilter
추가합니다.
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
JwtToken공급자
JwtTokenProvider
에는 다음과 같은 책임이 있습니다.
내사용자세부정보
자체 사용자 정의 loadUserbyUsername 함수를 정의하기 위해 UserDetailsService
구현합니다. UserDetailsService
인터페이스는 사용자 관련 데이터를 검색하는 데 사용됩니다. 여기에는 사용자 이름을 기반으로 사용자 엔터티를 찾고 사용자를 찾는 프로세스를 사용자 지정하기 위해 재정의할 수 있는 loadUserByUsername 이라는 메서드가 하나 있습니다.
인증 중에 사용자에 대한 세부 정보를 로드하기 위해 DaoAuthenticationProvider
에서 사용됩니다.
웹보안구성
WebSecurityConfig
클래스는 WebSecurityConfigurerAdapter
확장하여 사용자 지정 보안 구성을 제공합니다.
이 클래스에서는 다음 Bean이 구성되고 인스턴스화됩니다.
JwtTokenFilter
PasswordEncoder
또한 WebSecurityConfig#configure(HttpSecurity http)
메서드 내에서 보호/비보호 API 엔드포인트를 정의하는 패턴을 구성합니다. 쿠키를 사용하지 않기 때문에 CSRF 보호를 비활성화했습니다.
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
Java 8 및 Maven이 설치되어 있는지 확인하십시오.
이 저장소를 포크하고 복제하세요.
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
로 이동하여 모든 것이 올바르게 작동하는지 확인하세요. application.yml
파일에서 기본 포트를 변경할 수 있습니다. server :
port : 8080
/users/me
에 GET 요청을 하세요. 아직 유효한 JWT 토큰을 설정하지 않았으므로 Access Denied
메시지와 함께 403
응답을 받아야 합니다. $ curl -X GET http://localhost:8080/users/me
/users/signin
에 대한 POST 요청을 만듭니다. $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
에 대한 초기 GET 요청을 다시 수행합니다. $ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}