skarltjr / Memory_Write_Record

나의 모든 학습 기록
0 stars 0 forks source link

Spring Security #6

Open skarltjr opened 3 years ago

skarltjr commented 3 years ago

-Http Basic은 헤더에 어떤 정보가 들어올 지 알고있어서 위험하다 https쓸때만 사용해야한다.

skarltjr commented 3 years ago

image

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();

-SecurityContextHolder SecurityContext 제공, 기본적으로 ThreadLocal을 사용한다. 안에 들어있는 객체는 모두 인증된 객체

-SecurityContext Authentication 제공.

-Authentication Principal과 GrantAuthority 제공.

-Principal “누구"에 해당하는 정보. -UserDetailsService에서 리턴한 그 객체. 객체는 UserDetails 타입.

-GrantAuthority: “ROLE_USER”, “ROLE_ADMIN”등 Principal이 가지고 있는 “권한”을 나타낸다. 인증 이후, 인가 및 권한 확인할 때 이 정보를 참조한다.

-UserDetails 애플리케이션이 가지고 있는 유저 정보와 스프링 시큐리티가 사용하는 Authentication 객체 사이의 어댑터.

-UserDetailsService 유저 정보를 UserDetails 타입으로 가져오는 DAO (Data Access Object) 인터페이스.

skarltjr commented 3 years ago

스프링 시큐리티에서 인증(Authentication)은 AuthenticationManager가 한다

< AuthenticationManager 구현체가 ProviderManager > 인자로 받은 Authentication이 유효한 인증인지 확인하고 Authentication 객체를 리턴한다. 인증을 확인하는 과정에서 비활성 계정, 잘못된 비번, 잠긴 계정 등의 에러를 던질 수 있다. Authentication authenticate(Authentication authentication) throws AuthenticationException;

즉 SecurityContextHolder는 Authentication정보를 담고있는 곳이고 인증을 처리하는것이 AuthenticationManager

인자로 받은 Authentication 사용자가 입력한 인증에 필요한 정보(username, password)로 만든 객체. (폼 인증인 경우) Authentication

유효한 인증인지 확인 사용자가 입력한 password가 UserDetailsService를 통해 읽어온 UserDetails 객체에 들어있는 password와 일치하는지 확인 해당 사용자 계정이 잠겨 있진 않은지, 비활성 계정은 아닌지 등 확인

Authentication 객체를 리턴 Authentication

ThreadLocal

스프링의 Transaction을 쓰는것도 ThreadLocal을 사용하는것

Java.lang 패키지에서 제공하는 쓰레드 범위 변수. 즉, 쓰레드 수준의 데이터 저장소. -같은 쓰레드 내에서만 공유. -따라서 같은 쓰레드라면 해당 데이터를 메소드 매개변수로 넘겨줄 필요 없음. -SecurityContextHolder의 기본 전략.

서블릿기반에서는 하나의 요청 당 하나의 서블릿 -> 하나의 쓰레드를 사용

ex)

public class AccountContext {

    private static final ThreadLocal<Account> ACCOUNT_THREAD_LOCAL
            = new ThreadLocal<>();

    public static void setAccount(Account account) {
        ACCOUNT_THREAD_LOCAL.set(account);
    }

    public static Account getAccount() {
        return ACCOUNT_THREAD_LOCAL.get();
    }

}
위와 같은 클래스를 설정하여 사용하면 파라미터로 Account정보를 넘겨주지않아도 된다.
SecurityContextHolder에서 이미 사용하기때문에 이것을 이용한다. 내부적으로 이런 방법으로 구현되어있다~

AuthenticationManager가 인증을 마친 뒤 리턴 받은 Authentication 객체의 행방은?

UsernamePasswordAuthenticationFilter -폼 인증을 처리하는 시큐리티 필터 -★인증된 Authentication 객체를 SecurityContextHolder에 넣어주는 필터★ -SecurityContextHolder.getContext().setAuthentication(authentication)

SecurityContextPersistenceFilter -SecurityContext를 HTTP session에 캐시(기본 전략)하여 여러 요청에서 Authentication을 ★공유할 수 있 공유하는 필터. -SecurityContextRepository를 교체하여 세션을 HTTP session이 아닌 다른 곳에 저장하는 것도 가능하다


스프링 시큐리티 Filter와 FilterChainProxy

스프링 시큐리티가 제공하는 필터들 = 모든 필터는 servlet filter

  1. WebAsyncManagerIntergrationFilter
  2. SecurityContextPersistenceFilter

  3. HeaderWriterFilter
  4. CsrfFilter
  5. LogoutFilter
  6. UsernamePasswordAuthenticationFilter

  7. DefaultLoginPageGeneratingFilter
  8. DefaultLogoutPageGeneratingFilter
  9. BasicAuthenticationFilter
  10. RequestCacheAwareFtiler
  11. SecurityContextHolderAwareReqeustFilter
  12. AnonymouseAuthenticationFilter
  13. SessionManagementFilter
  14. ExeptionTranslationFilter
  15. FilterSecurityInterceptor

이 모든 필터는 FilterChainProxy가 호출한다. FilterChainProxy -> Filters

이 필터들은 config(SecurityConfig)에 따라 생성된다. 만약 config에서 http.httpBasic():을 설정하지 않는다면 BasicAuthenticationFilter는 필터에 추가되지 않는다.

skarltjr commented 3 years ago
  1. 일반적인 서블릿 필터.
  2. 서블릿 필터 처리를 스프링에 들어있는 빈으로 위임하고 싶을 때 사용하는 서블릿 필터.
  3. 타겟 빈 이름을 설정한다.
  4. 스프링 부트 없이 스프링 시큐리티 설정할 때는 AbstractSecurityWebApplicationInitializer를 사용해서 등록.
  5. 스프링 부트를 사용할 때는 자동으로 등록 된다. (SecurityFilterAutoConfiguration)

인증 절차에 관한 정리

  1. SecurityContextHolder안에 인증정보 Authentication이 존재
  2. 이 인증정보는 AuthenticationManager가 인증을 처리하고
  3. UsernamePasswordAuthenticationFilter를 통해 SecurityContextHolder에 저장
  4. 적용되는 필터들은 보통 SecurityConfig에서 설정하는 정보를 바탕으로 필터가 생성되고 FilterChainProxy가 호출한다
  5. FilterChainProxy는 DelegatingFilterProxy를 통해 접근된다


AccessDecisionManager - 이미 인증을 거친 사용자가 특정한 리소스, 매서드에 접근할 때 이를 허용할 지를 판단 = 인가

Access Control 결정을 내리는 인터페이스로, 구현체 3가지를 기본으로 제공한다.

AccessDecisionVoter

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * .mvcMatchers("/User").hasRole("USER") 일 때 Admin은 유저이면서 admin롤을 갖는다
     * 따라서 admin도 user role에 접근할 수 있어야하는데 이대로면 admin은 user role로 접근할 수 없다.
     */
    public SecurityExpressionHandler securityExpressionHandler() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        /**  게층구조를 설정해주고*/
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setRoleHierarchy(roleHierarchy);
        /** 핸들러에 계층구조를 설정*/
        return handler;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .mvcMatchers("/", "/info", "/account/**").permitAll()
                .mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
                .mvcMatchers("/User").hasRole("USER")
                .anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
                .expressionHandler(securityExpressionHandler());
                //.and() //and로 이어가거나 아니면 Request내용 이후 다른 내용을 추가할 때 새로 http로 시작해주거나
        http
                .formLogin(); //로그아웃도 가능
        http
                .httpBasic(); //httpBasic도 사용하겠다
    }
}

AccessDecisionManager - 이미 인증을 거친 사용자가 특정한 리소스, 매서드에 접근할 때 이를 허용할 지를 판단 = 인가 는 어디에서 사용되는가 ?


인증 과정에서 발생하는 예외 처리 ExceptionTranslationFilter

AuthenticationException 발생 시 / 인증에 실패한 경우 -> 다시 인증을 하도록

AccessDeniedException 발생 시 / 인증은 됐는데

skarltjr commented 3 years ago

image

  1. SecurityConfig 에서 WebSecurity , HttpSecurity로 설정한 내용을 바탕으로 필터구성
  2. DeligatingFilterProxy를 통해 FilterChainProxy로 필터에 접근
  3. 인증은 AuthenticationManager를 통해 처리-> (UsernamePasswordAuthenticationFilter) 인증정보를 SecurityContextHolder에 담고
  4. 인가는 FilterSecurityInterCeptor -> AccessDecisionManager를 통해
skarltjr commented 3 years ago
  @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .mvcMatchers("/", "/info", "/account/**").permitAll()
                .mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
                .mvcMatchers("/User").hasRole("USER")
                .anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
                .expressionHandler(securityExpressionHandler());
                //.and() //and로 이어가거나 아니면 Request내용 이후 다른 내용을 추가할 때 새로 http로 시작해주거나
        http
                .formLogin(); //로그아웃도 가능
        http
                .httpBasic(); //httpBasic도 사용하겠다
    }

    /**  요청 - http filter 적용  / static resource - web */
    @Override
    public void configure(WebSecurity web) throws Exception {
       // web.ignoring().mvcMatchers("/favicon.ico"); // 파비콘 요청은 시큐리티 안걸리도록 그러나 다 설정하기 귀찮으니

        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        // 정적리소스 모두에 대해 ignore 적용
    }

Generally mvcMatcher is more secure than an antMatcher. As an example:

antMatchers("/secured") matches only the exact /secured URL mvcMatchers("/secured") matches /secured as well as /secured/, /secured.html, /secured.xyz



  1. WebAsyncManagerIntergrationFilter
  2. SecurityContextPersistenceFilter
  3. HeaderWriterFilter
  4. CsrfFilter
  5. LogoutFilter
  6. UsernamePasswordAuthenticationFilter
  7. DefaultLoginPageGeneratingFilter
  8. DefaultLogoutPageGeneratingFilter
  9. BasicAuthenticationFilter
  10. RequestCacheAwareFtiler
  11. SecurityContextHolderAwareReqeustFilter
  12. AnonymouseAuthenticationFilter
  13. SessionManagementFilter
  14. ExeptionTranslationFilter
  15. FilterSecurityInterceptor

Async 웹 MVC를 지원하는 필터: WebAsyncManagerIntegrationFilter

  1. ★ @Async어노테이션을 서비스에서 사용하는경우

    • 쓰레드가 다르기 때문에 SecurityContext를 공유받지 못한다. -> 설정

      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http
              .authorizeRequests()
              .mvcMatchers("/", "/info", "/account/**").permitAll()
              .mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
              .mvcMatchers("/User").hasRole("USER")
              .anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
              .expressionHandler(securityExpressionHandler());
              //.and() //and로 이어가거나 아니면 Request내용 이후 다른 내용을 추가할 때 새로 http로 시작해주거나
      http
              .formLogin(); //로그아웃도 가능
      http
              .httpBasic(); //httpBasic도 사용하겠다
      
      SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
      // 어디까지 자원을 공유할 것인가? -> 기본은 쓰레드로컬
      }

      SecurityContext 영속화 필터: SecurityContextPersistenceFilter

      SecurityContextRepository를 사용해서 기존의 SecurityContext를 읽어오거나 초기화 한다.

    • 기본으로 사용하는 전략은 HTTP Session을 사용한다.
    • Spring-Session과 연동하여 세션 클러스터를 구현할 수 있다
    • ex) 이미 인증을 했으면 /home /board 등 다른 인증을 필요로하는 url에 접근할 때 다시 인증을 요청하지 않아도 되게 securityContext를 공유해주는것이 SecurityContextPersistenceFilter
    • 당연히 session을 이용하는것이기 떄문에 session에 담아둔 정보를 사용하는데 session이 종료되면 정보도 끝

시큐리티 관련 헤더 추가하는 필터: HeaderWriterFilter

응답 헤더에 시큐리티 관련 헤더를 추가해주는 필터

skarltjr commented 3 years ago

CSRF 어택 방지 필터: CsrfFilter // Cross-site request forgery

1


thymeleaf 2.1이상부터는 form에 대해서 자동으로 csrf토큰을 설정해준다.

@SpringBootTest
@AutoConfigureMockMvc
class SignUpControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    @DisplayName("회원가입")
    void signUp() throws Exception {
        mockMvc.perform(get("/signup"))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("_csrf")));
    }

    @Test
    @DisplayName("회원가입처리")
    void processSignUp() throws Exception {
        mockMvc.perform(post("/signup")
                .param("username", "kiseok")
                .param("password", "123")
                .with(csrf()))
                .andDo(print())
                .andExpect(status().is3xxRedirection());
    }
}

로그아웃 처리 필터: LogoutFilter

로그아웃 필터 설정

http.logout()
        .logoutUrl("/logout")
        .logoutSuccessUrl("/")
   //     .logoutRequestMatcher()
    //    .invalidateHttpSession(true)
     //   .deleteCookies()
     //   .addLogoutHandler()
     //   .logoutSuccessHandler();

인증 처리 필터: UsernamePasswordAuthenticationFilter

폼 로그인을 처리하는 인증 필터

Basic 인증 처리 필터: BasicAuthenticationFilter

Basic 인증이란?

요청 캐시 필터: RequestCacheAwareFilter

현재 요청과 관련 있는 캐시된 요청이 있는지 찾아서 적용하는 필터. 캐시된 요청이 없다면, 현재 요청 처리 캐시된 요청이 있다면, 해당 캐시된 요청 처리 ex) 로그인이 필요한 admin페이지를 갈 때 로그인이 안된상태라면 로그인창을 먼저띄워준다. 그 다음 캐시에 저장된 원래 목적지 (admin페이지)로 가도록

skarltjr commented 3 years ago

시큐리티 관련 서블릿 스팩 구현 필터: SecurityContextHolderAwareRequestFilter

익명 인증 필터: AnonymousAuthenticationFilter

세션 관리 필터: SessionManagementFilter

인증/인가 예외 처리 필터: ExceptionTranslationFilter

인증, 인가 에러 처리를 담당하는 필터

ExceptionTranslationFilter -> FilterSecurityInterceptor // 14번필터가 15번필터를 크게 감싼 형태
AuthenticationException -> AuthenticationEntrypoint  // 로그인한 유저만 접근가능한 곳에 그냥 접근했을 때
AccessDeniedException -> AccessDeniedHandler  // 인증은 했는데 AdminRole만 접근가능한곳에 UserRole로 접근한 경우

AccessDeniedHandler 커스터마이징

 http
                .exceptionHandling()
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
                        UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                        String username = principal.getUsername();
                        log.info(username + " is denied to access " + httpServletRequest.getRequestURI()); //서버단에 로그도 남겨주고
                        httpServletResponse.sendRedirect("/access-denied");
                    }
                });

++ controller 와 html도 추가해줘야한다

인가 처리 필터: FilterSecurityInterceptor

HTTP 리소스 시큐리티 처리를 담당하는 필터. AccessDecisionManager를 사용하여 인가를 처리한다.

  http
                .authorizeRequests()
                .mvcMatchers("/", "/info", "/account/**","/signup").permitAll()
                .mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
                .mvcMatchers("/User").hasRole("USER")
                .anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
                .expressionHandler(securityExpressionHandler());
skarltjr commented 3 years ago

타임리프 스프링 시큐리티 확장팩

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Authentication과 Authorization참조
<div th:if="${#authorization.expr('isAuthenticated()')}">
    <h2 th:text="${#authentication.name}"></h2>
    <a href="/logout" th:href="@{/logout}">Logout</a>
</div>
<div th:unless="${#authorization.expr('isAuthenticated()')}">
    <a href="/login" th:href="@{/login}">Login</a>
</div>

@AuthenticationPrincipal

public class UserAccount extends User {

    private Account account;

    public UserAccount(Account account) {
        super(account.getUsername(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_" + account.getRole())));
        this.account = account;
    }

    public Account getAccount() {
        return account;
    }
}
메타어노테이션으로 + expression을 통해 익명 Authentication인 경우 (“anonymousUser”)에는 null 아닌 경우에는 account 필드를 사용한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentUser {
}
skarltjr commented 3 years ago

스프링 시큐리티 http vs web config https://stackoverflow.com/questions/56388865/spring-security-configuration-httpsecurity-vs-websecurity

@Override
public void configure(WebSecurity web) throws Exception {
    web
        .ignoring()
        .antMatchers("/login", "/register", "/api/public/**");
}

@Override
public void configure(HttpSecurity http) throws Exception {

    http
        .csrf().disable()
        .authorizeRequests()
        .antMatchers("/login", "/register", "/api/public/**").permitAll()
        .anyRequest().authenticated();
}

@Override
public void configure(WebSecurity web) throws Exception {
    web
        .ignoring()
        .antMatchers("/resources/**")
        .antMatchers("/publics/**"); // 그렇기 때문에 만약 여기서 publics에 대해 ignoring을 한다면 필터체인을 아예 거치지않기때문에
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .antMatchers("/publics/**").hasRole("USER") // no effect - 당연히 여기서 설정할 필요조차 없으며 효과가 없다.
        .anyRequest().authenticated();
}
skarltjr commented 3 years ago
package buravel.buravel.infra;

import buravel.buravel.infra.jwt.JwtAuthenticationFilter;
import buravel.buravel.infra.jwt.JwtAuthorizationFilter;
import buravel.buravel.modules.account.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final AccountRepository accountRepository;
    private final CorsConfig corsConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().configurationSource(corsConfigurationSource())
                .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http
                .formLogin().disable()
                .httpBasic().disable();
        http
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                .addFilter(new JwtAuthorizationFilter(authenticationManager(), accountRepository))

                .authorizeRequests()
                .mvcMatchers("/signUp", "/login", "/", "/tempPassword", "/findUsername","/index/search").permitAll()
                .mvcMatchers(HttpMethod.GET, "/plans","/plans/{id}").permitAll()
                .antMatchers(HttpMethod.OPTIONS,"/**").permitAll()

                .anyRequest().authenticated();

    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.addExposedHeader("Authorization");
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    @Override
    public void configure(WebSecurity web) throws Exception
    {
        web.ignoring()
                .mvcMatchers("/favicon.ico","/resources/**","/error")
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }
}

cors에서 문제가 되는 경우 기록 : jwt토큰을 헤더에 담아줬는데 리액트를 사용하는 프론트부분에서 개발자도구로는 토큰을 확인할 수 있었지만 콘솔에서는 뜨지 않았다. 그래서 preflight를 위해 .antMatchers(HttpMethod.OPTIONS,"/**").permitAll()을 설정했고 config.addExposedHeader("Authorization");를 통해 authorization 헤더를 노출할 수 있도록 설정해서 해결했다.

skarltjr commented 2 years ago

login / logout 시 405 에러가 나는 경우 login / logout은 시큐리티의 default 경로


아마 custom url로 login 혹은 logout을 사용한다면
Request method 'GET' not supported을 만날 수 있을 것
그럴 땐 url변경 나는 logOut으로 사용한 경험이 있다
``