feat : JwtSecurtyConfiguration, JwtAuthenticationFilter, JwtUserRepository 추가

This commit is contained in:
banjjoknim
2022-03-27 18:44:30 +09:00
parent a3fcd21c29
commit b2d330db7d
3 changed files with 137 additions and 3 deletions

View File

@@ -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)
}
}

View File

@@ -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<GrantedAuthority> {
val authorities = mutableListOf<GrantedAuthority>()
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)
}
}

View File

@@ -0,0 +1,7 @@
package com.banjjoknim.playground.jwt.domain.user
import org.springframework.data.jpa.repository.JpaRepository
interface JwtUserRepository : JpaRepository<JwtUser, Long> {
fun findByUsername(username: String): JwtUser?
}