이번에는 로그인 등 인증/인가, API에 대한 접근 제한에 사용되는 Spring Sequrity에 대해서 정리해보고자 한다.
인증 - 해당 API를 접근 가능한지 인증
인가 - 특정 리소스에 접근 가능한지 인증
Security의 기본 원리 이해
기본적으로는 Servlet Context(tomcat)와 Spirng Dispatcher Servlet 사이에 Filter를 이용해 Sequrity를 적용한다. 하지만 이런식으로 Filter에 구현하게 되면, Filter는 Spring Bean을 인식하지 못하기에 문제가 발생한다. 예를들어, API 접근에 대한 권한을 부여할 때, 로그인 후 접근이 가능한 API가 있다고 가정하자. 이 때, FIlter에서 접근 제한을 한다면, 여러가지 Bean들에 대한 접근이 불가능하므로 인증/인가에 대한 어려움이 생긴다.
따라서, 이 보안/인증/인가 기능을 스프링에게 위임하여 실행된다. 방법 중 하나로는 Spring Security Interface를 사용하는 것이다. (Cofing 파일을 생성한다)
인터페이스를 사용한 스프링 시큐리티의 빈들은 필터들로 구성되어 있고 이 필터들의 모음을 Security Filter Chain이라고 부른다. 이 안에 는 여러가지 필터들이 존재하는데 원리는 우선적으로 default로 설정된 필터들도 존재하고 개발자들은 이 필터들을 사용하거나 필터들을 커스터마이징하여 인증/인가/보안 작업을 하면 된다.
1. 모든 요청을 AuthenticationFilter로 전달 : Config 파일로 전달
2. UsernamePasswordAuthenticationToken 생성
3. Token or Session 등 사용하는 인증 수단 권한 부여 설정
- (3~9의 과정을 의미)
- 개발자가 설정하여 어떤 조건을 가질 시 어떤 권한을 부여하며 어떤 API에 접근을 가능하게 할건지 등을 설정한다.
- 필요한 경우 즉, 롤 검사 등이 필요한 경우 UserDetailsService에 접근해 정보를 추출한다.
4. 결과를 UsernamePasswordAuthenticationToken에 담고 SecurityContext에 담는다.
5. 이 후 이를 UsernamePasswordAuthenticationFilter에서 처리한다.
다른 개념부터는 실제 코드를 작성해보면서 정리하려고 한다.
사용 방법은 다음과 같다.
1. 의존성 추가
2. Config 구성
3. Jwt Filter 설정
1. 의존성 추가
의존성만 추가하더라도 Security는 시작된다.
login API, logout API가 자동으로 생성되고 이 외의 모든 API에 대한 접근이 제한된다.
초기 로그인할 때, 아이디는 user, 비밀번호는 Spring 시작시 생성된 비밀번호를 console에서 확인해 사용하면 된다.
또한, 로그인에 성공하면 Spring security로 부터 자동으로 세션이 생성되어 로그인 필요없이 모든 API 접근이 가능하다.
이는 앞서 설명한것처럼 서버 재부팅시 삭제된다.
따라서 Spring Security는 이미 설정된 정보들을 커스터마이징하면서 제작하는 과정이라고 볼 수 있다.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.5.0</version>
</dependency>
2. Config 구성
위에서 의존성 추가만으로도 모든 API에 대한 접근이 막힌다고 하였고 Spring Security 설정은 어떠한 API에 대해서 어떤 조건을 가진 유저가 접근할 수 있는지, 인증, 인가 구현을 통해 작성해나가는 프레임워크와 같다고 하였다.
따라서 Config 파일 설정을 통해 그 과정을 구현하면 된다.
@EnableWebSecurity
모든 URL 요청이 여기 config를 거치게한다.
Spring Security는 동작 위치가 필터라고 하였다. 따라서 Spring에 빈들을 사용할 수 있지만 원론적으로는 필터의 위치에서 동작한다. 따라서 모든 접근에 대해서 즉, 컨트롤러, 서비스 등의 빈에 접근하기 전에 처리된다.
과정을 살표보면 우선, 비밀키를 .yml 등에서 가져온다. 이는 토큰을 해독하는데 사용한다. 참고로 현재 프로그램에서는 JWT 토큰을 활용하여 인증/인가 정보를 Stateless하게 검증한다. 이 후, SecurityFiltetChain을 만든다. 이로써, Secuirity Context가 생기고 이 Context안에서 Spring Security의 인증/인가 처리가 과정이 이루어진다.
CSRF(cross site request forgery)
웹 사이트 취약점 공격을 방지를 위해 사용하는 기술이다. 스프링 시큐리티가 CSRF 토큰 값을 세션을 통해 발행하고 웹 페이지에서는 폼 전송시에 해당 토큰을 함께 전송하여 실제 웹 페이지에서 작성된 데이터가 전달되는지를 검증하는 기술이다. 전달하고 전달받는 토큰이 정확한지에 따라 악의적인 접근이 아님을 확인한다.
현재는 프론트처리를하고 있지 않기에 Disable 해준다.cors는 간단하게 다른 오리진끼리 요청을 보내는 것을 가능하게 해주는 것으로 설정을 한다. 이 후, authorizenHttpRequests()로 HTTP 요청에 대한 권한 부여 구성을 시작한다..permitAll()로 된 API는 접근을 가능하게 하고 .hasAuthority를 설정하여 특정 권한이 있을 때, 이 권한을 가진 유저들에 한해서 해당 API를 접근하게 해준다.기본적으로는 Session으로 인증/인가를 한다. 여기서는 JWT를 사용할 것이기 때문에 STATELESS를 통해 사용하지 않음을 처리한다.이 후, 받은 토큰에서 정보를 꺼내오는 작업을 JWTFilter에서 처리한다. 이 후, 꺼내온 정보를 바탕으로 UsernamePasswordAuthenticationFilter에서 인가를 처리한다.
추가적으로 .hasAnyAuthority 사용시 여러가지 권한 중 하나만 가지고 있어도 API 접근을 허용한다는 의미이다.
cors plus : 현재 웹 브라우저가 출력되기 위해 연결된 서버가 아닌 다른 서버의 자원을 호출하는 것을 가능하게 해준다. 예를 들어, OAuth 등을 통해 다른 웹사이트를 통해 로그인등을 할 때, 이를 가능하게 해준다.
또한, 만약 Role처리 등에 대해서 서버에 저장된 내용(DB)을 가지고 한다면 접근이 가능한 빈을 (ex-Service Bean) 주입해 JWT filter에서 사용할 수 있게 해주면 된다.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Value("${jwt.secret}")
private String secretKey;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.httpBasic().disable().csrf().disable().cors().and().authorizeHttpRequests()
.requestMatchers("/member/login", "/member").permitAll()
.requestMatchers(HttpMethod.POST, "/member/test").hasAuthority("ROLE_USER")
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtFilter(secretKey), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Session vs Token
기본적으로 session이란 HTTP 통신에 필요한 데이터를 서버 메모리에 저장하는 방식이다.(서버 재부팅시 삭제된다)
스프링 시큐리티는 기본적으로 세션으로 로그인 여부를 관리한다. 발급은 tomcat이 한다.
또한, 로그인이 완료된 session 아이디를 다른 브라우저에서 로그인시 로그인이 가능하다. 이는 이미 세션아이디 내부에 로그인에 대한 정보가 들어있기 때문이다.
하지만 Session에 가장 큰 단점은 서버에 저장공간을 차지하는 것이다. 즉, 정보를 받아서 메모리에 접근해 현재 인증되었는지 확인하고 인가를 하는 구조이다. 따라서 Token을 많이 사용한다. 토큰은 토큰 자체에 정보를 담기에 따로 서버에 저장을 하지 않는다.
3. JWT filter
HTTP request로 부터 AUTHORIZATION을 읽는다. 즉, 전달받은 토큰을 불러온다.
이 후, 토큰이 유효한지 검사한후, 유효하다면 토큰에 담겨있는 내용을 추출해 인가에 관한 정보를 담아 반환한다.
정보를 담을 때는 UsernamePasswordAuthenticaion에 먼저 담고 이를 SecurityContextHolder에 담는다.이 후, UsernamePasswordAuthenticationFilter에서 인가를 처리한다.
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorization == null) {
System.out.println("잘못된 토큰");
filterChain.doFilter(request, response);
return;
}
String token = authorization.split(" ")[1];
// Expired Check
if (JWTutil.isExpired(token, secretKey)) {
System.out.println("만료된 토큰");
filterChain.doFilter(request, response);
return;
};
// Token에서 꺼냄
String userId = JWTutil.getUserId(token, secretKey);
String userRole = JWTutil.getUserRole(token, secretKey);
// userId, credentials, roll
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userId, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userRole)));
// Detail Build
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
'Server Development > Spring Basic' 카테고리의 다른 글
Spring - Image Save (0) | 2023.06.11 |
---|---|
Spring - @Transactional (0) | 2023.04.25 |
Spring - Interceptor (0) | 2023.04.06 |
Spring - Jasypt (0) | 2023.04.06 |
Spring - Multiple Requests, Threads (0) | 2023.04.05 |