Spring Boot JWT Tutorial
JWT 소개, 프로젝트 생성
스프링 부트를 이용해서 JWT을 사용하는 방법
JWT는 RFC 7519 웹 표준으로 지정되어 있고 JSON 객체 사용 -> 토큰 자체에 정보를 저장하는 웹 토큰
Header Payload Signature 3개의 부분으로 구성
헤더 - 시그니쳐를 해싱하기 위한 알고리즘 정보를 담아둠
페이로드 - 서버와 클라이언트가 주고받는, 시스템에서 실제로 사용될 정보를 담음
시그니쳐 - 토큰의 유효성을 검사하기 위한 문자열
장점 - 중앙의 인증서버, 데이터 스토어에 대한 의존성 없음, 시스템 수평 확장 유리
URL, 쿠키, 헤더 어디든 모드 사용 가능
단점 - 페이로드의 정보가 많아지면 트래픽의 크기가 커짐
토큰이 서버에 저장되지 않아서 서버에서 클라이언트의 토큰을 조작할 수 없음
추가해야 할 디펜던시
Spring web, Spring Security, Spring Data JPA, Lombok, H2, Validation
인텔리제이 Setting > Annotation Processeors > Enable annotation processing CHECK!
Security 설정, Data 설정
config 패키지 만들기
SecurityConfig 클래스 생성
@EnableWebSecurity 어노테이션 : 기본적인 Web 보안을 활성화 하겠다는 의미
WebSecurityConfigurerAdapter 를 extends
WebSecurityConfigurerAdapter의 configure 메소드를 오버라이드
.authorizeRequests : HttpServeletRequest를 사용하는 요청들에 대한 접근 제한을 설정
.antMatcher("/api/...") : /api/... 에 대한 요청은 인증 없이 접근을 허용하겠다는 의미
.anyRequest().authenticated() : 나머지 요청들은 모두 인증되어야 한다는 의미
application.properties 파일을 Refactor를 이용하여 application.yml로 파일명 변경 ( 편의 상 변경된 것 )
h2: console: enable : h2 DB 사용, 메모리에 데이터를 저장 > Datasource 설정 추가
jpa : 기본 설정
create-drop의 의미 : SessionFactory가 시작될 때 Drop, Create, Alter / 종료될때 Drop 을 진행함
properties: hibernate: format_sql: show_sql : 콘솔창에서 실행되는 sql들을 보기 좋게 보여주는 설정 추가
logging: level: me.silvernine: DEBUG : 로깅 레벨 디버그로 설정
entity 패키지 생성 > User, Authority 클래스 생성
User 클래스 어노테이션
@Entity : 데이터베이스의 테이블과 1:1 매핑되는 객체
@Table : 테이블 명을 지정해 주는 어노테이션
@Getter, @Setter, @Builder, @...Constructor : 롬복 어노테이션으로 Get, Set, Builder, Constructor 관련 코드를 자동으로 생성해 줌 / 실무에서는 고려해서 사용해야 하니 주의!
@ManyToMany, @JoinTable : User 객체와 권한 객체의 다대다 관계를 일대다, 다대일 관계의 조인 테이블로 정의했다는 의미 > JPA 부분에서 더 알아봐야 할 것
create-drop를 사용하여 SpringBoot 서버가 시작될 때마다 테이블들을 새로 만들기 때문에 편의를 위해서 서버를 시작할 때마다 Data를 자동으로 DB에 넣어주는 기능을 활용해야 함
리소스 폴더 > data.sql 파일 만들기
INSERT INTO USER (USER_ID, USERNAME, PASSWORD, NICKNAME, ACTIVATED) VALUES (1, 'admin', '$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO/Vxd5DOi', 'admin', 1);
INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_USER');
INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_ADMIN');
INSERT INTO USER_AUTHORITY (USER_ID, AUTHORITY_NAME) values (1, 'ROLE_USER');
INSERT INTO USER_AUTHORITY (USER_ID, AUTHORITY_NAME) values (1, 'ROLE_ADMIN');
서버가 시작될 때마다 실행할 쿼리문
Security 설정 추가 > h2-console 접근을 원활하게 하기 위해
SecurityConfig 클래스 > h2-console 하위 모든 요청들과 파비콘 관련 요청은 Spring Security 로직을 수행하지 않도록
configure 메소드를 오버라이드하여 내용을 추가
JWT 코드, Security 설정 추가
application.yml > JWT 설정 추가
jwt:
header: Authorization
//HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
//echo 'silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret'|base64
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
token-vaildity-in-seconds : token의 만료시간: 86400초
build.gradle > JWT 관련 라이브러리 추가
-------------------------------------------------------------
JWT 패키지 추가 > 토큰의 생성, 토큰의 유효성 검증 등을 담당할 TokenProvider 클래스 추가
1)InitializingBean을 2)implements 해서 3)afterPropertiesSet을 4)오버라이드 한 이유 : 5)Bean이 생성이 되고 의존성 주입을 받은 후에 6)secret값을 7)Base64 Decode해서 8)ket변수에 할당 하기 위함
Authentication 객체의 권한 정보를 이용해서 토큰을 생성하는 createToken 메소드 추가
Authentication authentication 파라미터를 받아서 권한들, application.yml에서 설정했던 만료시간을 설정해주고 JWT 토큰 생성 후 리턴
Token에 담겨있는 정보를 이용해 Authentication 객체를 리턴하는 메소드 생성
1)token을 파라미터로 받은 뒤 2)토큰을 이용해서 3)클레임을 만들고 4)권한 정보를 빼낸 후 5)이를 이용해 6)유저 객체를 생성 > 7)유저객체와 토큰, 권한 정보를 이용하여 8)Authentication 객체 리턴
Token의 파라미터를 받아서 유효성 검증을 수행하는 validateToken 메소드 추가
1)토큰을 파라미터로 받아서 2)파싱을 해보고 나오는 3)익셉션들을 캐치. 문제가 있으면 false, 정상이면 true
-------------------------------------------------------------
-------------------------------------------------------------
JWT를 위한 커스텀 필터를 만들기 위해 JwtFilter클래스 생성
JWT 패키지 > JwtFilter 클래스 생성
1)GenericFilterBean을 extends > 2)doFilter Override > 실제 필터링 로직은 doFilter 내부에 작성 3) JwtFilter는 tokenProvider를 주입받음
doFilter는 토큰의 인증정보를 SecurityContext에 저장하는 역할 수행
Request Header에서 토큰 정보를 꺼내오기 위한 resolveToken 메소드 추가
doFilter 내용 작성
1) request에서 토큰을 받아서 2)방금 만든 유효성 검증을 하기 위한 메소드를 통과하고 3)토큰이 정상적이면 토큰에서 authentication객체를 받아와서 4)SecurityContext에 셋(저장)해 줌
-------------------------------------------------------------
-------------------------------------------------------------
위 TokenProvider, JwtFilter를 SecurityConfig에 적용할 때 사용할 JwtSecurityConfig 클래스 추가
JWT 패키지 > JwtSecurityConfig 클래스
1)SecurityConfigurerAdapter를 extends하고 2)TokenProvider를 주입받음 3)configure 메소드를 오버라이드 해서 4)방금 만든 JwtFilter를 통해 5)Security로직에 필터 등록
-------------------------------------------------------------
-------------------------------------------------------------
유효한 자격증명을 제공하지 않고 접근하려 할 때 401 Unauthorized 에러를 리턴할 JwtAuthenticationEntryPoint 클래스 생성
JWT 패키지 > JwtAuthenticationEntryPoint 클래스
1)AuthenticationEntryPoint를 implements 하고 2)401 unauthorized 에러를 3)send하는 4)commence 메소드를 오버라이드
-------------------------------------------------------------
-------------------------------------------------------------
필요한 권한이 존재하지 않는 경우 403 Forbidden 에러를 리턴하기 위해 JwtAccessDeniedHandler 클래스 생성
JWT 패키지 > JwtAccessDeniedHandler 클래스
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//필요한 권한이 없이 접근하려 할때 403
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
-------------------------------------------------------------
-------------------------------------------------------------
jwt패키지에서 만든 5개의 클래스를 SecurityConfig에 추가
...코드스니펫 딸깍..?
@EnableGlobalMethodSecurity : @PreAuthorize 어노테이션을 메소드 단위로 추가하기 위해서 적용됨
SecurityConfig는 TokenProvider, JwtAuthenticationEntryPoint, JwtAccessDeniedHandler 주입
PasswordEncoder는 BCryptPasswordEncoder를 사용
configure 메소드를 오버라이드 한 부분에서 추가된 내용
1)
.csrf().disable() : 토큰 방식을 사용하기 때문에 csrf 설정을 disable 해 줌
2)
.exceptionHandling()을 할 때 .authenticationEntryPoint()와 .accessDeniedHandler() 에 우리가 만든 클래스들을 추가해줌
3)
.and
.headers()
.frameOptions()
.sameOrigin() -> H2-console을 위한 설정
4)
.and()
.sessionManagement()
//세션을 사용하지 않기 때문에 세션 설정을 STATELESS로 설정
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
5)
로그인 API, 회원가입 API는 토큰이 없는 상태에서 요청이 들어오기 때문에 모두 permitAll 설정
.andMatchers("/api/authenticate").permitAll()
.andMatchers("/api/signup").permitAll()
6)
마지막으로 JwtFilter를 addFilterBefore로 등록했던 JwtSecurityConfig 클래스 적용
.and()
.apply(new JwtSecurityConfig(tokenProvider));
-------------------------------------------------------------
DTO, Repository, 로그인
-------------------------------------------------------------
-외부와의 통신에 사용할 DTO 클래스 생성
-Repository 관련 코드 생성
-로그인 API, 관련 로직 생성
-------------------------------------------------------------
-------------------------------------------------------------
dto 패키지 생성 > LoginDto 클래스 생성
1) @Vaild 관련 어노테이션 추가
2) username, password 2개의 필드를 가지고 있음
-------------------------------------------------------------
-------------------------------------------------------------
Token 정보를 Response할 때 사용할 TokenDto 생성
dto 패키지 > TokenDto 클래스 생성
-------------------------------------------------------------
-------------------------------------------------------------
회원가입시에 사용할 UserDto 생성
dto 패키지 > UserDto 클래스 생성
-------------------------------------------------------------
-------------------------------------------------------------
Repository들을 만들기 위해 repository 패키지 생성
UserRepository 인터페이스 생성
간단 설명!
JpaRepository를 extends하면 findAll, save등의 메소드를 기본적으로 사용할 수 있습니다
findOneWithAuthoritiesByUsername 메소드는 username을 기준으로 User 정보를 가져올 때 권한 정보도 같이 가져옵니다
@EntityGraph : 쿼리가 수행이 될 때 Lazy조회가 아니고 Eager조회로 authoriries 정보를 같이 가져오게 됩니다
-------------------------------------------------------------
-------------------------------------------------------------
커스텀유저디테일서비스 클래스를 생성하기 위해 service 패키지 생성
service 패키지 > CustomUserDetailService 클래스 생성
...코드스니펫 딸깍...