스프링 시큐리티 jwt 만드는 방법과 시큐리티 동작흐름 (tistory.com)
스프링 시큐리티 jwt 만드는 방법과 시큐리티 동작흐름
이번편은 스프링 시큐리티 동작 방식을 이해하고 실제 어떤식으로 구현해야 할지 알아본다. 1. 스프링 시큐리티 동작방식 앞서 그 전 포스터에서도 게시했지만 스프링 시큐리티는 로그인 방식
thaud153.tistory.com
이 인증을 보기전에 이 게시글을 한번 보도록하자.
jwt관련된 내용이지만 시큐리티의 흐름 방식등을 정리한 글이기에 이 글을 읽고 본다면
이 글이 이해가 될 것 이다.
1. 스프링 시큐리티 권한 설정
스프링 시큐리티에서는 어떤식으로 권한 설정을 하는지 알아보자.
시큐리티는 hasRole을 통해 권한 설정을 한다.
1. hasRole
hasRole은 사용자가 주어진 단일 역할을 가지고 있어야 함을 나타낸다.
예를들어
/user/** 이러한 경로에는 User의 권한이 있는 사용자만 들어오게 하고싶다하면
스프링 시큐리티에서는 이러한 기능을 제공해준다.
해당 유저가 /user/** 경로에 User의 권한을 가지고 있다면 200 ok를 보낼것이고
User의 권한을 가지고 있지 않다면 403에러를 보내게끔 하여 유저의 접속을 차단 할 수 있다.
이렇게 해서 ADMIN관리자와 USER의 권한등을 나눌 수 있게 한다.
그렇다면 어떤식으로 해야할까?
2. 동작 흐름
1. 사용자 요청 : 사용자가 웹 어플리케이션의 접속을 시도한다
2.FilterSecurityInterceptor : 이 필터는 들어오는 사용자의 보안을 처리한다.
요청이 들어오면 , 요청에 대한 권한이 필요한지 판단하기 위해
FilterInvocationSecurityMetaDataSource에 정보를 요청한다.
(이 필터가 사용되는 시점은 유저의 자격증명이 다 처리되고 난 후 맨 마지막에 실행된다
즉 : 유저의 아이디와 패스워드가 일치하면 해당 유저는 인증된 유저가 확인 되었고
그 다음에 유저의 해당권한을 처리한다.)
3. FilterInvocationSecurityMetadataSource
이 컴포넌트는 특정 요청에 필요한 권한 정보를 제공한다.
예를들어 우리가 /admin/** 경로에는 ADMIN 권한만 접속 할 수 있고
/user/** 경로에는 USER의 권한만 접속할 수 있다고 했다면
Map 형태에서 : Key : (url 경로 : /user/**) : Value : (ADMIN) 경로로
맵에서 키 벨류 형식으로 가지게 된다.
사진으로 본다면 RequestMap이라는 Map에서
키 : (경로) , 벨류 (권한 정보)로 가지게 된다.
4. 권한 룰 조회
해당 경로에 필요한 권한이 무엇인지 조회한다.
5. AcessDeicisionManager
: FilterSecurityInterceptor는
FilterInvocationSecurityMetadataSource에서 제공받은 권한 정보와 현재 인증이 된 사용
권한 정보를 AcessDeicisionManager로 넘긴다.
즉 위에 사진에 있는 requestMap의 형식의 키 밸류 값을 넘긴다고 생각하면 쉽다.
6. AcessDeicisionManager
SecurityContextHolder에 있는 권한정보를 추출한 후
requestMap에 있는 권한정보와 매치 시킨다.
만약 어떠한 클라이언트가 /user/** 경로로 진입했고
이 보호된 리소스는 USER의 권한이 필요하다고 한다면
AcessDeicisionManager가 user의 경로에 USER의 권한이 필요한것을 확인한 후
SecurityContextHolder에 있는 권한정보와 매치 시켜본다.
만약 SecurityContextHolder의 권한이 USER라면
YES : 사용자가 필요한 권한을 가지고있다면 , 요청을 계속 진행할 수 있다.
NO : 사용자가 필요한 권한을 가지고 있지 않다면 , AceessDeniedException이 발생하고
사용자는 접근 거부가 된다. (403에러)
이 부분이 FilterInvocationSecurityMetadataSource의 인터페이스의 기본 구현체인데
이 동그라미 친 로직에서 RequestMap에서 키와 밸류를 가져와서
매치를 시킨다.
사진으로 본다면 이렇게 되어있는데 /user의 경로에 접근시켜
값이 ROLE_USER임으로 우리가 설정한 RequestMap
즉 사진으로 본다면 우리는
Key : /buy/bronze
value : bronze , Silver
Key : buy/Silver
value : Silver
이렇게 RequestMap에 키 밸류 형식으로 들어가게 된다.
RequestMap을 돌면서 두 가지의 경우가 일어난다.
1. RequestMap 경로에 해당 권한이 있는 경우
해당 권한이 있는경우는 키 값을 return해서 권한 정보를 다음 필터로 보낸다
2. RequestMap 경로에 권한이 없는 경우 ( 모든 경로를 허용 하였을 경우)
requestMap을 돌다 만약 클라이언트가
/user/ 경로를 요청하였고 리퀘스트 경로에 /user/의 경로가 있지만
해당 권한이 없는 경우는 모든 경로를 허용한다는 뜻임으로
null을 반환함. null을 반환할 경우 모든 경로는 허용됨.
그 후 AcessDecisionManager의 전달되어
return된 키 값과 SecurityContextHolder의 있는 권한정보를 비교함.
3. 권한 구현
@Bean
public SecurityFilterChain filterChain01(HttpSecurity security) throws Exception {
security.authorizeHttpRequests(request -> request.requestMatchers("/buy/bronze")
.hasAnyRole("bronze","Silver")
.requestMatchers("/buy/silver").hasAnyRole("silver")
.anyRequest().permitAll());
return security.build();
}
1. bronze의 경로에는 브론즈와 실버의 권한이 접근할 수 있다
2. Silver의 경로에는 실버의 권한만이 접근할 수 있다.
hasRole을 통해 시큐리티에 접근권한을 부여할 수 있다.
2. 인증 정보 DB에서 조회
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account byUsername = repository.findByUsername(username);
if(byUsername == null) throw new UsernameNotFoundException("인증 실패");
AccountContext accountContext = new AccountContext(byUsername);
return accountContext;
}
}
UserDetailsSerivce에서 DB의 있는 유저 정보를 조회한다.
조회하고 반환타입을 USerDetails의 타입으로 객체를 넘겨준다.
3. 권한 부여 및 설정
package com.example.springsecurity03.Service;
import com.example.springsecurity03.Jpa.Account;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
@Getter
public class AccountContext implements UserDetails {
private final Account account;
public AccountContext(Account account){
this.account = account;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(account.getRole()));
authorities.add(new SimpleGrantedAuthority(account.getTear()));
return authorities;
}
@Override
public String getPassword() {
return account.getPassword();
}
@Override
public String getUsername() {
return account.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails의 인터페이스를 구현받은 AccountContext에서 유저의 아이디와 비밀번호 권한 정보를
설정할 수 있다.
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(account.getRole()));
authorities.add(new SimpleGrantedAuthority(account.getTear()));
return authorities;
}
이 부분에서 권한을 설정하면 스프링 시큐리티가
SecurityContextHolder부분에 넣는데
나의 경우는 권한정보가 2개가 필요해서 2개의 권한정보를 넣었다.
이렇게 되면 2개의 권한정보가 저장되어 만약에
User의 권한도 필요하면서 Bronze나 Silver의 권한을 각각 세분화 하고싶다면
이렇게 2개의 권한을 설정해줘야 한다.
(즉 권한정보가 여러개 필요하다면)
이렇게 설정한다면 권한 설정은 끝났다.
그 후 나는 위에서 말했듯이 2개의 권한을 세분화해서 필요하다고 하였는데
1.USER , MANAGER , ADIMN의 권한
security.authorizeHttpRequests(auth ->
auth.requestMatchers("/join").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("MANAGER","ADMIN")
.requestMatchers("/user/**").hasRole("USER")
.anyRequest().permitAll());
2. USER의 권한을 가지면서 Bronze , Silver의 권한
@Bean
public SecurityFilterChain filterChain01(HttpSecurity security) throws Exception {
security.authorizeHttpRequests(request -> request.requestMatchers("/buy/bronze")
.hasAnyRole("bronze","Silver")
.requestMatchers("/buy/silver").hasAnyRole("silver")
.anyRequest().permitAll());
return security.build();
}
이렇게 2개의 권한을 가지고있다.
@GetMapping("/buy/Bronze")
@PreAuthorize("hasAnyRole('ROLE_Bronze','ROLE_Silver') and hasAnyRole('ROLE_USER')")
public String bronzeTear(){
return "브론즈 티어 확인";
}
그 후 @PreAuthorzie에서 두 개의 권한을 동시에 검사한다.
또한 이렇게 권한이 없다면 AcessDeinedExcetion을 터뜨리는데
security.exceptionHandling(handler -> handler.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AccountContext principal = (AccountContext) authentication.getPrincipal();
}
}));
이렇게 exceptionHandling을 통해
AccessDeniedHandler의 익명객체를 구현해서 사용하면
AcessDeinedExcetion가 발생했을 때 이 핸들러에서 처리할 수 있다.
1. USER의 권한을 가지면서 /buy/Bronze의 권한은 Bronze의 권한과 Silver의 권한이 두 개 중 하나가 있어야만
접근이 가능하게 했다.
테스트 해보니 200ok가 잘 되는걸 확인 할 수 있고
해당 권한이 하나라도 없다면 403에러가 뜨는걸 볼 수 있다.
'스프링 시큐리티 -세션 > Chatper02 스프링 시큐리티 인증 흐름' 카테고리의 다른 글
스프링 시큐리티 jwt 만드는 방법과 시큐리티 동작흐름 (3) | 2024.01.03 |
---|---|
6장 스프링 시큐리티 Authentication,SecurityContextHolder 인증흐름 (1) | 2023.12.28 |