-
Notifications
You must be signed in to change notification settings - Fork 5
스프링 시큐리티를 이용한 api 사용 인증 #68
base: develop
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package kr.flab.wiki.app.components.authentication | ||
|
|
||
| import io.jsonwebtoken.Claims | ||
| import io.jsonwebtoken.Jws | ||
| import kr.flab.wiki.app.utils.JwtUtils | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken | ||
| import org.springframework.security.core.context.SecurityContextHolder | ||
| import org.springframework.web.filter.OncePerRequestFilter | ||
| import javax.servlet.FilterChain | ||
| import javax.servlet.http.HttpServletRequest | ||
| import javax.servlet.http.HttpServletResponse | ||
|
|
||
| class JwsAuthenticationFilter : OncePerRequestFilter() { | ||
| override fun doFilterInternal( | ||
| request: HttpServletRequest, | ||
| response: HttpServletResponse, | ||
| filterChain: FilterChain | ||
| ) { | ||
| val jws: String? = JwtUtils.parseJws(request.getHeader("Authorization") ?: "") | ||
| var result: Jws<Claims>? | ||
| if (jws != null) { | ||
| result = JwtUtils.validateJws(jws) | ||
| if (result != null) { | ||
| val email = JwtUtils.getEmailFromJws(jws) | ||
| SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(email, null) | ||
| } | ||
| } | ||
| filterChain.doFilter(request, response) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package kr.flab.wiki.app.components.authentication | ||
|
|
||
| import org.springframework.security.core.AuthenticationException | ||
| import org.springframework.security.web.AuthenticationEntryPoint | ||
| import org.springframework.stereotype.Component | ||
| import javax.servlet.http.HttpServletRequest | ||
| import javax.servlet.http.HttpServletResponse | ||
|
|
||
| @Component | ||
| class UnauthorizedEntryPoint : AuthenticationEntryPoint { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요거 네이밍 좋습니다. |
||
| override fun commence( | ||
| request: HttpServletRequest?, | ||
| response: HttpServletResponse?, | ||
| authException: AuthenticationException? | ||
| ) { | ||
| response?.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,25 @@ | ||
| package kr.flab.wiki.app.utils | ||
|
|
||
| import io.jsonwebtoken.SignatureAlgorithm | ||
| import io.jsonwebtoken.Claims | ||
| import io.jsonwebtoken.Jws | ||
| import io.jsonwebtoken.Jwts | ||
| import io.jsonwebtoken.SignatureAlgorithm | ||
| import io.jsonwebtoken.security.Keys | ||
| import org.springframework.security.core.Authentication | ||
| import org.springframework.util.StringUtils | ||
| import java.security.KeyPair | ||
| import java.util.Date | ||
|
|
||
| class JwtUtils private constructor() { | ||
| companion object { | ||
| private const val EXPIRATION_TIME: Long = 1 * 1 * 30 * 60 * 1000 | ||
|
|
||
| /** | ||
| * 해당 키페어는 서버 시작할 때마다 재생성됩니다. | ||
| * 추후에는 미리 생성한 후 설정파일(ex : application-*.yml)에서 읽어들일 예정입니다. | ||
| */ | ||
| private val KEY_PAIR: KeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256) | ||
| private const val BEARER_LENGTH: Int = 7 | ||
|
||
|
|
||
| fun generateJws(authentication: Authentication): String { | ||
| val email = authentication.name | ||
|
|
@@ -30,17 +35,47 @@ class JwtUtils private constructor() { | |
| .signWith(KEY_PAIR.private) | ||
| .compact() | ||
| } | ||
| // fun validateJws(jwsString: String): Jws<Claims>? { | ||
|
|
||
| fun getEmailFromJws(jwsString: String): String { | ||
| return Jwts.parserBuilder() | ||
| .setSigningKey(KEY_PAIR.public) | ||
| .build() | ||
| .parseClaimsJws(jwsString) | ||
| .body["email"] | ||
| .toString() | ||
| } | ||
|
|
||
| fun validateJws(jwsString: String): Jws<Claims>? { | ||
| // val jws: Jws<Claims> | ||
| // try { | ||
| // jws = Jwts.parserBuilder() | ||
| // .setSigningKey(KEY_PAIR.public) | ||
| // .build() | ||
| // .parseClaimsJws(jwsString) | ||
| // } catch (e: JwtException) { | ||
| // } catch (e: ExpiredJwtException) { | ||
| // return null | ||
| // } catch (e: SignatureException) { | ||
| // return null | ||
| // } catch (e: MalformedJwtException) { | ||
| // return null | ||
| // } catch (e: UnsupportedJwtException) { | ||
| // return null | ||
| // } catch (e: IllegalArgumentException) { | ||
| // return null | ||
| // } catch (e: UnsupportedEncodingException) { | ||
| // return null | ||
| // } | ||
| // return jws | ||
| // } | ||
| return Jwts.parserBuilder() | ||
| .setSigningKey(KEY_PAIR.public) | ||
| .build() | ||
| .parseClaimsJws(jwsString) | ||
| } | ||
|
|
||
| fun parseJws(token: String): String? { | ||
| if (StringUtils.hasText(token) and token.startsWith("Bearer ")) { | ||
| return token.substring(BEARER_LENGTH, token.length) | ||
| } | ||
| return null | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,8 @@ import io.restassured.module.kotlin.extensions.Then | |
| import io.restassured.module.kotlin.extensions.When | ||
| import io.restassured.specification.RequestSpecification | ||
| import kr.flab.wiki.TAG_TEST_E2E | ||
| import kr.flab.wiki.app.api.user.request.LoginRequest | ||
| import kr.flab.wiki.app.components.authentication.LoginUserService | ||
| import kr.flab.wiki.app.components.authentication.UserAuthentication | ||
| import kr.flab.wiki.core.testlib.user.Users | ||
| import org.mockito.MockitoAnnotations | ||
|
|
@@ -48,14 +50,18 @@ class LoginWithSpringSecurityAndJwtTest { | |
| * 로그인 이후 | ||
| * 1. 발행한 JWT 토큰을 매 요청마다 검증한다. (스프링 시큐리티에서 인증이 필요하다고 정한 요청에만) | ||
| * 2. JWT 가 유효하면 정상적으로 요청을 수행하도록 한다. | ||
| * 3. JWT 가 유효하지 않으면 Http 403을 반환한다. | ||
| * 3. JWT 가 유효하지 않으면 Http 401을 반환한다. | ||
| * | ||
| */ | ||
|
|
||
| private val faker = Faker.instance() | ||
|
|
||
| @Inject | ||
| private lateinit var objectMapper: ObjectMapper | ||
|
|
||
| @Inject | ||
| private lateinit var loginUserService: LoginUserService | ||
|
|
||
| @MockBean | ||
| private lateinit var userAuthentication: UserAuthentication | ||
|
|
||
|
|
@@ -169,19 +175,50 @@ class LoginWithSpringSecurityAndJwtTest { | |
| } | ||
| } | ||
|
|
||
| @Disabled | ||
|
|
||
| @Nested | ||
| inner class `인증이 필요한 API 는` { | ||
|
|
||
| //테스트할 타겟 api uri | ||
| //private val targetApiUri = springApi.getRandomAuthenticatedApiPattern() | ||
| private val targetApiUri = "/test" | ||
|
|
||
| @Nested | ||
| inner class `매 요청마다 토큰을 검증해서` { | ||
| inner class `매 요청마다 헤더의 토큰을 확인해서` { | ||
|
|
||
| private lateinit var email: String | ||
| private lateinit var password: String | ||
|
|
||
| @BeforeEach | ||
| fun setup() { | ||
| email = faker.internet().emailAddress() | ||
| password = faker.internet().password() | ||
| } | ||
|
|
||
| @Nested | ||
| inner class 유효하면 { | ||
|
|
||
| @BeforeEach | ||
| fun setup() { | ||
| `when`(userAuthentication.authenticateUser(email, password)) | ||
| .thenReturn(Users.randomUser(emailAddress = email)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `API 를 정상수행한다`() { | ||
| //정상적으로 로그인한 정보 | ||
| val loginResponse = loginUserService.login(LoginRequest(email, password)) | ||
|
||
|
|
||
| Given { | ||
| spec(requestSpecification) | ||
| //정상적으로 로그인한 정보에서 추출한 token을 헤더에 담아서 요청한다. | ||
| header("Authorization", "Bearer ${loginResponse?.token}") | ||
| } When { | ||
| get(targetApiUri) | ||
| } Then { | ||
| statusCode(200) | ||
| log().all() | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
@@ -190,8 +227,15 @@ class LoginWithSpringSecurityAndJwtTest { | |
| inner class `유효하지 않으면` { | ||
|
|
||
| @Test | ||
| fun `403을 반환한다`() { | ||
|
|
||
| fun `401을 반환한다`() { | ||
|
||
| Given { | ||
| spec(requestSpecification) | ||
| } When { | ||
| get(targetApiUri) | ||
| } Then { | ||
| statusCode(401) | ||
| log().all() | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package kr.flab.wiki.app.testlib | ||
|
|
||
| import org.springframework.context.ApplicationContext | ||
| import org.springframework.stereotype.Component | ||
| import org.springframework.web.method.HandlerMethod | ||
| import org.springframework.web.servlet.mvc.method.RequestMappingInfo | ||
| import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping | ||
| import java.util.* | ||
| import javax.inject.Inject | ||
|
|
||
|
|
||
| @Component | ||
| class SpringApi { | ||
|
||
|
|
||
| @Inject | ||
| private lateinit var context: ApplicationContext | ||
|
|
||
| private val random: Random = Random() | ||
|
|
||
| fun getAuthenticatedApiPatternList(): List<String> { | ||
| val requestMappingHandlerMapping = context | ||
| .getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping::class.java) | ||
| val map = requestMappingHandlerMapping | ||
| .handlerMethods | ||
|
|
||
| return map.filter { (key: RequestMappingInfo?, value: HandlerMethod?) -> | ||
| !key.patternValues.contains("/login") && !key.patternValues.contains("/error") | ||
| }.map { (key: RequestMappingInfo?, value: HandlerMethod?) -> | ||
| key.patternValues.toString() | ||
| } | ||
| } | ||
|
|
||
| fun getRandomAuthenticatedApiPattern(): String { | ||
| val patternList = getAuthenticatedApiPatternList() | ||
| return patternList[random.nextInt(patternList.size)] | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
서버 실행할때 jackson-kotlin 어쩌고 경고 뜨진 않던가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
서버 시작을 아직 안해봤는데, 확인해보고 말씀드리겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
음.. 로컬에 wsl, docker, mysql 세팅하고 서버실행해봤는데, 말씀하신 경고는 뜨지 않고 있습니다.