diff --git a/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/filter/JwtAuthenticationFilter.kt b/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/filter/JwtAuthenticationFilter.kt index 5626343..b8efef3 100644 --- a/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/filter/JwtAuthenticationFilter.kt +++ b/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/filter/JwtAuthenticationFilter.kt @@ -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) } } diff --git a/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/security/JwtSecurityConfiguration.kt b/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/security/JwtSecurityConfiguration.kt index d53c123..c535104 100644 --- a/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/security/JwtSecurityConfiguration.kt +++ b/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/security/JwtSecurityConfiguration.kt @@ -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 { val authorities = mutableListOf()