feat : JwtAuthenticationFilter 를 이용한 로그인 인증 로직 추가

This commit is contained in:
banjjoknim
2022-03-27 21:17:59 +09:00
parent 483df22623
commit 7071770951
2 changed files with 76 additions and 10 deletions

View File

@@ -1,8 +1,14 @@
package com.banjjoknim.playground.jwt.config.filter
import com.banjjoknim.playground.jwt.config.security.PrincipalDetails
import com.banjjoknim.playground.jwt.domain.user.JwtUser
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@@ -26,7 +32,9 @@ import javax.servlet.http.HttpServletResponse
* @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
* @see com.banjjoknim.playground.jwt.config.security.JwtSecurityConfiguration
*/
class JwtAuthenticationFilter(authenticationManager: AuthenticationManager) : UsernamePasswordAuthenticationFilter() {
class JwtAuthenticationFilter(
private val authenticationManagerFromSecurityConfiguration: AuthenticationManager // authenticationManager 로 변수명을 지으면 이름이 겹쳐서 컴파일 에러가 발생하여 변수명 변경.
) : UsernamePasswordAuthenticationFilter() {
/**
* 기존의 /login URL로 요청을 하면 로그인 시도를 위해 호출되는 함수이다.
@@ -38,25 +46,81 @@ class JwtAuthenticationFilter(authenticationManager: AuthenticationManager) : Us
*
* /login URL로 요청을 하면 UsernamePasswordAuthenticationFilter 가 해당 요청을 낚아채서 아래의 함수가 자동으로 실행된다.
*
* 로그인시 Filter의 동작 순서 및 구현해줘야 하는 것들은 아래와 같다.
* 따라서 구현해줘야 하는 것들은 아래와 같다.
*
* 1. username & password 를 받는다.
* 2. 포함하고 있는 AuthenticationManager로 정상인지 로그인 시도를 한다.
* 3. 로그인 시도를 하면 우리가 만든 PrincipalDetailsService#loadUserByUsername(String) 이 호출된다.
* 4. 정상적으로 로직이 수행되어서 PrincipalDetails 가 리턴되면 해당 PrincipalDetails 를 세션에 담는다.
* - 만약 세션에 PrincipalDetails 를 담지 않으면 Spring Security 에서 권한관리가 동작하지 않는다.
* - Spring Security 세션에 PrincipalDetails 객체가 존재해야 권한관리를 해준다.
* - 굳이 권한관리를 안할거면 PrincipalDetails 객체를 세션에 담을 필요가 없다.
* 5. 마지막으로 JWT 토큰을 만들어서 응답해준다.
* - 데이터베이스로부터 일치하는 id, password 가 있는지 검사한다.
* - 로직이 정상적으로 완료되면 로그인을 시도한 유저의 정보를 담고 있는 Authentication 객체가 반환된다.
* 4. 정상적으로 로직이 수행되어서 Authentication 객체가 리턴되면 해당 객체를 리턴해서 Spring Security 세션에 담는다.
* - 만약 세션에 Authentication 객체를 담지 않으면 Spring Security 에서의 권한관리가 동작하지 않는다.
* - Spring Security 는 세션에 Authentication 객체가 존재해야 권한관리를 해준다.
* - 만약 Spring Security 를 통해 권한관리를 안할거면 Authentication 객체를 세션에 담을 필요가 없다.
* 5. 마지막으로 JWT 토큰을 만들어서 응답으로 돌려주면 된다(선택-successfulAuthentication() 을 override 해서 구현해줘도 됨).
*
* @see org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
* @see org.springframework.security.authentication.AuthenticationManager
* @see org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
* @see org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter
* @see com.banjjoknim.playground.jwt.config.security.PrincipalDetailsService
* @see org.springframework.security.authentication.UsernamePasswordAuthenticationToken
*/
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
println("JwtAuthenticationFilter : 로그인 시도중")
return super.attemptAuthentication(request, response)
// println(request.inputStream) // username, password 가 담겨있다. request의 inputStream 은 Request 당 1회만 호출할 수 있으므로 주석처리.
// val bufferedReader = request.reader
// bufferedReader.lineSequence().forEach(::println) // request 데이터 확인
val objectMapper = ObjectMapper().registerKotlinModule()
val jwtUser = objectMapper.readValue(request.inputStream, JwtUser::class.java)
// println(jwtUser)
// 로그인 시도를 위해서 id, password 를 이용해서 직접 토큰을 만든다.
// UsernamePasswordAuthenticationFilter#attemptAuthentication() 함수를 참고하도록 한다.
// 즉, 우리가 직접 토큰을 만들어서 호출을 대신 수행해준다고 보면 될듯.
val authenticationToken = UsernamePasswordAuthenticationToken(jwtUser.username, jwtUser.password)
// 직접 만든 토큰을 인자로 넣고 AuthenticationManager#authenticate(Token) 을 호출하면
// 내부적으로 로직이 돌면서 우리가 만든 PrincipalDetailsService#loadUserByUsername(String) 함수가 호출된다.
// 그 결과로 User의 로그인 정보가 담긴 Authentication 객체를 얻을 수 있다.
// Authentication 객체를 얻어다는 것은 데이터베이스에 있는 username 과 password 가 일치한다는 뜻이다.
val authentication = authenticationManagerFromSecurityConfiguration.authenticate(authenticationToken)
// 위 처럼 인증이 정상적으로 진행되어 Authentication 객체를 얻었다면
// 아래처럼 Authentication 객체 내부의 PrincipalDetails 객체를 꺼내어 정보 확인이 가능하다.
// 즉, 로그인이 정상적으로 되었다는 뜻이다.
val principalDetails = authentication.principal as PrincipalDetails
println("로그인 완료됨: ${principalDetails.user.username}")
// return super.attemptAuthentication(request, response)
// 로그인이 정상적으로 되었으므로 Authentication 객체를 Session 영역에 저장해야 한다.
// Authentication 객체를 Session 영역에 저장하는 방법은 Authentication 객체를 return 해주는 것이다.
// Authentication 객체를 return 해주면 Spring Security 가 자동으로 Authentication 객체를 Security Session 영역에 저장해준다.
// Authentication 객체를 return 해서 Session 영역에 저장하는 이유는 권한 관리를 Spring Security 가 대신 해주어 관리가 편해지기 때문이다(원하지 않으면 Session 영역에 저장을 안하면 된다).
// JWT 토큰을 사용한다면 Session 영역을 굳이 만들 필요가 없다. 다만, 권한 처리 때문에 Session 에 저장하는 것이다.
// 기본적으로 Authentication 객체를 세션에 저장하는 로직은 AbstractAuthenticationProcessingFilter#successfulAuthentication() 함수에서 수행하고 있다.
// Security Session 영역에 저장되는 정보들은 잠시 사용하고 응답이 끝났을 때 버리면 된다(세션 정보는 시간이 지나면 자동으로 사라진다).
return authentication
}
/**
* 본 함수는 attemptAuthentication() 함수를 통한 인증이 성공적으로 이루어져서 Authentication 객체를 얻을 수 있는 경우 그 다음으로 호출되는 함수다.
*
* AbstractAuthenticationProcessingFilter#successfulAuthentication() 함수에는 Security Session 영역에 Authentication 객체를 저장하는 로직이 포함되어 있다.
*
* 자세한 내용은 AbstractAuthenticationProcessingFilter#successfulAuthentication() 에 달린 javadoc 을 참고하도록 하자.
*
* 따라서, 여기서 JWT 토큰을 만들어서 Request 요청한 사용자에게 JWT 토큰을 응답해주면 된다(선택사항).
*/
override fun successfulAuthentication(
request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
authResult: Authentication
) {
println("successfulAuthentication 실행됨 : ${(authResult.principal as PrincipalDetails).user.username}의 인증이 완료되었다는 뜻.")
super.successfulAuthentication(request, response, chain, authResult)
}
}

View File

@@ -59,7 +59,9 @@ class JwtSecurityConfiguration(
.formLogin().disable() // Form 태그 방식 로그인을 사용하지 않는다.
.httpBasic().disable() // HttpBasic 방식 로그인을 사용하지 않는다.
.addFilter(JwtAuthenticationFilter(authenticationManager())) // formLogin().disable() 로 인해 직접 만든 필터를 등록해주어야 Security 가 UserDetailsService 를 호출할 수 있다. 이때, AuthenticationManager 라는 녀석과 함께 등록해주어야 한다.
// formLogin().disable() 로 인해 직접 만든 필터를 등록해주어야 Security 가 UserDetailsService 를 호출할 수 있다.
// 이때, WebSecurityConfigurerAdapter 에 포함되어 있는 AuthenticationManager 라는 녀석과 함께 등록해주어야 한다.
.addFilter(JwtAuthenticationFilter(authenticationManager()))
.authorizeRequests()
.antMatchers("/api/v1/user/**").hasAnyRole("USER", "MANAGER", "ADMIN")
@@ -70,7 +72,7 @@ class JwtSecurityConfiguration(
}
class PrincipalDetails(
private val user: JwtUser
val user: JwtUser
) : UserDetails {
override fun getAuthorities(): Collection<GrantedAuthority> {
val authorities = mutableListOf<GrantedAuthority>()