그동안 배운 것들을 적용해 기존 만들었던 CRUD 게시판 처음부터 다시 만들어보기
<변경점>
1.Spring Security 적용 (JWT방식 커스텀 필터 사용)
2.Controller에 @Valid 어노테이션 적용
3.JPA 연관관계 변경 (양방향 -> 단방향)
4.Custom Exception으로 처리하기
5.ERD, API 작성
<1. Spring Security 적용>
Spring Security : https://imslo.tistory.com/70
Spring Security
인증 / 인가 절차를 Spring에서 관리해주는 기능 Document : https://docs.spring.io/spring-security/reference/servlet/getting-started.html Hello Spring Security :: Spring Security You can now run the Spring Boot application by using the Maven P
imslo.tistory.com
(1). 기존에는 로그인 요청이 오면 ID와 비밀번호를 확인해서 JWT 토큰을 발급해줬다.
==> Spring Security에서 Username과 password를 확인해서 jwt 토큰으로 응답하게 하기 가능한지
==> usernameandpassword 필터를 상속받아서 구현 가능하다. 일단 Service에서 jwt토큰 반환하는식으로 구현하고, 그 다음 진행해보기
(2). 이후 다른 요청에 대해 JWT 요청을 일일히 확인했다.
==> 컨트롤러로 가기 전에 Spring Security에서 JWT토큰을 확인하는 필터를 추가 (커스텀 필터)
(1) 먼저, Spring security에서 기본으로 제공하는 UsernamePasswordAuthenticationFilter에서 User를 찾는 부분을 커스텀

이 필터는 로그인 요청이 들어오면 ID와 PW를 받아와서 unauthenticated 상태의 Token을 생성해 AuthenticationManager에게 던진다.
Spring Security에서는 AuthenticationManager interface의 구현체는 ProviderManager가 있다


ProviderManager는 AuthenticationProvider를 리스트로 가진다. Provider는 실제로 인증 로직이 이루어지는 곳이며, Manager가 이를 List로 가지는 이유는 여러가지 인증 방식이 있을 수 있기 때문이다. 따라서 각 Provider 안에는 들어온 토큰이 내가 인증 가능한 타입인지 확인하는 메소드가 있다. 이를 확인한 후, 가능한 Provider를 찾으면 authenticate 메소드를 호출하여 인증을 진행한다.



각 Provider는 검증을 해야하기 때문에 유저정보를 가져와야한다. 따라서 로그인 시도 시 입력한 Principal(아이디)를 통해 유저정보를 가져오는 UserDetailsService를 필드로 가진다.

정리하자면 로그인요청 -> UsernamePasswordAuthenticationFilter(토큰생성) -> ProviderManager -> 알맞은 Provider (유저 찾아와서 Credential(비밀번호) 검증) -> 성공 시 성공에 해당하는 토큰 생성하여 return
이런 Spring 기본 인증로직을 활용하되, 내 DB에서 User를 찾아와 검증하게끔 하고싶다면 DaoAuthenticationUser의 UserDetailsService가 내가 구현한 UserDetailsService를 가지게 하고, 그 안에 내 repository에서 유저를 찾아오게끔 loadByUserName 메소드를 오버라이드 해주면 된다.
UserDetailsServiceImpl : loadByUsername 메소드 오버라이드 -> User 찾아서 UserDetails를 상속받은 클래스 반환
UserDetailsImpl : User를 받아서 UserDetails에 맞는 클래스로 변환해줌
<구현>


UserDetails는 getAuthorities, getPassword, isNonExpired 등 필수 override해야하는 메소드들 있음.

추가로, 비밀번호를 Encode 하는 방식을 검증하는 부분과 DB에 저장하는 부분 통일해야함. PasswordEncoder를 Bean으로 등록해서 SpringSecurity에게 알려주고, 회원가입 시 DB에 저장할 때도 같은 Encoder 사용해준다.


커스텀 로그인페이지를 사용한다면 로그인 요청이 어떤 URL로 들어오는지 써줘야한다. 그래야 Username~~필터가 이 요청을 Process할 수 있다.
이렇게 되면 UsernamePasswordAuthenticationFilter가 로그인요청을 처리해줄 것이다. 하지만 로그인이 확인되면 JWT토큰을 발급하는 로직이 구현되지 않았다. 로그인요청이 Controller에 오기 전에 처리되기 때문에 요청을 받아서 Controller나 Service에서 발급해줄 수는 없다. 따라서 Spring Security가 authentication에 성공했을 때 하는 행동을 정의해주어야 한다.
이는 SuccessHandler에서 가능하다

onAuthenticationSuccess를 Override 해서 커스텀 LoginSuccessHandler를 구현하고, 로그인 시 이 핸들러를 사용하라고 알려주면 된다. 나는 로그인 성공 시 response에 토큰을 담아서 보내주되, 이후에는 기존 SuccessHandler의 기능을 모두 사용하고싶다. 따라서 다음과 같이 구현했다.

Spring Security의 디폴트 SuccessHandler는 SavedRequestAwareAuthenticationSuccessHandler() 이다.
이 default handler는 이전 요청으로의 리디렉션, 인증 성공/실패 로깅 등 여러가지 기능을 수행해주기 때문에 내 커스텀 기능(jwt 토큰을 헤더에 추가)을 수행한 후, default handler를 호출하게 하였다.
문제는, default핸들러를 호출하여 인증이 성공 시 이전 요청으로 redirection 하게 되면, 클라이언트가 헤더에 담긴 JWT 토큰을 받아볼 수 없다는 점이다.


해결방법이 보이지 않아 일단은 defaulthandler를 호출하는 것을 주석처리 해놓았다. 나중에 해결하겠다 ..
결과↓↓↓↓↓↓↓

토큰을 헤더에 잘 받아오긴 한다 (임시)
(2). JWT 필터 만들기
로그인 된 유저가 헤더에 JWT 토큰을 추가해서 요청을 보내면, 이를 검증할 필터가 필요하다.
Jwt필터는 OncePerRequestFilter를 상속받아 구현한다. 서버에 로그인 세션을 유지할 것이 아니기 때문에 요청마다 헤더를 확인하고, 토큰이 있다면 토큰을 검증해야하기 때문이다. 만약 헤더에 토큰이 없다면 이후 필터가 실행되도록 해야한다.

OncePerRequestFilter를 보면, doFilter가 동작했을 때 해당 Filter가 동작해야하는지를 확인한 후, 동작해야한다면
doFilterInternal을 실행한다.

내 커스텀필터는 doFilterInternal에 검증 로직을 override해야한다.

dofilterinternal을 override해주고, 로직을 작성. hasBearer는 토큰이 있는지 확인하는 메소드

토큰을 담기로 정해진 헤더이름으로 getHeader를 날렸을 때 있으면 가져와서 Bearer_prefix로 시작하는지 확인하고, 없으면 Empty라고 하므로 위와 같이 작성하였다. 헤더에 토큰이 없으면 filterchain의 다음 필터를 실행하도록 dofilter해주고 return;

token을 받아서 User를 내 repository에서 username으로 찾아서(기존 userDetailsService 활용 ) UserDetail을 반환하는 메소드 작성

토큰이 없을 때, 토큰이 유효하지 않을 때를 거치면 1번이 실행된다. 1번은 2,3이 실행된 후 뒤 필터를 실행시킨다.
2번은 헤더에 담긴 토큰을 가지고 UserDetail을 가져온다. 이 때 User는 인증이 된 상태이므로 인증토큰을 만들어주는데, AbstractAuthenticationToken 을 상속받은 UsernamePasswordAuthenticationToken을 사용하였다. 내 경우엔 principal이 username이기 때문. 만약 username이 아니라 다른 방법으로 식별한다면, Token을 커스텀하던지 다른Token 사용.
UsernamePasswordAuthenticationToken은 생성자를 두 개 가지는데, authorities가 세팅되지 않으면 isAuthenticated를 false로, authorities가 세팅되면 isAuthenticated를 true로 설정한다.
==> principal에 userDetail 자체를 넣어주었다.
3번은 Authenticated 된 토큰을 securitycontext에 세팅하고, 이 context를 SecurityContextHolder에 세팅한다.
SecurityContextHolder는 ThreadLocal이기 때문에 따로 선언하는 것이 아니라 그냥 가져다 쓸 수 있다.

<테스트>

테스트코드 짤줄몰라서 테스트용 컨트롤러 만들어버리기 ,,,,


토큰이 검증되지 않으면 controller까지도 못 오고 로그인페이지로 튕겨버리는 모습.👍
로그인기능은 얼추 끝난 것 같다. config에서 요청URL 별 권한설정, 액세스 실패 시 forwarding URL 설정 등 자잘한 것들이 남았지만, 다 간단하게 수정할 수 있는 것들이니 여기까지 해야겠다.
<결론>
간단하게 구현할 수 있을 줄 알았는데 공부하는데 생각보다 오래걸렸다 ...........
그래도 Spring Security를 활용해서 Authentication, Authorization을 전부 처리했으니 서비스와 컨트롤러가 가벼워졌을 듯
로그인 컨트롤러 없애서 뿌듯하다 !
'공부 > Spring' 카테고리의 다른 글
게시판 미니 프로젝트 // 연관관계에 대한 생각 (0) | 2023.05.11 |
---|---|
CRUD 게시판 리팩토링해보기 - <2> 프로젝트 구조 만들기, Entity 생성, Exception handler (0) | 2023.04.29 |
Spring Security (0) | 2023.04.25 |
ExceptionHandler // ResponseEntity // HTTP 상태 반환 (0) | 2023.04.23 |
간단한 게시판 CRUD // 로그인, 회원가입, JWT 추가 (0) | 2023.04.20 |