rhakdnj / kopring

0 stars 0 forks source link

spring-security #15

Open rhakdnj opened 7 months ago

rhakdnj commented 7 months ago
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
}


spring.security.user.name: username
spring.security.user.password: password

만약 아무 설정을 안했더라면 헤더에 base64 인코딩해서 전달해야한다.

Request Header; Authorization : Basic base64(username:password)


@Configuration
class SpringSecurityConfiguration {
  @Bean
  fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http.authorizeRequests(auth -> auth.anyRequest().authenticated())

    http.crsf().disable()
    http.headers().frameOptions().disable()
  }
}
rhakdnj commented 7 months ago

요청을 보낼 때마다 Spring Security가 해당 요청을 가로챕니다.

예를 들어 localhost:8080/users로 요청을 보내면 Spring Security가 이 요청을 가로채서 일련의 필터를 실행합니다. 이런 일련의 필터를 필터 체인이라고 합니다.

  1. All requests should be authenticatd.
    • 인증되지 않았다면, 기본값으로 웹 페이지가 나타납니다.
  2. CSRF
    • POST, PUT 요청에 영향을 주게 됩니다. (csrf 해제를 해야지만 POST 요청이 가능하다)
rhakdnj commented 7 months ago
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()
}
rhakdnj commented 7 months ago

CSRF

사이트 간 요청 위조입니다. 사용자가 웹사이트에 로그인하면 사용자 세션이 생성되는데요, 이 사용자 세션은 브라우저의 쿠키를 사용해 식별됩니다.

사용자가 이 웹사이트에서 로그아웃하지 않은 채 악성 웹사이트로 이동하면, 악성 웹사이트에서는 사용자의 이전 인증 정보 브라우저에 있는 쿠키를 이용할 수 있습니다. (same-site, http-only 걸어야함)

이러한 취약성을 CSRF, 사이트 간 요청 위조라고 합니다.

Spring Security를 추가하면 CSRF 보호가 자동으로 사용 설정됩니다.

POST, PUT, 어떤 업데이트 요청에든 CSRF 보호가 사용 설정됩니다.

rhakdnj commented 7 months ago

Spring Security의 FilterChain은 WebMvc가 적용되면 무조건적으로 거치는 Filter이다.

이 과정을 거쳐 모든 요청은 authenticated 된 요청이 됩니다.

rhakdnj commented 7 months ago

Based Authentication (Base 64)

image

이후, 웹사이트에서 일어나는 모든 작업에 대해 이 세션 쿠키가 요청과 함께 전송됩니다.

rhakdnj commented 7 months ago

Spring Security

  1. 기본적으로 모든 것을 보호한다.
  2. 폼 기반 인증이 기본적으로 사용 설정되는 인증 방식이다.
  3. 폼 기반 인증에서는 세션 쿠기 사용된다.
rhakdnj commented 7 months ago

CSRF

사이트 간 요청 위조란 무엇일까?

예를 들어, 은행 웹사이트는 대개 웹 애플리케이션이므로 쿠키가 생성돼서 웹 브라우저에 저장됩니다

은행 웹사이트에서 로그아웃하지 않고 악성 웹사이트로 이동하면, 사용자가 알지 못하는 상태에서 악성 웹사이트는 쿠키를 이용해 은행 웹사이트에 요청을 실행합니다.

이러한 문제를 사이트 간 요청 위조라고 합니다.

Spring Security에서 읽기 요청은 그대로 허용하지만, 업데이트 요청에는 CSRF 토큰이 없으면 401에러가 발생한다.

image image

상태가 없는 REST API를 사용한다면 CSRF를 사용 해제하는 것이 좋다.

CSRF 토큰 대신에 SameSite 쿠키를 사용해 쿠키는 해당 사이트로만 전송되도록 방어합니다.

server.servlet.session.cookie.same-site: strict
rhakdnj commented 7 months ago

SpringBootWebSecurityConfiguration

rhakdnj commented 7 months ago

CORS

CORS 설정은

rhakdnj commented 7 months ago
        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);
        }
rhakdnj commented 7 months ago

 @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)
  }
rhakdnj commented 7 months ago

Jdbc H2로 사용자 인증 과정

h2 console frame 허용 config

JdbcDaoImpl

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;

    // ...
image


image
rhakdnj commented 7 months ago

인코딩, 해싱, 암호화

스크린샷 2024-04-14 오후 3 08 22

BcryptPasswordEncoder의 기본값은 10이지만, 이를 높이면 해싱 작업량을 늘릴 수 있습니다.

rhakdnj commented 7 months ago

기본 인증의 경우에는 기본 인증 헤더에 만료 기한, 사용자에 관련된 세부 정보(권한)이 없습니다.

JWT, 즉 Json Web Token이 사용됩니다.

image

jwt.io

이렇게 Base64로 인코딩된 헤더의 값, 그리고 Base64로 인코딩된 페이로드의 값, Base64로 인코딩된 Verify Signature 값이 공유됩니다. (Client와 Server)

첫 번째 부분이 헤더, 두 번째 부분은 페이로드, 마지막 부분은 시그니처입니다.

image
rhakdnj commented 7 months ago

JWT Authentication using Spring Boot’s OAuth2

Resource Server

  1. Create Key Pair

    • java.security.KeyPairGenerator
    • openssl을 통해 KeyPair 을 만들어도 됨
  2. Create RSA Key object using Key Pair

    • com.nimbusds.jose.jwk.RSAKey (org.springframework.boot:spring-boot-starter-oauth2-resource-server에 기본적으로 포함되어 있음)
  3. Create JWKSource (JSON Web Key source)

    • Create JWKSet (a new JSON Web Key set) with the RSA Key
    • Create JWKSource using the JWKSet
  4. Use RSA Public Key for Decoding

    • NimbusJwtDecoder.withPublicKey(rsaKey().toRSAPublicKey()).build()

5: Use JWKSource for Encoding

rhakdnj commented 7 months ago

Spring Security에서 인증

image image

인증은 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;
}
rhakdnj commented 7 months ago

인증 결과는 다음과 같은 방식으로 저장됩니다.

SecurityContextHolder > SecurityContext > Authentication > GrantedAuthority