이 글은 '뱀귤'님의 Spring Security+JWT 예제 코드 블로그를 보고 개인 공부 목적으로 작성한 글입니다. 이 글에 나오는 모든 코드와 로직은 제것이 아님을 밝힙니다! 전반적인 로직을 확인하려면 계속 보시는 것을 추천드리고 코드가 궁금하시다면 뱀귤님의 블로그를 추천드립니다~!! 저는 깃허브에서 코드를 받아와 디버깅하며 공부했습니다 :)
회원 정보 멤버변수
id(GeneratedValue)
: 식별 idemail
: 사용자 이메일 password
: 사용자 비밀번호authority(Enum)
: 권한ROLE_USER
ROLE_ADMIN
사용자, 관리자 두 권한이 정의되어 있다메서드
Builder 생성자
: 이메일, 패스워드, 권한DB에 저장할 RefreshToken(↔ AccessToken)
멤버변수
key
: 키value
: 값Builder
생성자: 키, 값메서드
updateValue(토큰)
: 매개변수로 전달 받은 token으로 갱신DB에 있는 회원정보 조회
Optional<Member> findByEmail(이메일)
: 이메일로 회원 정보 조회boolean existsByEmail(이메일)
: 중복 가입 방지를 위해 해당 이메일로 회원 등록 유무 조회식별 id, 이메일로 회원 정보 조회
반환형: MemberResponseDto
MemberResponseDto findMemberInfoById(회원 식별 id)
: 식별 id로 회원 찾기 (Long 타입)MemberResponseDto findMemberInfoByEmail(이메일)
: 이메일로 회원 찾기회원 정보 요청 시 보내줄 응답
email
: 회원 이메일public static MemberResponseDto of(Member 엔티티)
: Member을 DTO로 만들어주기 GET 요청 받았을때 회원 정보 응답 전송
"api/member"로 RequestMapping
@GetMapping("/me")
public ResponseEntity<MemberResponseDto> findMemberInfoById()
: 내 정보 가져옴@GetMapping("/{email}")
public ResponseEntity<MemberResponseDto> findMemberInfoByEmail(url로 받은 이메일)
: Get으로 받은 email로 회원 정보 응답 전송✅ 다른 Controller에서 API 요청이 들어오면 필터에서 Access Token
을 복호화 해서 유저 정보를 꺼내 SecurityContext
라는 곳에 저장한다
✅ SecurityContext
에 저장된 유저 정보는 전역이라서 어디서든 꺼낼 수 있다
✅ SecurityUtil.getCurrentMemberId()
로 꺼낼 수 있다
회원 정보 조회하는 요청을 했을때 실행됨 "/api/member/me"
/*뱀귤님의 설명인데 이해가 잘 되지 않지만 필요한듯 하여ㅜㅜ*/
//SecurityContext에 회원 정보가 저장되는 시점
//요청이 왔을때 JwtFilter의 doFilter에서 저장
Long getCurrentMemberId()
: SpringContext에서 회원 식별 id(이메일 아님)만 반환authentication.getName()
으로 id를 반환해준다생략
멤버변수
AUTHORITIES_KEY = "auth"
: 인가 키BEARER_TYPE = "Bearer"
: 인증 타입으로 JWT 토큰을 사용한다는 뜻ACCESS_TOKEN_EXPIRE_TIME = 30분
: Access Token 유효 시간REFRESH_TOKEN_EXPIRE_TIME = 7일
: Refresh Token 유효 시간key
: 토큰 만드는 과정 중에서 암호화할때 사용될 비밀 키메서드
TokenProvider(비밀키)
: 생성자application.yml
에 정의해놓은 jwt.secret값으로 암호화 키값 생성@Value("${jwt.secret}") String secretKey
로 가져오면 된다 jwt:
secret: c3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXR1dG9yaWFsLWppd29vbi1zcHJpbmctYm9vdC1zZWN1cml0eS1qd3QtdHV0b3JpYWwK
TokenDto generateTokenDto(Authentication authentication)
: Authentication으로 회원 정보를 받아 Access&Refresh Token 생성
Access Token
Jwts.builder()
로 토큰 생성Refresh Token
Jwts.builder()
로 토큰 생성리턴(TokenDto)
Authentication getAuthentication(Access 토큰)
: 토큰을 복호화해서 정보를 꺼낸다
accessToken
에서 클레임을 가져온다. => 권한이 있는지 체크해준다Claims parseClaims(Access 토큰)
: 토큰에서 클레임을 가져와 반환boolean validateToken(토큰)
: 토큰 정보 검증AUTHORIZATION_HEADER = "Authorization"
: 헤더에 Authorization있는지 확인할때BEARER_PREFIX = "Bearer "
: 인증 타입이 Bearer Token인지 확인할때String resolveToken(요청)
: 요청 헤더에서 토큰 정보를 꺼내온다getHeader()
메서드로 토큰 정보를 꺼내온다StringUtils.hasText()
로 토큰 null 체크를 하고void doFilterInternal(요청, 응답, 필터체인)
: 토큰 인증 정보를 SecurityContext에 저장한다resolveToken()
로 토큰 정보를 꺼내온다validateToken()
메서드로 유효성 검사 TokenProvider의 getAuthentication()
로 Authentication 얻어오고doFilter()
로 필터링설명
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>
의 구현체 http.addFilterBefore()
로 직접 만든 JwtFilter를 Spring Security Filter 앞에 추가한다메서드
void configure(HttpSecurity http)
: TokenProvider를 주입 받아서 JwtFilter를 통해 Security 로직에 필터 등록인증 정보 없을때, 유효한 자격증명을 제공하지 않고 접근하려 할때 => 401 에러 반환
(Entry Point: 접근, 입구 지점)
메서드
void commence(요청, 응답, AuthenticationException)
: 매개변수로 에러를 받고, 응답에 HttpServletResponse.SC_UNAUTHORIZED 에러를 추가한다인증 정보는 있지만 접근 권한은 없을 때 403 에러 반환
메서드
void handle(요청, 응답, AccessDeniedException)
직접 만들었던 클래스를 사용한다! 리마인딩해보자
TokenProvider
: 토큰 생성 & 토큰에서 정보 꺼내기JwtAuthenticationEntryPoint
: 인증 정보 없을때 오류JwtAccessDeniedHandler
: 접근 권한 없을때 오류메서드
PasswordEncoder passwordEncoder()
: 비밀번호 암호화WebSecurityCustomizer webSecurityCustomizer()
: DB 관련 API 모두 무시(h2 DB라서)SecurityFilterChain filterChain(HttpSecurity http)
: 스프링 시큐리티 필터 체인 실행맨마지막 메서드인 필터체인 메서드를 더 자세하게 알아보자!
1. CSRF(사이트 간 요청 위조 공격) 설정 Disable
http.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint) //클래스 변수로 가져옴
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.headers() //응답 헤더
.frameOptions() //커스텀 허용
.sameOrigin() //같은 곳에서 오는 요청들을 허락
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
참고: stateless는 값이 변하지 않는 상태, statefull은 값이 변할 수 있는 상태를 말한다. 토큰은 값이 변하지 않지만 세션은 들어있는 값이 변한다는 것을 떠올리면 된다.
.and()
.authorizeRequests() //인가 요청
.antMatchers("/auth/**").permitAll() // "/auth/**" api 요청들은 권한 상관없이 모두 허용
.anyRequest().authenticated() // 나머지 API 는 전부 인증 필요
.and()
.apply(new JwtSecurityConfig(tokenProvider));
리마인딩: 직접 만든 JwtFilter를 Security Fillter 앞에 추가하는 클래스이다
SecurityContext에서 유저 정보를 꺼낸다
메서드
Long getCurrentMemberId()
: 현재 context에 들어있는 회원 id 꺼내기보통 토큰이 만료될때 Redis로 자동 삭제 처리를 하는데 RDB로 구현할 때에는 배치(일괄) 작업으로 만료된 토큰들을 삭제해주어야 한다.
이때 생성, 수정 시간 컬럼이 필요하다
key
: 회원 고유 id 값value
: Refresh TokenfindByKey()
: 회원 ID 값으로 토큰을 가져온다드디어!!!!!ㅠㅠ JWT 관련 설정이 끝이 났다..
이제 로그인 요청이 들어왔을 때 인증 처리 후 JWT 토큰을 발급하는 과정을 살펴보자
toMember(비밀번호 Encoder)
: 이 DTO를 Member Entity로 만들어준다toAuthentication()
: 이메일과 패스워드로 UsernamePasswordAuthenticationToken을 만들어준다회원가입 form을 받아 authService의 signup()
메서드에 보내준다
로그인 form을 받아 authService의 login()
메서드에 보내준다
Access&Refresh Token이 있는 DTO를 받아 authService의 reissue()
메서드에 보내준다
회원가입 form을 받는다
toMember()
메서드로 비밀번호 암호화해서 저장@Transactional
어노테이션로그인 form을 받는다
로직 순서
1. 아직 인증 받지 못한 ID와 PW로 UsernamePasswordAuthenticationToken(토큰)을 생성한다
AuthenticationManager의 authenticate() 메서드로 검증을 받는다
2-1. Authentication 객체에 회원 정보를 넣는다
2-2. 회원 식별 id가 이때 이메일, 비밀번호와 함께 들어간다
인증된 정보로 JWT 토큰을 생성한다
3-1. TokenProvider의 generateTokenDto()
메서드
3-2. Access Token, Refresh Token 모두 새로 생성한다
Refresh Token을 DB에 저장한다
권한, Access 토큰, Refresh 토큰, Access 토큰 만료 시간을 반환한다
5-1. TokenDto를 반환한다
DTO로 Access Token, Refresh Token을 받는다
로직 순서
1. Refresh Token이 만료되었는지 검증한다
1-1. 유효하다면 다음 단계!
Access Token에서 회원 식별 id를 가져온다
2-1. TokenProvider의 getAuthentication() 메서드 - 여기서 토큰 복호화해줌
2번에서 가져온 회원 id로 저장소의 Refresh Token 값을 가져온다
DTO로 받은 Refresh Token과 저장소에 있던 Refresh Token이 일치하는지 검사한다
4-1. 일치한다면 다음 단계!
Access 토큰, Refresh 토큰 모두 새로 생성해준다
Refresh 토큰의 재사용을 막기 위해 저장소의 Refresh Token을 업데이트해준다.
권한, Access 토큰, Refresh 토큰, Access 토큰 만료 시간을 반환한다
7-1. TokenDto를 반환한다
UserDetails와 Authentication의 비밀번호를 비교하고 검증한다
메서드
UserDetails createUserDetails(Member 엔티티)
: Member Entity를 UserDetails로 만든다UserDetails loadUserByUsername(이메일)
: 이메일 받아서 UserDetails 반환createUserDetails()
로 UserDetails 객체로 만들어준다지금 스프링 부트로 쇼핑몰 프로젝트를 구현하고 있는데 Spring Security를 써봐야겠다 싶어서 구글링하게 되었다. 그러던중 괜찮은 예제 코드를 찾게 되었는데 알고보니 Spring Security에서 한 단계 더 나아간.. JWT+Spring Security였다ㅋㅋㅋㅋㅋㅋ (나는 Spring Security와 초면이었는데 말이다!!!!!) 그치만 JWT 공부하는게 너무 재밌었어서 끝까지 재밌게 끝냈다~~
내가 직접 다 구현했으면 정말 좋았겠지만 프로젝트 마감기한 때문에ㅠㅠㅠ 아쉽게 다른 분의 로직으로 개발을 진행해야겠다.. 그래서 최대한 많이 이해하려고 많이 노력했당......ㅜㅜ 내가 이해하려고 정리한 글이라서 다른 분들께 도움이 될지는 모르겠지만....ㅎㅎㅎㅎㅎㅎㅎㅎ 그래도 짱 수고많았다! 이 글 보고있는 분들도 모두모두 화이팅!!!
피드백이나 틀린 내용 있다면 언제든지 댓글 남겨주세요