feat : JwtSecurtyConfiguration, JwtAuthenticationFilter, JwtUserRepository 추가
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
Reference in New Issue
Block a user