Backend
Frontend
Database & Infra
Project
codesche’s blog
/
Project
/
miniblog
/
miniblog
Backend
Frontend
Database & Infra
Project
codesche’s blog
/
Project
/
miniblog
/
miniblog
Share
Backend
Frontend
Database & Infra
Project
miniblog
miniblog
Search
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
OAuth 인증 적용 위한 테스트 코드 작성
•
테스트코드 수정 - 굵은 글씨로 표시한 부분 참고
핵심 요약
1.
쿠키란 사용자가 어떠한 웹사이트를 방문했을 때, 그 웹사이트가 사용하는 서버를 통해 로컬에 저장되는 작은 데이터이다. 쿠키는 키와 값으로 이루어져 있으며 만료 기간, 도메인 등의 정보를 가지고 있다.
2.
OAuth는 제3의 서비스에게 계정을 맡기는 방식이다. OAuth 에서 정보를 취득하는 방법은
권한 부여 코드 승인 타입,
암시적 승인 타입, 리소스 소유자 암호 자격증명 승인 타입, 클라이언트 자격증명 승인 타입으로 나뉜다.
3.
OAuth 방식 중 권한 부여 코드 승인 타입은 클라이언트가 리소스에 접근하는 데 사용되며, 권한에 접근할 수 있는 코드를 제공받으면 리소스 오너에 대한 액세스 토큰을 제공받게 된다.
18일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
스프링 시큐리티로 OAuth2를 구현하고 적용하기
•
스프링 시큐리티를 사용하여 OAuth2 구현 시작
•
가장 먼저 쿠키 관리 클래스를 구현하고, OAuth2에서 제공받은 인증 객체로 사용자 정보를 가져오는 역할을 하는 서비스를 구현하기
•
기존 ‘시큐리티 설정하기’ 에서 구현했던 WebSecurityConfig 클래스 대신 사용할 OAuth2 설정 파일을 구현하기
•
마지막에는 직접 테스트할 수 있도록 뷰를 구성
의존성 추가
•
build.gradle
쿠키 관리 클래스 구현
OAuth2 인증 플로우를 구현하며 쿠키를 사용할 일이 생기는데 그때마다 쿠키를 생성하고 삭제하는 로직을 추가하면 불편하므로 유틸리티를 사용할 쿠키 관리 클래스를 미리 구현한다.
17일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
토큰 발급받기
•
구글 클라우드 콘솔에 접속하여 구글 클라우드 사용 동의 절차를 진행한 이후에 새 프로젝트 생성하기
•
프로젝트 이름 지은 다음에 만들기 버튼 클릭
16일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
OAuth란?
OAuth는 제3의 서비스에 계정 관리를 맡기는 방식이다. 흔히 볼 수 있는 네이버로 로그인하기, 구글로 로그인하기 같은 것이다.
•
리소스 오너(resource owner)
◦
자신의 정보를 사용하도록 인증 서버에 허가하는 주체이다. 서비스를 이용하는 사용자가 리소스 오너에 해당된다.
•
리소스 서버(resource server)
◦
리소스 오너의 정보를 가지며, 리소스 오너의 정보를 보호하는 주체를 의미한다. 네이버, 구글, 페이스북이 리소스 서버에 해당한다.
•
인증 서버(authorization server)
◦
클라이언트에게 리소스 오너의 정보에 접근할 수 있는 토큰을 발급하는 역할을 하는 애플리케이션을 의미한다.
•
클라이언트 애플리케이션(client application)
◦
인증 서버에게 인증을 받고 리소스 오너의 리소스를 사용하는 주체를 의미한다. 지금 만들고 있는 서비스가 이에 해당된다.
OAuth를 사용하면 인증 서버에서 발급받은 토큰을 사용하여 리소스 서버에 리소스 오너의 정보를 요청하고 응답받아 사용할 수 있다. 그런데 클라이언트는 어떻게 리소스 오너의 정보를 취득할 수 있을까? 리소스 오너 정보를 취득할 수 있는 방법은 4가지가 있다.
•
리소스 오너 정보를 취득하는 4가지 방법
◦
권한 부여 코드 승인 타입(authorization code grant type)
▪
OAuth 2.0에서 가장 잘 알려진 인증 방법이다. 클라이언트가 리소스에 접근하는 데 사용하며, 권한에 접근할 수 있는 코드와 리소스 오너에 대한 액세스 토큰을 발급받는 방식이다.
◦
암시적 승인 타입(implicit grant type)
▪
서버가 없는 자바스크립트 웹 애플리케이션 클라이언트에서 주로 사용하는 방법이다. 클라이언트가 요청을 보내면 리소스 오너의 인증 과정 이외에는 권한 코드 교환 등의 별다른 인증 과정을 거치지 않고 액세스 토큰을 제공받는 방식이다.
◦
리소스 소유자 암호 자격증명 승인 타입(resource owner password credentials)
▪
클라이언트의 패스워드를 이용해서 액세스 토큰에 대한 사용자의 자격 증명을 교환하는 방식이다.
◦
클라이언트 자격증명 승인 타입(client credentials grant)
▪
클라이언트가 컨텍스트 외부에서 액세스 토큰을 얻어 특정 리소스에 접근을 요청할 때 사용하는 방식이다.
15일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
JWT 서비스 구현 - 테스트 코드
•
테스트 코드 작성
◦
generateToken() - 토큰을 생성하는 메서드를 테스트하는 메서드
@DisplayName
(
"generateToken(): 유저 정보와 만료 기간을 전달해 토큰을 만들 수 있다."
)
@Test
void
generateToken
(
)
{
// given
User
testUser
=
userRepository
.
save
(
User
.
builder
(
)
.
email
(
"user@gmail.com"
)
.
password
(
"test"
)
.
build
(
)
)
;
// when
String
token
=
tokenProvider
.
generateToken
(
testUser
,
Duration
.
ofDays
(
14
)
)
;
// then
Long
userId
=
Jwts
.
parser
(
)
.
setSigningKey
(
jwtProperties
.
getSecretkey
(
)
)
.
parseClaimsJws
(
token
)
.
getBody
(
)
.
get
(
"id"
,
Long
.
class
)
;
assertThat
(
userId
)
.
isEqualTo
(
testUser
.
getId
(
)
)
;
}
Java
복사
given
토큰에 유저 정보를 추가하기 위한 테스트 유저를 만든다.
when
토큰 제공자의 generateToken() 메서드를 호출해 토큰을 만든다.
then
jjwt 라이브러리를 사용해 토큰을 복호화한다. 토큰을 만들 때 클레임으로 넣어둔 id값이 given절에서 만든 유저ID와 동일한지 확인한다.
◦
validToken_invalidToken()
// validToken() 검증 테스트
@DisplayName
(
"validToken(): 만료된 토큰인 때에 유효성 검증에 실패한다."
)
@Test
void
vaildToken_invalidToken
(
)
{
// given
String
token
=
JwtFactory
.
builder
(
)
.
expiration
(
new
Date
(
new
Date
(
)
.
getTime
(
)
-
Duration
.
ofDays
(
7
)
.
toMillis
(
)
)
)
.
build
(
)
.
createToken
(
jwtProperties
)
;
// when
boolean
result
=
tokenProvider
.
vaildToken
(
token
)
;
// then
assertThat
(
result
)
.
isFalse
(
)
;
}
Java
복사
validToken_invalidToken() 메서드는 토큰이 유효한 토큰인지 검증하는 메서드인 validToken() 메서드를 테스트하는 메서드이다. 검증 실패를 확인하는 validToken_invalidToken() 메서드와 검증 성공을 확인하는 validToken_validToken() 메서드로 구성하였다.
given
jjwt 라이브러리를 사용하여 토큰을 생성한다. 이때 만료 시간은 1970년 1월 1일부터 현재 시간을 밀리초 단위로 치환한 값(new Date().getTime())에 1000을 빼, 이미 만료된 토큰으로 생성한다.
when
토큰 제공자의 validToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결과값을 반환받는다.
then
반환값이 false(유효한 토큰이 아님)인 것을 확인한다.
◦
valid_validToken()
@DisplayName
(
"validToken(): 유효한 토큰인 때에 유효성 검증에 성공한다."
)
@Test
void
validToken_validToken
(
)
{
// given
String
token
=
JwtFactory
.
withDefaultValues
(
)
.
createToken
(
jwtProperties
)
;
// when
boolean
result
=
tokenProvider
.
vaildToken
(
token
)
;
// then
assertThat
(
result
)
.
isTrue
(
)
;
}
Java
복사
given
jjwt 라이브러리를 사용하여 토큰을 생성한다. 만료 시간은 현재 시간으로부터 14일 뒤로, 만료되지 않은 토큰으로 생성한다.
when
토큰 제공자의 validToken() 메서드를 호출해 유효한 토큰인지 검증한 뒤 결과값을 반환받는다.
then
반환값이 true(유효한 토큰임)인 것을 확인한다.
◦
getAuthentication() - 토큰을 전달받아 인증 정보를 담은 객체 Authentication을 반환하는 메서드인 getAuthentication()을 테스트한다.
// getAuthentication() 검증 테스트
@DisplayName
(
"getAuthentication(): 토큰 기반으로 인증 정보를 가져올 수 있다."
)
@Test
void
getAuthentication
(
)
{
// given
String
userEmail
=
"user@email.com"
;
String
token
=
JwtFactory
.
builder
(
)
.
subject
(
userEmail
)
.
build
(
)
.
createToken
(
jwtProperties
)
;
// when
Authentication
authentication
=
tokenProvider
.
getAuthentication
(
token
)
;
// then
assertThat
(
(
(
UserDetails
)
authentication
.
getPrincipal
(
)
)
.
getUsername
(
)
)
.
isEqualTo
(
userEmail
)
;
}
Java
복사
given
jjwt 라이브러리를 사용하여 토큰을 생성한다. 이때 토큰의 제목인 subject는 “user@email.com”라는 값을 사용한다.
when
토큰 제공자의 getAuthentication() 메서드를 호출해 인증 객체를 반환받는다.
then
반환받은 인증 객체의 유저 이름을 가져와 given절에서 설정한 subject값인 “user@email.com”과 같은지 확인한다.
◦
getUserId() 메서드는 토큰 기반으로 유저 ID를 가져오는 메서드를 테스트하는 메서드이다. 토큰을 프로퍼티즈 파일에 저장한 비밀값으로 복호화한 뒤 클레임을 가져오는 private 메서드인 getClaims()를 호출해서 클레임 정보를 반환받아 클레임에서 id 키로 저장된 값을 가져와 반환한다.
// getUserId() 검증 테스트
@DisplayName
(
"getUserId(): 토큰으로 유저 ID를 가져올 수 있다."
)
@Test
void
getUserId
(
)
{
// given
Long
userId
=
1L
;
String
token
=
JwtFactory
.
builder
(
)
.
claims
(
Map
.
of
(
"id"
,
userId
)
)
.
build
(
)
.
createToken
(
jwtProperties
)
;
// when
Long
userIdByToken
=
tokenProvider
.
getUserId
(
token
)
;
// then
assertThat
(
userIdByToken
)
.
isEqualTo
(
userId
)
;
}
Java
복사
given
jjwt 라이브러리를 사용해 토큰을 생성한다. 이때 클레임을 추가한다. 키는 “id”, 값은 1이라는 유저 ID 이다.
when
토큰 제공자의 getUserId() 메서드를 호출해 유저 ID를 반환받는다.
then
반환받은 유저 ID가 given 절에서 설정한 유저 ID값인 1과 같은지 확인한다.
◦
테스트 결과 확인
14일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
JWT 서비스 구현
•
의존성 추가
•
토큰 제공자 추가
•
해당 값들을 변수로 접근하는 데 사용할 JwtProperties 작성
•
토큰을 생성하고 올바른 토큰인지 유효성 검사를 하기 위해 토큰에서 필요한 정보 가져오는 클래스 작성 (TokenProvider.java)
13일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
회원가입, 로그인 뷰 작성하기
•
뷰 컨트롤러 구현
•
뷰 작성 - login.html, signup.html
로그아웃 메서드 작성
•
UserApiController 수정
12일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
회원가입 구현하기
•
사용자 정보 담고 있는 객체 작성
•
AddUserRequest 객체를 인수로 받는 회원 정보 추가 메서드 작성 - 서비스
•
회원가입 폼에서 회원 가입 요청을 받으면 서비스 메서드 사용해 사용자 저장 후 로그인 페이지로 이동하는 signup 메서드 작성
11일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
시큐리티 설정하기
•
requestMathcers(): 특정 요청과 일치하는 url에 대한 액세스를 설정한다.
•
permitAll(): 누구나 접근이 가능하게 설정한다.
•
anyRequest(): 위에서 설정한 url 이외의 요청에 대해 설정한다.
•
loginPage(): 로그인 페이지 경로 설정한다.
•
defaultSuccessUrl(): 로그인이 완료되었을 때 이동할 경로를 설정한다.
•
logoutSuccessURl(): 로그아웃이 완료되었을 때 이동할 경로를 설정한다.
•
invalidateHttpSession(): 로그아웃 이후에 세션을 전체 삭제할지 여부를 결정한다.
•
userDetailService: 사용자 정보를 가져올 서비스를 설정한다. 이때 설정하는 서비스 클래스는 반드시 UserDetailsService를 상속받은 클래스여야 한다.
•
passwrodEncoder(): 비밀번호를 암호화하기 위한 인코더를 설정한다.
10일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
회원 도메인 만들기
•
의존성 추가하기
•
회원 엔티티 만들기
•
domain 패키지에 UserDetail 클래스를 상속하는 User 클래스 생성
•
UserRepository 생성
9일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
생성 기능 마무리
•
article.js 에 코드 작성
•
article.js 파일을 열어 [등록] 버튼을 누르면 입력 칸에 있는 데이터를 가져와 게시글 생성 API에 글 생성 관련 요청을 보내준다.
•
id가 create-btn인 엘리먼트를 찾아 그 엘리먼트에서 클릭 이벤트 발생 시 id가 title, content인 엘리먼트의 값을 가져와 fetch() 메서드를 통해 생성 API로 /api/articles/ POST 요청을 보낸다.
•
articleList.html 파일 수정
•
결과 확인
8일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
트러블슈팅
•
스프링 부트 3.2 버전 매개변수 이름 인식 문제
◦
링크:
https://www.notion.so/3-2-e1dfc8a765ca4f1a88c36b7d991e3039?pvs=4
◦
스프링 부트 3.2부터 매개변수 이름을 인식하지 못하는 문제가 있는데 자바 컴파일러에 -parameter 옵션을 넣어주어야 애노테이션의 이름을 생략할 수 있다.
◦
해당 케이스 같은 경우 빌드 시 Gradle이 아닌 IntelliJ IDEA 로 설정되어 있어서 발생한 문제였다.
◦
Gradle을 사용해서 빌드하고 실행한다. 이렇게 하면 parameter 추가할 필요 없다.
◦
만약에 IntelliJ IDEA 로 유지를 하고 사용해야 한다면 애너테이션 옆에 -parameter 옵션을 추가해주어야 한다.
◦
스프링 부트 3.2 전까지는 바이트코드를 파싱해서 매개변수 이름을 추론하려고 시도했으나 스프링 부트 3.2부터는 이런 시도를 하지 않는다. 그래서 에러가 발생한 것이다.
•
에러 해결 후 정상적으로 글 상세 화면이 잘 나오는 것을 확인할 수 있다.
7일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
블로그 글 목록 뷰 구현
•
요청을 받아 사용자에게 뷰를 보여주려면 뷰 컨트롤러가 필요함
•
뷰 컨트롤러 메서드는 뷰의 이름을 반환하고, 모델 객체에 값을 담음
•
뷰에 전달하기 위한 객체 생성
•
/article GET 요청을 처리할 코드 작성 - 블로그 글 전체 리스트를 담은 뷰를 반환
◦
addAttribute() 메서드를 사용해 모델에 값을 저장
◦
“articles” 키에 블로그 글들을, 즉, 글 리스트를 저장
•
HTML 뷰 만들기
◦
th:each로 “articles” 키에 담긴 데이터 개수만큼 반복
◦
th:text 는 반복 대상 객체의 id, “text” 를 출력
6일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
타임리프
•
뷰 작성 - example.html
•
출력 결과
5일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
타임리프
•
타임리프는 템플릿 엔진이다.
◦
템플릿 엔진: 스프링 서버에서 데이터를 받아 우리가 보는 웹 페이지, 즉 HTML 상에 그 데이터를 넣어 보여주는 도구이다.
•
문법
표현식
설명
${…}
변수의 값 표현식
#{…}
속성 파일 값 표현식
@{…}
URL 표현식
*{…}
선택한 변수의 표현식. th:object에서 선택한 객체에 접근
th:text
텍스트를 표현할 때 사용 - th:text=${person.name}
th:each
컬렉션을 반복할 때 사용 - th:each=”person:${persons}”
th:if
조건이 true인 때만 표시 - th:if=”${person.age} ≥ 20”
th:unless
조건이 false인 때만 표시 - th:unless=”${person.age}≥20”
th:href
이동 경로 - th:href=”@{/person(id=${person.id})}”
th:with
변수값으로 지정 - th:with=”name = ${person.name}”
th:object
선택한 객체로 지정 - th:object=”${person}”
•
타임리프 의존성 추가
•
타임리프 문법 익히기용 컨트롤러 작성
◦
모델(Model) 객체는 뷰, 즉, HTML 쪽으로 값을 넘겨주는 객체이다.
◦
모델 객체는 따로 생성할 필요 없이 코드처럼 인자로 선언하기만 하면 스프링이 알아서 만들어주므로 편리하게 사용할 수 있다.
4일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
블로그 글 삭제 API 구현
•
BlogService.java
•
BlogApiController.java
•
삭제 결과 확인
◦
postman 테스트 (왼쪽 - DELETE 요청 보냄, 오른쪽 - DELETE 요청 보낸 후)
◦
결과 확인 (왼쪽 - 변경 전, 오른쪽 - 변경 후)
•
테스트 코드 작성 - BlogApiControllerTest.java
@DisplayName
(
"deleteArticle: 블로그 글 삭제에 성공한다."
)
@Test
public
void
deleteArticle
(
)
throws
Exception
{
// given
final
String
url
=
"/api/articles/{id}"
;
final
String
title
=
"title"
;
final
String
content
=
"content"
;
Article
savedArticle
=
blogRepository
.
save
(
Article
.
builder
(
)
.
title
(
title
)
.
content
(
content
)
.
build
(
)
)
;
// when
mockMvc
.
perform
(
delete
(
url
,
savedArticle
.
getId
(
)
)
)
.
andExpect
(
status
(
)
.
isOk
(
)
)
;
// then
List
<
Article
>
articles
=
blogRepository
.
findAll
(
)
;
assertThat
(
articles
)
.
isEmpty
(
)
;
Java
복사
블로그 글 수정 API 구현
3일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
블로그의 모든 글 조회하는 API 구현
•
BlogService.java
응답을 위한 DTO 생성
•
ArticleResponse.java
전체 글을 조회한 뒤 반환
2일차
본 프로젝트는 “스프링부트 3 백엔드 개발자 되기” 서적을 참고하여 진행하였음
엔티티 구성
리포지토리 만들기
API 구현
•
DTO (Data Transfer Object)
1일차