이 글은 spring security와 jwt에 대한 개념을 설명하는 글이 아닌 동작 방식을 이해하는데 도움이 되는 글이다. 구현을 당장 해야 한다면 다른 블로그의 글을 보며 복붙을 한 후 읽어보면 동작 방식을 알기 쉬울 것이다. Univey 개발을 진행할 때 코드를 긁어와서 하다 보니 전체적 흐름이 궁금하였기에 이 글을 작성하여 기록을 남긴다.
정의와 이론 부분은 아래 블로그를 읽어보면 도움이 많이 된다.
https://wildeveloperetrain.tistory.com/50
Spring Security 용어 정리
1. UsernamePasswordAuthenticationToken : jwt가 우리 서비스의 사용자 정보를 갖는 토큰인 것과 같이 사용자 정보를 들고 있는 토큰이다. 이 토큰으로 AunthenticationManager에서 사용자 인증을 진행한다. UserDetails라는 객체에 DB에 있는 사용자의 정보를 담고 이를 유저의 로그인 정보와 비교하여 사용자를 인증한다. 성공한다면 인증 정보를 갖고 있는 Authentication객체를 반환한다.
2. SecurityContext, SecurityContextHolder : 이름에서 알 수 있듯이 SecurityContextHolder는 SecurityContext 객체를 보관하는 wrapper 클래스이다. SecurityContext는 위에 설명했던 Authentication 객체를 보관한다. 생성된 Authentication을 필요할 때 SecurityContext에서 꺼내 쓴다.
전체적인 흐름
1. 사용자가 로그인하면 서버에서 jwt를 발급한다.
2. 사용자는 다음 요청부터 jwt를 갖고 (헤더에 담아서 서버로 요청을 보내게 한다)
3. 서버는 jwt를 확인하여 유효한 사용자인지 검증한다.(이 과정에서 spring security가 관여한다.)
이 흐름을 설정 흐름, 로그인, 로그인 이후(jwt를 갖고) 요청할때로 나누어 설명해 보겠다.
Setting
1. jwt를 사용해야 하기 때문에 jwt관련 설정을 해준다.
2. 사용자가 요청할 때 같이 보낸 jwt를 검증하는 JwtAuthroizationFilter를 작성한다.
2. jwtAuthorizationFilter를 UsernamePasswordAuthenticationFilter앞에 동작하게 한다.(spring security에서 제공하는 UsernamePasswordAuthenticationFilter는 아이디, 비밀번호로 사용자를 검증하는데 우리는 jwt를 쓸 것이기에 앞에 설정해 준다.)
3. securityConfig에 인증, 인가 관련 설정을 한다. login, 회원 가입과 같은 jwt가 없어도 접근 가능해야 하는(로그인 전) 리소스를 설정하거나, admin 설정등을 해준다.
4. customUserDetailsService 작성. security에서는 UserDetails라는 객체를 (회원을 의미하는 엔티티와 다름) 생성해 검증을 진행한다.
로그인
로그인을 할 때는 jwt로 검증을 할 수가 없다. 로그인을 해야 jwt 토큰이 발급되기 때문이다. 그렇기 때문에 설정으로 로그인을 진행하는 endpoint는 jwtAuthorizationFilter에서 검증을 진행하지 않도록 설정하고 수동을 인증을 해주어야 한다.
지금부터 수동으로 사용자를 인증하는 방법이다.
1. 로그인 시 입력한 사용자 정보로 UsernamePasswordAuthenticationToken을 만든다.
2. 1에서 만든 토큰으로 Authentication을 만든다. 이때 authenticationManager가 사용자 인증을 진행해 주는데 로그인 정보와 DB에 있는 사용자 정보를 비교하기 위해 UserDetailsService에 있는 loadUserByUsername 메서드를 내부적으로 '알아서' 호출해서 사용자를 인증한 후 성공하면(db에 있는 유저의 아이디, 비밀번호와 입력받은 로그인 정보가 일치하면) Authentication 객체를 반환한다. loadUserByUsername을 보면 db에서 사용자의 정보를 가져와
로그를 통해 순서를 알아볼 수 있었는데 "loadUserByUsername 1" 이후 authenticationManager에서 loadUserByUsername을 호출하였기에 "loadUserByUsername 실행되는 거야?"가 찍히는 것을 볼 수 있다.
설명을 조금 더 추가하자면 loadUserByUsername에서 createUser메서드를 호출하는데 여기서 db에 있는 사용자의 아이디와 비밀번호를 담고 있는 userdetails가 생성되고 authenticationManager가 이것과 위에서 생성한 UsernamePasswordAuthenticationToken와 비교하여 사용자 인증을 알아서 해준다.
3. 사용자 인증이 끝나고 Authentication이 성공적으로 반환되었다면 이를 SecurityContextHolder에 저장한다. 인증 객체를 저장을 해줘야 다른 security filter들이나 security에서 인증 객체가 필요할 때 꺼내쓸 수 있다. 그 후 authentication객체를 바탕으로 jwt를 만들어 클라이언트에게 전달한다.
로그인 이후의 요청(jwt 유효성 검사)
사실 이 이후로는 크게 어렵지 않다! 개발을 할 때는 securityContext 이런 용어가 낯설어서 어려웠는데 위의 과정을 했다면 밑에는 수월하다. 로그인할 때는 사용자의 아이디(본인은 이메일로 진행)와 비밀번호를 수동으로 검증해 주었고 로그인 이후의 요청은 클라이언트가 헤더에 jwt를 갖고 요청을 하기에 작성해 놓은 jwtAuthorizationFilter에서 jwt만 검증해 주면 된다.
1. 요청이 오면 jwtAuthorizationFilter에 들어온다.
2. jwt가 유효한지 검증한다.
3. 검증이 성공하면 로그인 때와 같이 Authentication 객체를 만든다. 여기서 User principal을 만들 때 비밀 번호가 빈칸인 이유는 우리는 아이디와 비밀번호가 아닌 jwt로 사용자를 구별하기에 비밀번호는 크게 의미가 없기 때문이다. jwt가 우리가 만들어 준 비밀번호라고 생각하면 조금 이해될 수 있다!
4. Authentication 객체를 만드는 데 성공했으면 역시 이를 SecurityContextHolder에 저장해 주면 된다. 저장을 하면 다른 security filter에서 이것을 꺼내 알아서 통과한다.
정리
UsernamePasswordAuthenticationToken을 만들어 인증에 성공해서 Authentication 객체를 만들고 securityContextHolder에 넣어서 다른 인증들을 할 때도 꺼내 쓰게 만들자! 하지만 JWT는 로그인을 해야 받을 수 있기 때문에 로그인 과정에서는 수동으로 인증을 해주자.
개발할 때 너무 막막했는데 한 줄씩 개념을 찾으면서 뜯어보니 나름 이해가 되었다!
틀린 부분이 존재할 수 있으니 피드백 매우 환영입니다 :]
'My project > Univey' 카테고리의 다른 글
java boolean? Boolean? (feat. boolean 값에 false만 들어오는 이유) (0) | 2024.02.14 |
---|---|
잇타 대학생연합 IT동아리 it’s time 4기 활동 간단 후기, 회고(Feat. 팀 불사조 - Univey) (3) | 2024.01.29 |