Spring Security란?
Spring Security는 Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다. Spring Security는 '인증'과 '권한'에 대한 부분을 Filter 흐름에 따라 처리하고 있다. Filter는 Dispatcher Servlet으로 가기 전에 적용되므로 가장 먼저 URL 요청을 받지만, Interceptor는 Dispatcher와 Controller사이에 위치한다는 점에서 적용 시기의 차이가 있다. Spring Security는 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.
"인증" 절차 설명
1) 로그인 폼, 혹은
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager(), memberRepository);
//jwtAuthenticationFilter.setFilterProcessesUrl("/api/v1/login");
jwtAuthenticationFilter.setFilterProcessesUrl("/login");
이런식으로 인증을 담당할 필터를 설정한 뒤, "/login" 으로 reqeust가 들어오면 인증을 담당하는 JwtAuthenticationFilter를 사용하도록 한다.
2) 인증 필터.
위에서 설정한 인증 필터에서는 request로 부터 로그인 정보를 받아서, 인증 객체를 받은 뒤 인증 로직을 트리거한다.
여기서 만약, request에 id,pw 가 들어있다, 두 정보를 request에서 받아서 사용하면되고,
만약 구글 엑세스 토큰을 받아서 유저정보를 로드해와야한다면, 이곳에서 받아서 로드해오면 된다.
log.debug("JwtAuthenticationFilter.attemptAuthentication() : try to login");
LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class);
log.debug("loginResponse = {}", loginRequest);
ClubMemberDTO clubMemberDTO = googleService.get_ClubMemberDtoByGoogle_Token(loginRequest.getAccessToken3rd());
........
}
public ClubMemberDTO get_ClubMemberDtoByGoogle_Token(String google_token) {//구글 토큰으로 유저정보 가져오기
WebClient webClient = WebClient.create();
String url = String.format("https://oauth2.googleapis.com/tokeninfo?id_token=%s", google_token);
return webClient.get()
.uri(url)
.retrieve()
.bodyToMono(ClubMemberDTO.class)
.block();
}
이처럼, 리퀘스트에 따라, 적절히 유저 정보를 추출한 뒤, 아래처럼 authentication 객체를 만든 뒤, authenticationManager .authenticate() 으로 인증 절차를 시작하자.
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(clubMemberDTO.getEmail(), "1111");
Authentication authentication = authenticationManager.authenticate(authenticationToken);
ClubAuthMemberDTO clubAuthMemberDTO= (ClubAuthMemberDTO) authentication.getPrincipal();
(UsernamePasswordAuthenticationToken은 Spring Security에서 인증에 사용되는 표준 Authentication 객체 중 하나입니다. UsernamePasswordAuthenticationToken은 사용자 이름과 비밀번호를 인자로 받아 생성됩니다.)
이런식으로 클라이언트에서 받은 유저 정보를 기반으로 UsernamePasswordAuthenticationToken라는 authenticaion (인증 객체) 를 생성한 뒤,
authenticationManager의 .authenticate() 메소드를 통해 해당 authentication의 인증 로직을 트리거한다.
authenticationManager.authenticate(authenticationToken) 메소드는 실제로 인증을 수행하는 메소드입니다. 이 메소드는 인증 요청에 대한 Authentication 객체를 받아서 실제로 인증을 수행하고, 인증이 성공하면 인증된 Authentication 객체를 반환합니다.
인증이 성공하면, SecurityContextHolder에 인증된 사용자의 정보가 저장됩니다. SecurityContextHolder는 Spring Security에서 현재 사용자에 대한 정보를 저장하는데 사용되는 컨텍스트 객체입니다. SecurityContextHolder는 기본적으로 ThreadLocal을 사용하여 현재 스레드에서만 사용할 수 있는 인증 정보를 저장합니다.
Authentication 객체는 Principal, Authorities 등의 정보를 포함하고 있습니다. Principal은 인증된 사용자를 식별하는 데 사용되는 객체이며, Authorities는 인증된 사용자가 가지고 있는 권한을 나타내는 객체입니다. 이후에 요청이 처리될 때, SecurityContextHolder에서 인증된 사용자의 정보를 가져와서 요청을 처리하는 메소드에서 사용할 수 있습니다.
이 authenticationManager.authenticate(authenticationToken) 메소드를 호출하면, 실제 인증 처리를 담당하는 AuthenticationProvider가 Authentication 객체를 받아서 인증을 수행합니다. AuthenticationProvider는 UserDetailsService를 사용하여 인증에 필요한 사용자 정보를 가져오며, UserDetailsService는 loadUserByUsername() 메소드를 통해 사용자 정보를 반환합니다.
3) UserDetailService를 사용해서 실제 인증 처리
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //실제 인증( 아이디, 비밀번호 확인) 하는 메소드
//UserDetialsService의 loadUserByUsername을 오버라이딩해서 아이디를 통한 인증을 처리한다.
log.info("ClubUserDetailsService loadUserByUsername " + username);
Optional<ClubMember> result = clubMemberRepository.findOnlyByEmail(username);
//멤버 정보를 리포지토리에서 찾는다.
if(result.isEmpty()){
throw new UsernameNotFoundException("Check User Email or from Social ");
}
ClubMember clubMember = result.get();
log.info("-----------------------------");
log.info(clubMember);
ClubAuthMemberDTO clubAuthMemberDTO = new ClubAuthMemberDTO( //인증, 허가에 필요한 정보를 담은 DTO 생성
clubMember.getId(),
clubMember.getEmail(),
clubMember.getPassword(),
clubMember.isFromSocial(),
clubMember.getRoleSet().stream() // role을 저장한 Set의 모든 요소들을 ROLE_USER와 같은 SimpleGrantedAuthority타입으로
.map(role -> new SimpleGrantedAuthority("ROLE_"+role.name())) //인증시, hasRole() 로 검증가능한 형태로 만든다.
.collect(Collectors.toSet())
);
clubAuthMemberDTO.setName(clubMember.getName());
clubAuthMemberDTO.setFromSocial(clubMember.isFromSocial());
return clubAuthMemberDTO;////인증, 허가에 필요한 정보를 담은 DTO 반환
}
이런식으로, 앞서 만든 id, pw를 기반으로 DB 에서 해당 사용자가 실제로 존재하는지 확인하고, 있다면 성공, 없다면 실패.
이후에 자신을 사용한 ( authenticationManager.authenticate() 으로 ) 으로 돌아간다.
4) 인증 필터에서 UserDetailService에서 수행한 인증 결과를 받고, 성공, 실패시의 handler를 통해 동작을 처리한다.
ex) 로그인 실패 시, 새로운 유저를 생성하여 리턴, 성공 시, 해당 유저 정보를 리턴 등등
authenticationManager.authenticate(authenticationToken) 등 다양한 인증 과정의 에러를 failure Handler로 처리하고, 성공시의 로직을 successFul Handler에서 처리 시킨다.
**** UserDetial 객체의 생성
보통 UserDetail을 상속받는 DTO 로 구성한다.
User객체(UserDetail의 하위) 를 상속받은 ClubAuthMemberDTO 로 저장시
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(clubAuthMemberDTO, null, clubAuthMemberDTO.getAuthorities());
User의 getAuthorities() 를 통해 " 해당 유저의 권한" 도 같이 받을 수 있다. 따라서 contextHolder에 저장,. 인증절차는 상관없다.
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(clubMemberDTO, null, null);
따라서 이런식으로 일반적인 DTO 객체 ( 유저 식별 정보가 있는) 를 저장 후, Authorites() 영역을 비워도 되지만, 그러면 시큐리티에서 권한 설정( ex: User 만 접근 가능) 는 같이 저장되지 않는다.
ClubAuthMemberDTO가 Spring Security의 인증 및 권한 부여 메커니즘에 적합하게 설계되어 있는 이유는 다음과 같습니다.
User( UserDetail ) 을 상속 받는 이점
- User 클래스를 상속받았기 때문에, Spring Security에서 제공하는 UserDetailsService 인터페이스의 구현체로 사용하기 용이합니다.
- UserDetailsService는 Spring Security에서 사용자 정보를 조회하기 위한 인터페이스입니다. User 클래스를 상속받은 ClubAuthMemberDTO는 UserDetailsService의 구현체로 사용하기 적합합니다.
- User 클래스가 제공하는 필드와 메소드를 활용할 수 있습니다.
- User 클래스는 Spring Security에서 제공하는 클래스로, 사용자 정보를 저장하기 위한 필드와 메소드를 이미 구현하고 있습니다. ClubAuthMemberDTO는 User 클래스를 상속받았기 때문에, User 클래스가 제공하는 필드와 메소드를 그대로 활용할 수 있습니다.
DTO를 사용하는 이점
- DTO를 사용하면 보안상의 이점이 있습니다. 그냥 유저정보를 담은 엔티티는 SecurityContextHolder 간의 결합도가 높아질 수 있습니다. 예를 들어, Member 엔티티의 필드를 변경하면 SecurityContextHolder에서도 변경된 값을 반영해주어야 합니다. 이는 유지보수에 어려움을 줄 수 있습니다.( 실제 user 엔티티 객체가 아닌, DTO 를 사용하는 이점)
-
- DTO를 사용하면 확장성이 높아집니다.
- ClubAuthMemberDTO는 User 클래스를 상속받아 만들어진 클래스이기 때문에, 필요에 따라 User 클래스의 기능을 확장하거나 수정할 수 있습니다. 또한, Spring Security에서 제공하는 다양한 기능들과의 연동이 용이합니다.
따라서, Spring Security에서는 인증된 사용자 정보를 저장할 때 ClubAuthMemberDTO와 같이 Spring Security의 메커니즘에 적합하게 설계된 DTO나, 엔티티와 도메인 모델 간의 결합도를 낮출 수 있는 VO(Value Object)를 사용하는 것이 좋습니다.
+
JWt 토큰은 "Stateless" 가 장점이기에, 굳이 JWT 토큰을 통한 인증, 인가를 하는데 세션을 같이 사용하는 것은 특수한 상황외엔 없기에
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
이런식으로 security config에서 세션을 끄고 시작한다.
하지만 Spring Security에서는 기본적으로 인증된 사용자의 정보를 보관하고 관리하는 세션 기반의 인증을 사용한다.
이 경우, 사용자 정보는 서버측의 세션에 저장되기 때문에, 다음 요청에서도 세션을 통해 사용자 정보에 접근할 수 있다.
그 이유는, Security Context Holder를 세션에서 관리하기 때문. 따라서 세션이 만료되기 전까지 인증 정보가 유지되고,
따라서 "login" 요청이 들어올 때만 Security Context Holder에 저장을 해놔도 해당 "login" 요청이 response 되어 Security Context Holder에 해당 request에서 인증된 authentication 객체의 관리를 멈추더라도 인증 객체를 사용할 수 있는것.
(이 경우, 앱을 강제종료하여 세션이 제거된 경우, 유저를 다시 인증해야하는 문제가 생기기도 한다)
하지만, Stateless한 RESTful API와 같이 서버에서 세션을 사용하지 않는 경우, 인증 정보를 클라이언트 측에서 저장하고 매 요청마다 전송하여 인증을 유지한다. 이 경우, 서버 측에서 인증 정보를 저장하고 관리하는 것이 아니기 때문에, SecurityContextHolder를 사용하여 인증 정보에 접근할 수 없다. 이 경우에는 매 요청마다 인증 정보를 검증하는 필터를 만들어 사용하거나, JWT와 같은 토큰 기반의 인증 방식을 사용하는 것이 일반적이다.
Security Context Holder: authentication 객체를 스레드 안에서 보관한다.
보통 스레드 = http request 부터 response 까지이기에, response 를 보내면 context holder에서 사라진다.
따라서, "login" 로직에서 유저를 인증하여 security context holder에 저장한다해도, "login" 이 끝나서 reponse가 간 이후에 "user/read" 요청이 들어왔다면 @AuthenticationPrincipal 등으로 security Context Holder에서 authentication 객체를 가져올 순 없다.
하지만 실제로 해보면, 저장이 되는것 처럼 보이는데, 이는 "Session" 이 있어서 그렇다.
Spring Security config에서 아래와 같은 설정으로 Session을 사용하지 말아보자.
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
이러면, contextHolder에서 authentication 객체를 가져올 수 없을 것이다. ( 없기 때문에 )
Session 에서 security context holder를 관리하여, 세션이 만료되기 전까지 해당 authentication 객체를 관리하는 것 ( 보통 30~2시간 이라고한다.)
세션(Session)과 토큰(Token)은 인증(Authentication) 및 인가(Authorization)에 사용되는 기술로, 각각 장단점이 있습니다.
세션(Session)의 장단점:
장점: 세션은 서버 측에서 유지되므로 보안성이 높습니다. 또한, 세션은 서버에 부담을 주지 않고 사용자 상태를 저장할 수 있습니다.
단점: 세션은 서버에 저장되므로, 다수의 사용자가 접속할 경우 서버에 부하를 줄 수 있습니다. 또한, 서버 재시작 등의 이유로 세션이 만료되거나 삭제될 수 있습니다.
토큰(Token)의 장단점:
장점: 토큰은 클라이언트 측에서 저장되므로, 서버에 부담을 주지 않습니다. 또한, 토큰은 만료 기간을 설정하여 보안성을 높일 수 있습니다.
단점: 토큰은 클라이언트에 저장되므로, 보안성이 낮을 수 있습니다. 또한, 클라이언트 장치의 문제로 토큰이 손실될 수 있습니다.
따라서, 세션과 토큰은 각각 장단점이 있으며, 사용 시 상황에 따라 적절히 선택해야 합니다. 보안이 중요한 경우에는 세션을 사용하고, 서버 부하를 줄이고자 할 경우에는 토큰을 사용하는 것이 좋습니다.
토큰: Session과 다르게 Stateless이기에, 굳이 session과 같이 사용하는 것은 특수 케이스 외엔 비효율 적이다.
따라서 이런식으로 인증 객체를 저장하면 안되는 것.
Stateless 관점에서 토큰과 세션의 차이점
StateLess:서버가 클라이언트의 상태를 보존하지 않음을 의미한다.
Stateless 관점에서 JWT 토큰은 서버 측에서 세션 정보를 유지하지 않기 때문에 서버 부하가 적다. 또한, 클라이언트가 요청을 보낼 때마다 토큰을 함께 보내기 때문에 중간에 인터셉트되어도 토큰이 탈취당해도 유효 기간이 지나면 자동으로 만료된다. 반면에 세션 기반 인증은 서버 측에서 세션 정보를 유지하기 때문에 서버 부하가 높아질 수 있다. 또한, 중간에 인터셉트되면 세션 ID를 탈취당할 가능성이 있어 보안에 취약할 수 있다.
filterChain 내의 모든 필터들이 인증에 사용되는 필터들이다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.addFilter(corsFilter.corsFilter())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(apiCheckFilter(), UsernamePasswordAuthenticationFilter.class) //사용자 인증 전에 apiCheckFilter수행
.authorizeRequests() //인증 요청을 구성하는 시작지점.
.antMatchers("/swagger-ui/**","/api/v1/login", "/auth/reissue").permitAll() // 이 3개의 엔드 포인트는 그냥 접근 가능
.anyRequest().authenticated() //그외 모든 요청은 인증이 필요하다는 것을 설정
.and()
.exceptionHandling() //시큐리티 filterChain 중 예외 발생시 아래 예외로 처리한다.
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
;
return http.build();
}
'Spring boot' 카테고리의 다른 글
[query Optimization] LAZY 로딩과 N+1 문제 (0) | 2023.04.26 |
---|---|
[walk-talk] 걸음 수 기반 채팅 어플 데모 영상 (0) | 2023.04.11 |
[스프링 MVC] Part2 (서블릿) (1) | 2023.03.14 |
[스프링 MVC] Part 1 (웹 어플리케이션의 이해) (0) | 2023.03.14 |
[JPA] Part 6 (객체지향 쿼리 언어 pt.2) (0) | 2023.03.13 |