Open rhakdnj opened 7 months ago
요청을 보낼 때마다 Spring Security가 해당 요청을 가로챕니다.
예를 들어 localhost:8080/users로 요청을 보내면 Spring Security가 이 요청을 가로채서 일련의 필터를 실행합니다. 이런 일련의 필터를 필터 체인이라고 합니다.
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.SecurityFilterChain
@Configuration
class SpringSecurityConfiguration {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain = http
.authorizeHttpRequests {
it.anyRequest().authenticated()
}
.httpBasic(Customizer.withDefaults())
.build()
}
사이트 간 요청 위조입니다. 사용자가 웹사이트에 로그인하면 사용자 세션이 생성되는데요, 이 사용자 세션은 브라우저의 쿠키를 사용해 식별됩니다.
사용자가 이 웹사이트에서 로그아웃하지 않은 채 악성 웹사이트로 이동하면, 악성 웹사이트에서는 사용자의 이전 인증 정보 브라우저에 있는 쿠키를 이용할 수 있습니다. (same-site, http-only 걸어야함)
이러한 취약성을 CSRF, 사이트 간 요청 위조라고 합니다.
Spring Security를 추가하면 CSRF 보호가 자동으로 사용 설정됩니다.
POST, PUT, 어떤 업데이트 요청에든 CSRF 보호가 사용 설정됩니다.
Spring Security의 FilterChain은 WebMvc가 적용되면 무조건적으로 거치는 Filter이다.
이 과정을 거쳐 모든 요청은 authenticated 된 요청이 됩니다.
Based Authentication (Base 64)
Authorization : Basic base64 Encode(username:password)
Uses a Session Cookie
JSESSIONID: E2E693A57F6F7E4AC112A1BF4D40890A
Base64 인코딩 사용자 이름과 패스워드가 요청 헤더로 전송된다.
Production 환경에서는 인코딩된 값을 디코딩하기가 쉽다. 특히 username 과 password를 !!
user accss 및 user role에 관한 정책이 없다. 만료일 자체가 없다.
이후, 웹사이트에서 일어나는 모든 작업에 대해 이 세션 쿠키가 요청과 함께 전송됩니다.
Spring Security
사이트 간 요청 위조란 무엇일까?
예를 들어, 은행 웹사이트는 대개 웹 애플리케이션이므로 쿠키가 생성돼서 웹 브라우저에 저장됩니다
은행 웹사이트에서 로그아웃하지 않고 악성 웹사이트로 이동하면, 사용자가 알지 못하는 상태에서 악성 웹사이트는 쿠키를 이용해 은행 웹사이트에 요청을 실행합니다.
이러한 문제를 사이트 간 요청 위조라고 합니다.
Spring Security에서 읽기 요청은 그대로 허용하지만, 업데이트 요청에는 CSRF 토큰이 없으면 401에러가 발생한다.
상태가 없는 REST API를 사용한다면 CSRF를 사용 해제하는 것이 좋다.
server.servlet.session.cookie.same-site: strict
SpringBootWebSecurityConfiguration
CORS 설정은
WebMvcConfigurer에서 addCorsMappings콜백 메서드 구성
@CrossOrigin(origins = "http://localhost:8080)
@CrossOrigin
: Allow from all origins public UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList(roles.length);
String[] var3 = roles;
int var4 = roles.length;
for(int var5 = 0; var5 < var4; ++var5) {
String role = var3[var5];
Assert.isTrue(!role.startsWith("ROLE_"), () -> {
return role + " cannot start with ROLE_ (it is automatically added)";
});
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return this.authorities((Collection)authorities);
}
@Bean
fun userDetailsService(): InMemoryUserDetailsManager {
val user = User.withUsername("user")
.password("{noop}user")
.roles("USER")
.build()
val admin = User.withUsername("admin")
.password("{noop}admin")
.roles("ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}
package org.springframework.security.core.userdetails.jdbc;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;
public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware {
public static final String DEFAULT_USER_SCHEMA_DDL_LOCATION = "org/springframework/security/core/userdetails/jdbc/users.ddl";
public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority from authorities where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private String authoritiesByUsernameQuery = "select username,authority from authorities where username = ?";
private String groupAuthoritiesByUsernameQuery = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
private String usersByUsernameQuery = "select username,password,enabled from users where username = ?";
private String rolePrefix = "";
private boolean usernameBasedPrimaryKey = true;
private boolean enableAuthorities = true;
private boolean enableGroups;
// ...
BcryptPasswordEncoder의 기본값은 10이지만, 이를 높이면 해싱 작업량을 늘릴 수 있습니다.
기본 인증의 경우에는 기본 인증 헤더에 만료 기한, 사용자에 관련된 세부 정보(권한)이 없습니다.
JWT, 즉 Json Web Token이 사용됩니다.
이렇게 Base64로 인코딩된 헤더의 값, 그리고 Base64로 인코딩된 페이로드의 값, Base64로 인코딩된 Verify Signature 값이 공유됩니다. (Client와 Server)
첫 번째 부분이 헤더, 두 번째 부분은 페이로드, 마지막 부분은 시그니처입니다.
Resource Server
Create Key Pair
Create RSA Key object using Key Pair
org.springframework.boot:spring-boot-starter-oauth2-resource-server
에 기본적으로 포함되어 있음)Create JWKSource (JSON Web Key source)
Use RSA Public Key for Decoding
5: Use JWKSource for Encoding
인증은 Spring Security 필터 체인의 일부로서 이루어집니다.
요청이 유입될 때마다 Spring Security가 그걸 가로채고 필터 체인 전체가 실행됩니다. 그리고 그 필터의 일부로서 인증 확인이 이루어집니다. 인증은 AuthenticationManager
에서 시작됩니다.
AuthenticationManager (인증 담당)
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
authenticate() 메서드가 호출되기 전에 Authentication에는 자격증명만 포함되어 있습니다. 만일 인증에 성공하면, 즉 authenticate() 메서드가 성공적으로 호출되면 Authentication는 주체와 권한도 포함하게 됩니다.
AuthenticationProvider (특정 인증 수행)
UserDetailsService (사용자 데이터를 로딩하기 위한 핵심 인터페이스)
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
인증 결과는 다음과 같은 방식으로 저장됩니다.
SecurityContextHolder > SecurityContext > Authentication > GrantedAuthority
만약 아무 설정을 안했더라면 헤더에 base64 인코딩해서 전달해야한다.
Request Header;
Authorization
:Basic base64(username:password)