본문 바로가기
백엔드/Spring Boot

Spring boot + JWT+ Kakao OAuth / 2편 (Security)

by 손정빈 2020. 11. 5.
728x90
반응형

2020/11/04 - [백엔드/Spring Boot] - Spring boot +JWT+ Kakao OAuth / 1편 (멀티모듈)

 

Spring boot +JWT+ Kakao OAuth / 1편 (멀티모듈)

안녕하세요. 하나셋입니다. 어....... 하하하 이 프로젝트는 사실 제가 카카오, 구글, 애플 등의 OAuth를 통해 유저를 관리하는 법을 좀 공부하기 위해서 만들었던 프로젝트입니다. Spring, Jwt, Oauth를

jeongbincom.tistory.com

안녕하세요. 하나셋입니다.

 

오늘은 Spring Security와 Jwt Token, 그리고 카카오 Oauth 통해 회원가입 및 로그인, 권한 체크하는 부분까지 알아볼 수 있으면 작성해보도록 하겠습니다.

 

 혹시나 Spring Security 모르시는 분은 아래의 url을 통해 보시면 되게 큰 도움이 되실것 같아 적어놨습니다.

dingue.tistory.com/5

 

Spring Security 개요 및 인증 과정

Spring Security란 ? Spring Security는 강력하면서 높은 수준의 커스터마이징 가능한 인증을 제공하고, 필터를 통한 접근 제어를 제공하는 프레임워크 Spring 기반 애플리케이션의 보안에서는 사실상의

dingue.tistory.com

 

 

우선 Spring Security Config를 먼저 살펴 볼까요?

 

SecurityConfig.class

@EnableWebSecurity
@EnableGlobalMethodSecurity(
    prePostEnabled = true
)
class SecurityConfig(
    private val unauthorizedHandler: JwtAuthenticationEntryPoint,
    private val jwtAuthenticationTokenFilter: JwtAuthenticationTokenFilter,
    private val userDetailsService: UserDetailsService
) : WebSecurityConfigurerAdapter() {

    @Bean(name = [BeanIds.AUTHENTICATION_MANAGER])
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }

    @Autowired
    fun configureAuthentication(authenticationManagerBuilder: AuthenticationManagerBuilder) {
        authenticationManagerBuilder
            .userDetailsService(this.userDetailsService)
            .passwordEncoder(this.passwordEncoder())
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }

    override fun configure(http: HttpSecurity) {

        http
            .csrf().disable()
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers(
                "/api/v1/kakao/backend/**",
                "/api/v1/user/**",

                // swagger
                "/v2/api-docs", "/configuration/ui",
                "/swagger-resources", "/configuration/security",
                "/swagger-ui.html", "/webjars/**", "/swagger/**"
            )
            .permitAll()
            .anyRequest().authenticated().and().cors() // @PreAuthorize를 위한 설정

        http
            .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java)

    }
}

 

우선 Spring Security를 사용하기 위해서는 @EnableWebSecurity 가 선언되어야 합니다.

위 코드처럼 cofigure()메소드에서 시큐리에 대한 설정을 하게 됩니다.

위 코드에서 등장하는 JwtAuthenticationEntryPoint, JwtAuthenticationTokenFilter, UserDetailsService 클래스는

 

1. JwtAuthenticationEntryPoint: JwtToken 인증을 할때 실패했을 경우의 예외처리 EntryPoint라고 생각하면 좋을것 같습니다.

2. JwtAuthenticationTokenFilter: JwtToken을 통해 UserDeatails를 불러오는 클래스라고 생각하면 좋을 것같습니다.

(JwtToken으로 유저를 인증하는 필터입니다.)

3. UserDetailsService: Interface라서 사실 JwtUserDetailsService라는 클래스를 빈등록해놨기에 실제로는 JwtUserDetailsService 클래스가 불러와지게 됩니다. 

 

하나하나 코드를 살펴도록 하죠!

 

JwtAuthenticationEntryPoint.class

@Component
class JwtAuthenticationEntryPoint: RestSupport(), AuthenticationEntryPoint {

    private val logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter::class.java)

    override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {

        val result = unauthorized(authException.message ?: "")
        response.status = HttpServletResponse.SC_UNAUTHORIZED
        response.contentType = MediaType.APPLICATION_JSON_VALUE
        response.characterEncoding = StandardCharsets.UTF_8.name()
        response.writer.write(Gson().toJson(result.body))

        logger.error("Exception: $authException")
    }
}

 

인증 과정에서 Exception 발생에 대한 처리를 작성한 코드입니다.

 

JwtAuthenticationTokenFilter.class

@Component
class JwtAuthenticationTokenFilter(
    private val userDetailsService: UserDetailsService,
    private val jwtTokenUtils: JwtTokenUtils,
    @Value("\${jwt.header}") private val header: String
) : OncePerRequestFilter() {

    private val logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter::class.java)

    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {

        val requestHeader: String? = request.getHeader(this.header)

        if(requestHeader != null && requestHeader.startsWith("Bearer ")) {

            val authToken = requestHeader.substring(7)
            val username = jwtTokenUtils.getUsernameFromToken(authToken)

            if(username != null && SecurityContextHolder.getContext().authentication == null) {
                val userDetails = userDetailsService.loadUserByUsername(username)

                if(jwtTokenUtils.validateToken(authToken, userDetails)) {
                    val authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
                    SecurityContextHolder.getContext().authentication = authentication
                }
            }
        }

        filterChain.doFilter(request, response)
    }

}

 

위 과정에서 Header에 "Bearer ~~~~"이라는 직정한 헤더의 값을 가지고 JwtToken을 검증하여 UserDetails를 불러오는 필터입니다.

 

JwtUserDetailsService.class

@Service(value = "userDetailsService")
class JwtUserDetailsService(
        private val userRepository: UserRepository
): UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails {

        val user = userRepository.findByUsernameAndActive(username) ?: throw DobiApiNotFoundUserException()

        return JwtUserFactory.create(user)
    }
}

 

유저 테이블에서 유저이름(userId일수도있고 username일수도 있습니다.)을 가지고 불러와 JwtUserDetails를 반환하는 클래스입니다.

JwtUserFactory라는 클래스는 UserDetails를 확장한 JwtUserDetails의 팩토리패턴으로 구현한 클래스입니다.

 

그 외 JwtToken 관련된 코드는 직접 보시길 추천드립니다.

 


이렇게 설정한 Spring Security와 JwtToken을 가지고 카카오 OAuth를 접목시켜야겠죠?

 

카카오 로그인을 통한 서비스 회원가입은 KakaoAuthService.class에서 보실수 있습니다.

 

 

KakaoAuthService.class

@Service
class KakaoAuthService(
    private val kakaoAuthClient: KakaoAuthClient,
    private val kakaoClient: KakaoClient,
    private val userAuthService: UserAuthService,
    private val userRepository: UserRepository,
    private val userDetailsService: UserDetailsService,
    private val authenticationManager: AuthenticationManager,
    private val jwtTokenUtils: JwtTokenUtils
) {

    fun kakaoSignIn(request: SigninRequest): SignInResponse {
        val tokenResult = kakaoAuthClient.token(redirectUri = request.redirectUri, code = request.code)

        if (!tokenResult.isSuccessful) throw DobiApiException(ErrorCode.UNAUTHORIZED_KAKAO, "카카오 토큰 조회 에러")

        val res = kakaoClient.getUserInfo(tokenResult.body()!!.accessToken)

        if (!res.isSuccessful) throw DobiApiException(ErrorCode.UNAUTHORIZED_KAKAO, "카카오 유저 정보 조회 에러")
        val kakaoRes = res.body()!!
        // 회원가입 여부 확인 후, null일 경우 회원가입
        val user = userRepository.findByProviderIdAndSocialTypeAndActive(providerId = kakaoRes.id.toString(), socialType = SocialType.KAKAO)
                ?: userAuthService.kakaoJoin(kakaoRes)

        val authentication = authenticationManager.authenticate(UsernamePasswordAuthenticationToken(KakaoAccountUtils.getUsernameByKakaoAccount(kakaoRes.id), KakaoAccountUtils.getPasswordByKakaoAccount(kakaoRes.id)))
        val userDetails = userDetailsService.loadUserByUsername(user.username)
        SecurityContextHolder.getContext().authentication = authentication

        return SignInResponse(
            nickname = user.nickname,
            email = user.email,
            socialType = user.socialType,
            token = jwtTokenUtils.generateToken(userDetails)
        )
    }

}

 

위 코드를 보면 사실상 카카오 인증을 통해 받은 유저의 정보를 가지고 JwtUserDetails을 만들어 authenticationManager를 통해 인증받습니다. authenticate라는 메소드를 통해서요.

인증 받은 authentication을 SecurityContextHolder에 넣어두게 됩니다.

 

그렇다면 권한 체크는 어떻게 되는건가?

그걸 확인하기 위해서는 이제 RestContorller로 호출해보면 되겠죠?

 

UserInfoController.class

@Api(tags = ["유저 정보 조회 및 수정"])
@RestController
@RequestMapping("/api/v1/user")
class UserInfoController(
    private val userInfoService: UserInfoService
) : RestSupport() {

    @GetMapping
    @ApiImplicitParams(
        ApiImplicitParam(
            name = "Authorization",
            value = "",
            required = true,
            allowEmptyValue = false,
            paramType = "header",
            dataTypeClass = String::class,
            example = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdHkiOlt7ImF1dGhvcml0eSI6IkxFVkVMMCJ9XSwic3ViIjoia2FrYW8xNTE0NjYxOTk4IiwiYXVkIjoibW9iaWxlIiwiaWF0IjoxNjAzODY4OTg3LCJleHAiOjE2MDUzNDg5ODd9.wf8la-S_BP011E6ufCAC7eOp3nJghZ5RbuZ57GmN9vD3bkdxH2aCRSoff6FTHYZs6L9urRdXS64Z2R4kWppKhA"
        )
    )
    @PreAuthorize("hasRole('ADMIN')")
    fun getUserInfo(user: AuthenticatedUser): ResponseEntity<Any> {
        userInfoService.getUserInfo(user)
        return response("ok")
    }
}

 

저희가 SecurityConfig에서 선언해놓은

@EnableGlobalMethodSecurity(
    prePostEnabled = true
)

 

어노테이션을 통해 @PreAuthorize라는 어노테이션을 사용할수 있게 됩니다.

@PreAuthorize를 통해 권한을 체크합니다.

 

유저와 권한에 대한 테이블 구조는 entity를 통해 확인하시는게 좋을꺼라 생각이 듭니다.

 

다음편은 마지막으로 WebMvcConfigurationSupport()를 통한 추가 작업을 하나 알아보도록 합시다.

 

2020/11/04 - [백엔드/Spring Boot] - Spring boot + JWT + Kakao OAuth / 1편 (멀티모듈)

2020/11/05 - [백엔드/Spring Boot] - Spring boot + JWT+ Kakao OAuth / 2편 (Security)

2020/11/05 - [백엔드/Spring Boot] - Spring boot + JWT + Kakao OAuth / 3편 (WebMvcConfig)

반응형

댓글