지난 포스트에서 JWT를 알아보았다. 이번에는 JWT를 이용해 보안 API를 구현하고 JWT 생성, 유효성 검증까지 알아본다.
프로젝트 환경 설정
start.spring.io
사진 외 추가한 의존성
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation group: 'com.auth0', name: 'java-jwt', version: '4.0.0'
프로젝트 생성에 대한 환경 설정이다.
빌드 : Gradle Spring Boot : 2.7.12 JDK : 11
의존성 목록
Spring Web : SpringMVC 및 내장 tomcat 서버, API 구현
Lombok : Getter, Setter, 생성자를 어노테이션을 통한 구현
Validation : 입력 및 출력에 대한 유효성 검증
Oracle Driver : Oracle DB 연결
Mybatis Framework : Mybatis 사용하여 DB와 통신
Spring Secuity : 보안 인증 및 암호화
jjwt-api : JWT 생성 및 검증
jjwt-impl : JWT 라이브러리의 구현체를 포함한다. JWT 검증 및 생성에 사용
jjwt-jackson : JWT의 payload를 JSON 직렬화, 역직렬화 처리
java-jwt : JWT 생성 및 검증 라이브러리
# Oracle DB 연결 설정
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:XE
spring.datasource.username=C##APITEST
spring.datasource.password=APITEST
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#
# MyBatis mapper 경로설정
mybatis.mapper-locations=mapper/*.xml
#
# MyBatis 클래스 별칭 설정
mybatis.type-aliases-package=com.newbie.training
#
# 카멜케이스-스네이크케이스 자동 매핑
mybatis.configuration.map-underscore-to-camel-case=true
패키지 구조
패키지 구조는 MVC 패턴에 따라 설계하고 Spring Security를 사용하여 구현하였다.
테스트로 사용될 테이블 스키마는 이렇게 생성했다.
Spring Security를 이용한 유저 저장
코드 설명
user ClassSecurityConfig ClassUserController Class
먼저 Controller에서 registerUser에 대한 Post 요청을 받으면 DTO인 user로 값을 받고 user에서 전달된 비밀번호를 SecurityConfig에 Bean으로 등록한 BCryptPasswordEncoder를 통해 암호화한다. 비밀번호가 암호화된 user 객체를 Service의 registerUser 메소드를 통해 전달하고 Service의 리턴값에 따라 "성공" 또는 "실패"를 반환한다. > POST /registerUser > userPw를 BCryptPasswordEncoder를 통해 암호화 후 user 객체에 저장 > userService.registerUser로 전달
UserServiceLogic
Service 에서는 전달된 user객체를 Repository의 registerUser메소드를 통해 전달하고 리턴되는 값에 따라 성공 또는 실패를 Controller로 리턴한다. > Controller에서 전달받은 user 객체를 Repository.registerUser로 전달 > 결과에 따라 성공 또는 실패를 Controller로 리턴
UserRepositoryLogicUserMapper
Repository는 받은 user 객체를 파라미터로 사용하여 UserMapper의 registerUser라는 insert문을 실행시키고 성공여부에 따라 1 또는 0을 반환한다. > INSERT문을 실행 > 결과를 service로 리턴
실행
Postman을 통해 실행해본 결과 리턴값은 "성공", DB에도 저장된 값을 통해 정상작동을 확인 할 수 있었다.
Spring Security를 사용하여 인증 후 인증 값을 통해 JWT 생성
코드 설명
UserController
Controller에서 login으로 요청을 받는다. 로그인 요청이 들어오면 인증객체인 PrincipalDetail을 사용하여 유저의 정보를 가져오고 passwordEncoder.matches를 통해 비밀번호 확인 후 인증에 성공시 JWT를 생성, 쿠키에 저장 후 http 응답과 같이 리턴한다. > POST /login 요청 > PrincipalDetailService로 userId를 통해 user 객체를 요청 > passwordEncoder를 통해 userPw의 일치 여부 확인 > 일치시 JwtProvider.createAccessToken으로 JWT 생성 > JWT를 쿠키로 저장, Http응답에 추가 후 리턴
PrincipalDetails
PrincipalDetails는 UserDetails 인터페이스의 구현체 이고 내부에는 user 객체를 저장하고 출력할 수 있다.
PrincipalDetailService
UserDetailsService의 구현체이고 loadUserByUsername 메소드를 통해 repository의 findByUserId 메소드로 userId를 전달하고 결과값에 따라 PrincipalDetails 객체 리턴 또는예외를 발생시킨다. > userRepository로 userId를 가지고 user 객체 요청 > 객체 존재 여부에 따라 예외 발생 또는 PrincipalDetails 객체 리턴
JwtProperties
jwt 정보나 비밀키를 저장하는 인터페이스 비밀키 같은 경우 Key를 사용하여 서버 재실행시 비밀키가 변경된다.
JwtProvider
전달받은 PrincipalDetails 객체를 사용하여 JWT를 생성하고 암호화 후 리턴한다. > JWT 생성 > sub를 username으로 작성 > 만료시간을 JwtProperties의 따라 30분으로 작성 > id 클레임을 userId로 작성 > 서명의 알고리즘을 HS512로 암호화 후 리턴
실행
로그인 성공과 함께 설정한대로 쿠키에 정상적으로 JWT가 저장된 것을 확인 할 수 있었다.
JWT를 사용한 인증 및 데이터 요청
Student classSecurityConfig class
클라이언트로부터 요청이 들어오면 SecurityConfig에 설정한 addFilterBefore 메소드를 통해 JwtAuthenticationFilter를 통해 검증을 진행하게 된다.
JwtAuthenticationFilter / 모든 요청에 인증을 하기 위해 OncePerRequestFilter 를 상속한다.JwtAuthenticationFilter
모든 요청에 대해 한번씩 인증하기 위해 OncePerRequestFilter를 상속하고 있다. doFilterInteranal 메소드를 통해 검증을 시작한다. extractTokenFromHeader 메소드를 통해 header에 요청으로 들어온 JWT를 꺼내고 저장, 확인을 한다. 확인 후 요청을 isLoginRequest 메소드를 통해 POST : /login 요청이라면 인증하지 않고 진행하고 아니라면 JWT의 null과 형식을 확인 후 handleAuthenticationError 메소드를 통해 인증 오류에 대한 처리를 진행하거나 이어서 진행한다. JWT의 시작값 Bearer를 제거 후 JWT에 대한 검증을 시작한다. 검증 후 성공여부에 따라 이어서 요청을 진행하거나 검증실패에 따른 handleAuthenticationError 메소드를 통해 인증 오류로 응답한다. > 모든 요청에 대응하여 JwtAuthenticationFilter 가 실행 > doFilterInteranal 메소드 실행 > extractTokenFromHeader 에서 JWT를 header에서 가져와 null 체크 및 형식 확인 > isLoginRequest 을 통해 POST /login 요청에 대한 여부 확인(로그인 요청에 대한 인증 예외를 두기 위함) > token의 null 체크 및 형식 확인 > token의 시작값 Bearer 제거 후 JwtProvider.validateToken으로 토큰 검증 > 검증 성공시 요청에 대한 처리를 이어서 진행한다. / 실패시 handleAuthenticationError 를 통해 인증 오류와 함께 응답한다.
JwtProvider
jwtProvider의 validateToken을 통해 검증을 시작한다. parseJwtToken 메소드를 통해 먼저 jwt의 비밀키와 저장된 비밀키가 동일한지 먼저 확인 후 동일하다면 클레임을 추출 한다. jwt 시간 저장을 UTC 세계표준시로 저장되었기 때문에 한국 표준시로 적용하여 만료 시간에 대한 변환을 하고 만료시간을 설정 한뒤 클레임을 반환한다. 다시 validateToken에서 만료시간을 확인 한뒤 결과에 따라 true 또는 flase를 반환한다. > validateToken을 통한 검증 시작 > 전달 받은 token 값을 parseJwtToken으로 토큰 파싱 요청 > parseJwtToken 을 통해 token의 검증 시작 > Jwts.parser를 통해 생성 > setSigningKey를 통해 비밀키 셋팅 > parseClaimsJws 를 통해 비밀키를 통한 파싱 > getBody로 클레임 반환 > 만료 시간 비교를 위한 시간 설정 > 클레임 리턴 > 리턴받은 Claims의 토큰 만료시간 확인 후 결과에 따라 true / flase 리턴
이어서 위 검증이 끝난 후 Controller의 GET : findNameById 요청을 이어서 진행한다.
UserController
받은 user객체를 Service에 파라미터로 전달하며 값을 요청한다. Service에서 받은 값에 따라 객체 또는 값이 없습니다를 리턴한다. > GET /findNameById 요청 > user객체를 사용하여 Servce.findNameById 수행 > 전달받은 객체값에 따른 결과 반환
UserServiceLogic
Repository로 user 객체를 전달 하며 값을 요청한다.
UserRepositoryLogicUserMapper
UserMapper의 select : findStudentById 를 user 객체를 파라미터로 사용하여 실행한다.