저번포스팅에서는 스프링 게이트웨이 자체에 jwt필터를 작성하고 그 구성을 알아보았다.
이번에는 개별 서비스에 설정해 줘야 할 부분들을 알아보자
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
class WebSecurityConfig(
val authenticationFilter: AuthenticationFilter,
val unauthorizationHander: CustomAnauthorizationHander,
val accessDeniedHandler: CustomAccessDeniedHandler
) : WebSecurityConfigurerAdapter() {
@Value("\${mapping.url}")
val mappingUrl: String? = null
@Bean
fun passwordEncoder(): PasswordEncoder? {
return BCryptPasswordEncoder()
}
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.cors()
.and()
.csrf()
.disable()
.addFilterBefore(authenticationFilter, AnonymousAuthenticationFilter::class.java)
.exceptionHandling()
.authenticationEntryPoint(unauthorizationHander)
.accessDeniedHandler(accessDeniedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/admin/api-auth/**").hasAnyRole("ADMIN")
.requestMatchers(RequestMatcher { request: HttpServletRequest? ->
CorsUtils.isPreFlightRequest(
request!!
)
}).permitAll()
.antMatchers("/admin/api/**").permitAll()
.antMatchers("/user/api/**").permitAll()
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/etc/api/**").permitAll()
}
@Bean
fun corsConfigurationSource(): CorsConfigurationSource? {
val configuration = CorsConfiguration()
configuration.addAllowedOrigin(mappingUrl!!)
configuration.addAllowedMethod("*")
configuration.addAllowedHeader("*")
configuration.allowCredentials = true
configuration.maxAge = 3600L
configuration.exposedHeaders = Arrays.asList("Content-Disposition")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
}
WebSecurityConfigurerAdapter
를 상속받아 구현한다.
@Configuration
어노테이션으로 설정파일임을 선언 해 주고 Bean을 등록 해 사용할 수 있도록 해 주자.
@EnableWebSecurity
어노테이션은 스프링 시큐리티를 구현하기 위해 필요하고, @EnableGlobalMethodSecurity
또한 보안적용을 위해 기재 해 주도록 한다.
일반적인 configure 작성은 구글링하면 많이 찾아볼 수 있기 때문에 주요한 부분만 살펴보자
@Component
class AuthenticationFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
try {
if (checkRequest(request)) {
val userDetails = getUserRequest(request)
if (!userDetails.isEnabled) {
throw InvalidUserException()
}
val authenticationToken = UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails.authorities
)
authenticationToken.details = WebAuthenticationDetailsSource()
.buildDetails(request)
SecurityContextHolder.getContext().authentication = authenticationToken
}
} catch (e: RuntimeException) {
SecurityContextHolder.clearContext()
}
filterChain.doFilter(request, response)
}
private fun checkRequest(request: HttpServletRequest): Boolean {
return StringUtils.hasText(request.getHeader("userJson"))
}
/**
* 헤더에서 UserPrincipal 정보추출
*/
@Throws(JsonProcessingException::class)
private fun getUserRequest(request: HttpServletRequest): UserDetails {
val mapper = ObjectMapper()
val userJson = request.getHeader("userJson")
val user = mapper.readValue(
userJson,
User::class.java
)
return UserPrincipal(
user.id,
user.userId, user.password, user.roles, user.locked,
user.checkActiveUser(), user
)
}
}
userDetails를 사용하는 기존의 필터와 거의 동일하게 구성한다.
getUserRequest
에서 request에 포함된 user정보 => 이전 포스팅에서 담아줬던 정보를 꺼내와서
class UserPrincipal: UserDetails {
private var id: Long
private var userId: String
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private var password: String
private var authorities: Collection<GrantedAuthority?>? = null
private lateinit var roles: Set<Role>
private var accountNonLocked = false
private var enabled = false
private var user: User
constructor(
id: Long,
userId: String,
password: String,
roles: Set<Role>,
accountNonLocked: Boolean,
enabled: Boolean,
user: User
) {
this.id = id
this.userId = userId
this.password = password
authorities = roles.stream()
.map { role: Role ->
SimpleGrantedAuthority(
role.type.name
)
}
.collect(Collectors.toList())
this.roles = roles
this.accountNonLocked = accountNonLocked
this.enabled = enabled
this.user = user
}
fun getId(): Long {
return id
}
fun getUserId(): String {
return userId
}
fun getRoles(): Set<Role> {
return roles
}
fun getUser(): User {
return user
}
override fun getAuthorities(): Collection<GrantedAuthority?>? {
return authorities
}
override fun getPassword(): String {
return password
}
override fun getUsername(): String {
return userId
}
override fun isAccountNonExpired(): Boolean {
return true
}
override fun isAccountNonLocked(): Boolean {
return accountNonLocked
}
override fun isCredentialsNonExpired(): Boolean {
return true
}
override fun isEnabled(): Boolean {
return enabled
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val that: UserPrincipal =
other as UserPrincipal
if (id != that.id) return false
if (userId != that.userId) return false
if (password != that.password) return false
return if (authorities != null) authorities == that.authorities else false
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + userId.hashCode()
result = 31 * result + password.hashCode()
result = 31 * result + if (authorities != null) authorities.hashCode() else 0
return result
}
}
따로 커스텀해 구성한,UserDetails
를 상속받아 구현한 클래스에 담아 return해 준다.
UsernamePasswordAuthenticationToken
구현체를 통해 userName과 비밀번호 등으로 새로운 authenticationToken 토큰을 만들어준다.
WebAuthenticationDetailsSource
는 인증 부가기능이라는데 사실 잘 모르겠다...ㅠㅠ
참고글을 봐도 잘 모르겠다^^!
아무튼 새로운 인증 토큰을 만들어주고 확인한다.
부분에 들어가기 때문에 유효한 회원정보가 아니면 예외처리에 걸리게 된다.
@Component
class CustomAnauthorizationHander : AuthenticationEntryPoint {
@Throws(IOException::class, ServletException::class)
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
) {
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
MessageUtil.getMessage("UNAUTHORIZED_ACCESS")
)
}
}
authenticationEntryPoint
는 인증/인가 실패에 따른 예외처리, 리다이렉트라고 하는데 스프링 시큐리티의 401처리를 담당한다고 한다.
@Component
class CustomAccessDeniedHandler : AccessDeniedHandler {
@Throws(IOException::class, ServletException::class)
override fun handle(
request: HttpServletRequest,
response: HttpServletResponse,
e: AccessDeniedException
) {
LoggerUtils.logger().error("Access Denied Error: " + e.message)
response.sendError(
HttpServletResponse.SC_FORBIDDEN,
"허가되지 않은 접근입니다. 다시 로그인해주세요."
)
}
}
accessDeniedHandler
는 권한이 없는 사용자에 대한 예외처리를 담당한다.