From b2d330db7d77ff64a105e93b8da3db9eff294f09 Mon Sep 17 00:00:00 2001 From: banjjoknim Date: Sun, 27 Mar 2022 18:44:30 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20JwtSecurtyConfiguration,=20JwtAuthen?= =?UTF-8?q?ticationFilter,=20JwtUserRepository=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwt/config/filter/JwtAuthenticationFilter.kt | 62 ++++++++++++++++ .../jwt/config/security/JwtSecurityConfiguration.kt | 71 ++++++++++++++++++- .../playground/jwt/domain/user/JwtUserRepository.kt | 7 ++ 3 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/filter/JwtAuthenticationFilter.kt create mode 100644 놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/domain/user/JwtUserRepository.kt 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 new file mode 100644 index 0000000..5626343 --- /dev/null +++ b/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/config/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,62 @@ +package com.banjjoknim.playground.jwt.config.filter + +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * Spring Security 에는 UsernamePasswordAuthenticationFilter 가 있다. + * + * 기본적으로는 /login 요청에서 username, password 를 전송하면 (post 요청) UsernamePasswordAuthenticationFilter 가 동작한다. + * + * 하지만 우리는 formLogin().disable() 설정을 해주었기 때문에 직접 Filter 를 만들어서 Security 설정에 등록해주어야 한다. 그래야 Security 에서 UserDetailsService 를 호출할 수 있다. + * + * 단, Security 에 등록해줄 때 AuthenticationManager 와 함께 등록해주어야 한다. AuthenticationManager 를 통해서 로그인이 진행되기 때문이다. + * + * 참고로, AuthenticationManager 는 WebSecurityConfigurerAdapter 가 들고 있고, 그 녀석을 사용하면 된다. + * + * AuthenticationManager 는 AbstractAuthenticationProcessingFilter 또한 가지고 있으므로 + * UsernamePasswordAuthenticationFilter 를 상속받아서 사용하는 대신 AbstractAuthenticationProcessingFilter 를 상속받아서 사용해도 된다. + * + * @see org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + * @see org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter + * @see org.springframework.security.authentication.AuthenticationManager + * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter + * @see com.banjjoknim.playground.jwt.config.security.JwtSecurityConfiguration + */ +class JwtAuthenticationFilter(authenticationManager: AuthenticationManager) : UsernamePasswordAuthenticationFilter() { + + /** + * 기존의 /login URL로 요청을 하면 로그인 시도를 위해 호출되는 함수이다. + * + * 추상 메서드로, AbstractAuthenticationProcessingFilter 에 포함되어 있으며, + * AbstractAuthenticationProcessingFilter 를 상속받은 UsernamePasswordAuthenticationFilter, OAuth2LoginAuthenticationFilter 등이 구현하고 있다. + * + * AbstractAuthenticationProcessingFilter#doFilter(HttpServletRequest, HttpServletResponse) 에서 내부적으로 호출하고 있다. + * + * /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 토큰을 만들어서 응답해준다. + * + * @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 + */ + override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication { + println("JwtAuthenticationFilter : 로그인 시도중") + return super.attemptAuthentication(request, response) + } +} 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 6abef66..890e50f 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 @@ -1,12 +1,20 @@ package com.banjjoknim.playground.jwt.config.security -import com.banjjoknim.playground.jwt.config.filter.AuthorizationFilter +import com.banjjoknim.playground.jwt.config.filter.CustomAuthorizationFilter import com.banjjoknim.playground.jwt.config.filter.CustomFilter3 +import com.banjjoknim.playground.jwt.config.filter.JwtAuthenticationFilter +import com.banjjoknim.playground.jwt.domain.user.JwtUser +import com.banjjoknim.playground.jwt.domain.user.JwtUserRepository import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.web.context.SecurityContextPersistenceFilter +import org.springframework.stereotype.Service import org.springframework.web.filter.CorsFilter /** @@ -32,15 +40,19 @@ class JwtSecurityConfiguration( // 우리가 원하는 위치에 Filter 를 등록한다. 만약 Spring Security Filter 보다도 먼저 실행되게 하고 싶다면 SecurityContextPersistenceFilter 보다 먼저 실행되도록 아래처럼 등록해주면 된다. http.addFilterBefore(CustomFilter3(), SecurityContextPersistenceFilter::class.java) - http.addFilterBefore(AuthorizationFilter(), SecurityContextPersistenceFilter::class.java) + http.addFilterBefore(CustomAuthorizationFilter(), SecurityContextPersistenceFilter::class.java) http.csrf().disable() // 기본적으로 웹은 STATELESS 인데, STATEFUL 처럼 쓰기 위해서 세션과 쿠키를 만든다. 이때, 그걸(세션과 쿠키) 사용하지 않도록 설정하는 것이다. - http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 사용하지 않겠다는 설정. 토큰 기반에서는 기본 설정이다. 상태가 없는 서버를 만든다. + http.sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 사용하지 않겠다는 설정. 토큰 기반에서는 기본 설정이다. 상태가 없는 서버를 만든다. .and() .addFilter(corsFilter) // filter 에 Bean 으로 등록해준 CorsFilter 를 추가한다. 따라서 모든 요청은 추가된 CorsFilter 를 거치게 된다. 이렇게 하면 내 서버는 CORS 정책에서 벗어날 수 있다(Cross-origin 요청이 와도 다 허용될 것이다). .formLogin().disable() // Form 태그 방식 로그인을 사용하지 않는다. .httpBasic().disable() // HttpBasic 방식 로그인을 사용하지 않는다. + + .addFilter(JwtAuthenticationFilter(authenticationManager())) // formLogin().disable() 로 인해 직접 만든 필터를 등록해주어야 Security 가 UserDetailsService 를 호출할 수 있다. 이때, AuthenticationManager 라는 녀석과 함께 등록해주어야 한다. + .authorizeRequests() .antMatchers("/api/v1/user/**").hasAnyRole("USER", "MANAGER", "ADMIN") .antMatchers("/api/v1/manager/**").hasAnyRole("MANAGER", "ADMIN") @@ -48,3 +60,56 @@ class JwtSecurityConfiguration( .anyRequest().permitAll() } } + +class PrincipalDetails( + private val user: JwtUser +) : UserDetails { + override fun getAuthorities(): Collection { + val authorities = mutableListOf() + for (role in user.getRoles()) { + authorities.add(GrantedAuthority { role }) +// authorities + GrantedAuthority { role } + } + return authorities + } + + override fun getPassword(): String { + return user.password + } + + override fun getUsername(): String { + return user.username + } + + override fun isAccountNonExpired(): Boolean { + return true + } + + override fun isAccountNonLocked(): Boolean { + return true + } + + override fun isCredentialsNonExpired(): Boolean { + return true + } + + override fun isEnabled(): Boolean { + return true + } +} + +/** + * 원래는 http://localhost:8080/login 요청이 올 때 동작한다(Spring Security 의 기본 로그인 url). + * + * 하지만 우리는 formLogin().disable() 했기 때문에 위 url로 요청이 들어올 때 직접 PrincipalDetailsService 를 호출할 Filter 를 만들어줘야 한다. + */ +@Service +class PrincipalDetailsService( + private val jwtUserRepository: JwtUserRepository +) : UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + val jwtUser = jwtUserRepository.findByUsername(username) + ?: throw UsernameNotFoundException("can not found user by username. username: $username") + return PrincipalDetails(jwtUser) + } +} diff --git a/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/domain/user/JwtUserRepository.kt b/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/domain/user/JwtUserRepository.kt new file mode 100644 index 0000000..d363050 --- /dev/null +++ b/놀이터(예제 코드 작성)/spring-security/src/main/kotlin/com/banjjoknim/playground/jwt/domain/user/JwtUserRepository.kt @@ -0,0 +1,7 @@ +package com.banjjoknim.playground.jwt.domain.user + +import org.springframework.data.jpa.repository.JpaRepository + +interface JwtUserRepository : JpaRepository { + fun findByUsername(username: String): JwtUser? +}