이상하게 강의를 보는 게 꼬여버렸습니다.
CustomLoginFilter (영상 제목이 이상한데...) 에서는 바로 전 강의에서 만들었던 커스텀 로그인 필터에서 직접 StudentAuthToken 과 TeacherAuthToken 을 만들게 바꾸고, 그걸 처리하는 StudentAuthProcessor 와 TeacherAuthProcessor 에서도 해당 토큰들을 필터링 & 처리하도록 바꿉니다. 이것도 언제 실습을 좀 해봐야 하는데
다음장인 05. Basic 토큰 인증에서는 BasicAuthFilter 를 사용한 Basic 인증을 진행합니다.
Basic 인증
HTTP 인증 방식에는 여러가지가 있습니다. 그 중 BasicAuthenticationFilter 는 Basic 방법(scheme)을 사용합니다. 이전에 벙 찐 상태로 멋모르고 사용해봤던 UsernamePasswordAuthentication 은 UI가 있을 때 쓰지만, BasicAuthenticationFilter 는 SPA 처럼 별도의 UI가 없는 경우에 사용할 수 "있습니다".
그 외 주로 사용하는 인증방식에는 Bearer 토큰 방식이 있는데, 이건 OAuth2 나 JWT 계열에서 쓰인다고 합니다. 암호화된 토큰 정보이구요, OAuth2 API 로 SNS용 앱 같은 걸 만들때 Bearer 토큰을 헤더에 넣어서 테스트해보신 적이 다들 있으시죠? 나만 있나?
- MDN 문서: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
- Spring Security 문서: https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html
Basic 인증은 HTTP 헤더에 "Authorization: Basic (아이디:패스워드).base64()" 라는 헤더를 넣어서 보내는 걸로 이루어집니다. 예를 들자면 "Authorization: Basic TXlBZG1pbjpNeVBhc3N3b3Jk" 같은 형태겠죠. TXlBZG1pbjpNeVBhc3N3b3Jk 의 내용은 브라우저 개발자도구에서 atob("TXlBZG1pbjpNeVBhc3N3b3Jk") 를 입력하시면 바로 나옵니다.
구체적인 handoff? 라고 부르나요? HTTP 입장에서의 인증 절차는 MDN 문서와 Spring Security 문서에 나와있고, Spring Security 문서에는 Spring Security 쪽에서 어떻게 동작하는지도 나와있습니다.
Basic 메소드는 ID / Password 를 진짜로 때려넣어서 보내는만큼 https 통신이 아니라면 내용이 그대로 보이므로 위험합니다. 그래서 인증이 된 이후부터는 세션으로 관리합니다. 반면 OAuth 나 JWT 계열처럼 암호화 토큰을 보내는 경우 세션을 사용하지 않고 매번 토큰을 실어서 보냅니다. 이 경우에도 https 가 아니면 토큰을 탈취해서 사용할 수 있지만 토큰은 구현한 거에 따라 만료시킬 수 있는 경우도 있으니 그나마 낫겠죠? 그냥 전부 https 를 쓰세요.
실습
이번에야말로 기본적인 실습을 해보겠습니다. 강의에서 제공되는 프로젝트를 클론하기는 싫고 해서 새로 만듦.
thymeleaf 만 빼먹어서 따로 추가
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
설정
@EnableWebSecurity
open class SecurityConf: WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
// super.configure(http)
http
// 기본으로 CSRF 방지 기능이 켜져있어서, POST 에 CSRF 토큰이 필요하므로 꺼버림.
// 다음 강의? 다다음 강의? 에서 CSRF 가 필요한 페이지와 같이 운영하는 방법 소개
.csrf().disable()
// 모든 경로에서 인증이 필요하도록 설정
.authorizeRequests().anyRequest().authenticated()
.and()
// 인증 방법은 httpBasic
.httpBasic()
}
override fun configure(auth: AuthenticationManagerBuilder) {
// super.configure(auth)
auth.inMemoryAuthentication()
.withUser(
// WARNING: This method is considered unsafe for production and is only intended for sample applications.
User.withDefaultPasswordEncoder()
// 이 밑은 deprecated 나 경고가 없습니다.
.username("user1")
.password("1111")
.roles("USER")
.build()
)
}
}
부모 클래스(HttpSecurity)로 돌아오는 빌더 패턴이 인상적입니다. 이전에 자세히 안 봤었는데, WebSecurityConfigurereAdapter 는 기본 설정이 켜져있는 설정인가봅니다. 순서가 중요하므로 csrf 끄는 건 앞에 두는 게 맞겠죠? 아닌가 이건 관계가 없나? :mollu:
테스트용 유저도 추가.
컨트롤러
@RestController
class GreetingController {
@GetMapping("/greeting")
fun greeting() = "Hello!"
@PostMapping("/greeting")
fun greeting(@RequestBody name: String) = "Hello, ${name}!"
}
네. 이게 답니다.
테스트
이번엔 테스트 코드에서 인증이 잘 되는지 확인해봅니다.
이번 강의의 테스트에서 새로 배운 내용들이 좀 있는데요.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
internal class GreetingControllerTest {
@LocalServerPort
var port: Int = 0
val greetingEndpoint
get() = "http://localhost:${port}/greeting"
val restTemplate = RestTemplate()
@Test
fun noAuthGreet_exception401() {
val exception: HttpClientErrorException = assertThrows(HttpClientErrorException::class.java) {
restTemplate.getForObject(greetingEndpoint, String::class.java)
}
assertEquals(401, exception.statusCode.value())
}
@Test
fun manualAuthGreet_returnsGreet() {
val headers = HttpHeaders()
headers.add(HttpHeaders.AUTHORIZATION, "Basic ${Base64.getEncoder().encodeToString("user1:1111".toByteArray())}" )
val entity = HttpEntity("", headers)
val result = restTemplate.exchange(greetingEndpoint, HttpMethod.GET, entity, String::class.java)
assertTrue(result.statusCode.is2xxSuccessful)
assertEquals("Hello!", result.body)
}
val testRestTemplate = TestRestTemplate("user1", "1111")
@Test
fun testRestTemplateAuthGreet_returnsGreet() {
val respEnt = testRestTemplate.getForEntity(greetingEndpoint, String::class.java)
assertEquals("Hello!", respEnt.body)
}
@Test
fun postAuthGreet_returnsGreet() {
val respEnt = testRestTemplate.postForEntity(greetingEndpoint, "Octocat", String::class.java)
assertEquals("Hello, Octocat!", respEnt.body)
}
}
테스트 시 랜덤 포트 설정
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
그리고 그 포트는 받아올 수 있음
@LocalServerPort
var port: Int = 0
TestRestTemplate 을 쓰면 ID / 암호 인증을 자동으로 해줌
val testRestTemplate = TestRestTemplate("user1", "1111")
정도입니다. 나머지는 까먹어서 결국 강의 화면을 보고 따라했다능...
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
그 외 상세사항은 다음에 이어집니다.
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04-2 시큐리티' 카테고리의 다른 글
Security 02-06: DB에 계정 넣기 - 패스트캠퍼스 챌린지 50일차 (0) | 2022.03.14 |
---|---|
Security 02-05: Basic Auth (2) - 패스트캠퍼스 챌린지 49일차 (0) | 2022.03.13 |
Security 02-04: Authentication 메커니즘 - 패스트캠퍼스 챌린지 47일차 (0) | 2022.03.11 |
Security 02: 전체 구조 - 패스트캠퍼스 챌린지 46일차 (0) | 2022.03.10 |
Security 01: 개요 및 맛보기 - 패스트캠퍼스 챌린지 42일차 (0) | 2022.03.06 |