Web Flux, 스프링 클라우드를 사용해 마이크로 서비스에서의 SSO를 구현 해 보자(2)

안상철·2023년 1월 7일
0

이모저모개발

목록 보기
7/8

저번포스팅에서는 스프링 게이트웨이 자체에 jwt필터를 작성하고 그 구성을 알아보았다.

이번에는 개별 서비스에 설정해 줘야 할 부분들을 알아보자

1. WebSecurityConfig

@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 작성은 구글링하면 많이 찾아볼 수 있기 때문에 주요한 부분만 살펴보자

2. AuthenticationFilter

@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는 인증 부가기능이라는데 사실 잘 모르겠다...ㅠㅠ

참고: https://sloth.tistory.com/39

참고글을 봐도 잘 모르겠다^^!

아무튼 새로운 인증 토큰을 만들어주고 확인한다.

부분에 들어가기 때문에 유효한 회원정보가 아니면 예외처리에 걸리게 된다.

3. CustomAnauthorizationHander

@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처리를 담당한다고 한다.

4. CustomAccessDeniedHandler

@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는 권한이 없는 사용자에 대한 예외처리를 담당한다.

profile
웹 개발자(FE / BE) anna입니다.

0개의 댓글