교육 (Today I Learned)/Hanaro

[Hanaro] 69일차 / Spring Boot (Security), 시큐어 코딩 (시간 및 상태, 에러 처리)

Bay Im 2024. 4. 25. 09:35

시큐어 코딩

03 시간 및 상태

  • 검사 시점과 사용 시점 (TOCTOU)
    • 자원을 사용하는 시점과 검사하는 시점이 다르기 때문에 존재하던 자원이 사라지는 등 자원의 상태가 변화는 경우가 발생
    • 공유자원에 여러 프로세스가 접근하여 사용할 경우 동기화 구문을 사용하여 한번에 하나의 프로세스만 접근하도록 하고, 성능에 미치는 영향을 최소화하기 위해 임계코드 주변만 동기화 구문 사용
    • 예시
      • public void run() {
        	// 멀티쓰레드 환경에서 synchronized를 사용하여 동시에 접근할 수 없도록 사용해야한다.
        	synchronized(SYNC) {
        		try {
        			if (manageType.equals("READ")) {
        			File f = new File("Test_367.txt");
        ...코드생략...
        public static void main (String[] args) {
        	FileMgmtThread fileAccessThread = new FileMgmtThread("READ");
        	FileMgmtThread fileDeleteThread = new FileMgmtThread("DELETE");
        	fileAccessThread.start();
        	fileDeleteThread.start();
        	}
        }
  • 종료되지 않는 반복문 또는 재귀함수
    • 재귀의 순환횟수를 제어하지 못하여 자원을 과다하게 사용하면 위험하다. 귀납 조건(Base Case)이 없는 재귀 함수는 무한루프에 빠진다.
    • 모든 재귀 호출 시 재귀 호출 횟수를 제한하거나 초기값을 설정(상수)하여 재귀 호출을 제한해야 한다.

 

04 에러처리

  • 오류 메시지 정보노출
    • 사용자 데이터나 시스템 내부 데이터 같은 민감한 정보를 포함하는 오류 메시지를 생성하여 외부에 제공하는 경우 위험하다.
    • 오류 메시지는 정해진 사용자에게 최소한의 정보만 포함하도록 한다.
    • 예외 상황은 내부적으로 처리하고 민감한 데이터를 포함하는 오류를 출력하지 않도록 미리 정의된 메시지를 제공하도록 설정한다.
    • 예시
      • try {
        	rd = new BufferedReader(new FileReader(new File(filename)));
        } catch(IOException e) {
        	// 에러 코드와 정보를 별도로 정의하고 최소 정보만 로깅
        	logger.error("ERROR-01: 파일 열기 에러");
        }
  • 오류 사항 대응 부재
    • 오류가 발생할 수 있는 부분에 예외처리를 하지 않을 경우 위험할 수 있다.
    • 오류가 발생할 수 있는 부분에 제어문을 사용하여 적절하게 예외처리(try-catch)를 한다.
  • 부적절한 예외 처리
    • 함수 결과값에 대한 적절한 처리 또는 예외 상황에 대한 조건을 검사하지 않을 경우 문제가 생길 수 있다.
    • 값을 반환하는 모든 함수의 결과 값을 검사하여 의도한 값인지 확인하고 예외처리 사용시 구체적인 예외처리 수행

 

 

Spring Boot

Spring Security

  • build.gradle- dependencies 추가
    • implementation 'org.springframework.boot:spring-boot-starter-security'
    • implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    • testImplementation 'org.springframework.security:spring-security-test'
  • localhost:8080 으로 접속 후 연결 확인
    • 별다른 코드 입력 없이 로그인 화면이 출력된다.
     
  • application.properties 추가하여 기본 아이디/비밀번호 설정
    • spring.security.user.name=설정한기본아이디
    • spring.security.user.password=설정할기본비밀번호
    • 이후 위에서 설정한 기본 아이디/비밀번호로 로그인 가능해진다.
  • SecurityConfig.java 생성
    • 클래스에 어노테이션 주입
      • @Configuration
      • @EnableWebSecurity
        • 웹 보안 활성화를 위한 어노테이션
    • 메소드 생성
      • filterChain 메소드 (throws Exception)
        • 메소드에 어노테이션 주입
          • @Bean
        • SecurityFilterChain 인터페이스를 return하는 메소드
        • 매개변수로 HttpSecurity를 받는다.
        • 메소드 안에 기능별 코드 작성
          • csrf 보안을 쿠키 방식으로 지정
            • .csrf((auth)->auth.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
          • 모든 경로의 요청 허가
            • .authorizeHttpRequests( (auth) → auth
              • .requestMatchers(new AntPathRequestMatcher("/"))**
              • .permitAll()
          • 로그인 인증
            • .formLogin((formLogin) -> formLogin
              • .loginPage(”로그인폼 URI”)
              • .loginProcessingUrl(”로그인액션 URI”)
              • .defaultSuccessUrl("로그인성공시넘어갈 URI")
              • .successHandler((((request, response, authentication) -> { response.sendRedirect("인증 성공 후 별도 커스텀 처리 URI"); })))
              • .failureUrl("실패시 에러페이지 URI")
              • .permitAll()
          • 로그아웃 처리
            • .logout((auth) -> auth
              • .logoutRequestMatcher(new AntPathRequestMatcher("로그아웃 액션 URI"))
              • .logoutSuccessUrl("로그아웃 성공시 넘어 URI")
              • .invalidateHttpSession(true)
                • 세션 객체를 해제 (내부 저장 데이터도 소멸)
              • .deleteCookies("JSESSIONID")
                • response 헤더에 Set Cookie에 ""을 넣어준다
        • 예시
          • @Configuration
            @EnableWebSecurity  // 웹보안 활성화를 위한 Annotation
            public class SecurityConfig {
                @Bean
                SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
                    http
                            // 기본적으로 csrf(사이트간 요청 위조)가 활성화되어 있다.
                            // csrf 보안을 비활성화 한다.
            //                .csrf((auth) -> auth.disable())
            
                            //csrf 보안을 쿠키방식으로 지정한다.
                            //CsrfTokenRepository 인터페이스는 HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository
                            //2개가 있다.
                            //기본적으로 스프링 시큐리티는 HttpSessionCsrfTokenRepository로 CSRF 토큰을 HttpSession에 저장한다.
                            //하지만 커스텀 CsrfTokenRepository를 설정하고 싶을 때도 있을 것이다.
                            //예를 들어 자바스크립트 기반 어플리케이션을 지원하려면 쿠키에 CsrfToken을 저장해야 한다.
                            .csrf((auth)->auth.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
            
                            // HTTP 요청에 대한 보안설정을 시작
                            .authorizeHttpRequests( (auth) -> auth
                                    // 루트 밑의 모든 경로에 대한 모든 요청을 허가
                                    .requestMatchers( new AntPathRequestMatcher("/"),
                                            new AntPathRequestMatcher("/joinForm"),
                                            new AntPathRequestMatcher(("/joinAction"))
                                    ).permitAll()
                                    .requestMatchers("/admin").hasRole("ADMIN")
                                    .requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
                                    .anyRequest().authenticated())  // 그 외 어떤 요청에도 인증한다.
            
                            // 로그인 인증에 대한 설정을 시작
                            .formLogin((formLogin) -> formLogin
                                    // 로그인 페이지를 /loginForm URI로 지정
                                    .loginPage("/loginForm")
                                    // 로그인 액션 URI
                                    .loginProcessingUrl("/loginAction")
                                    // 로그인 성공시 넘어갈 URI 지정
                                    .defaultSuccessUrl("/")
                                    // 인증 성공 후 별도의 처리가 필요한 경우 커스텀 핸들러를 생성 및 등록한다.
                                    .successHandler((((request, response, authentication) -> {
                                        System.out.println("로그인 성공");
                                        response.sendRedirect("/");
                                    })))
                                    // 실패시 에러 페이지
                                    .failureUrl("/loginForm?error")
                                    // 로그인 페이지를 모든 사용자에게 허용
                                    .permitAll())
            
                            // 로그아웃 처리
                            .logout((auth) -> auth
                                    .logoutRequestMatcher(new AntPathRequestMatcher("/logoutAction"))
                                    .logoutSuccessUrl("/")
                                    .invalidateHttpSession(true) // 세션 객체를 해제 (내부 저장 데이터도 소멸)
                                    .deleteCookies("JSESSIONID") // response 헤더에 Set Cookie에 ""을 넣어준다.
                            )
                    ;
                    return http.build();
                }
            
                // BCrypt 암호화 엔코더 빈 생성
                @Bean
                public PasswordEncoder passwordEncoder() {
                    //Spring Security 5.3.3에서 공식 지원하는 PasswordEncoder 구현 클래스들은 아래와 같습니다.
                    //BcryptPasswordEncoder : BCrypt 해시 함수를 사용해 비밀번호를 암호화
                    //Argon2PasswordEncoder : Argon2 해시 함수를 사용해 비밀번호를 암호화
                    //Pbkdf2PasswordEncoder : PBKDF2 해시 함수를 사용해 비밀번호를 암호화
                    //SCryptPasswordEncoder : SCrypt 해시 함수를 사용해 비밀번호를 암호화
            
                    // 강도는 4 ~ 31까지 설정할 수 있으며 기본강도는 10이다.
                    return new BCryptPasswordEncoder();
                    //return SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
                    //return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
                    //return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
                }
            }
  • UserRole.java 생성 (enum)
    • 클래스 어노테이션 주입
      • @Getter
    • ADMIN, USER 등 권한 필드 및 권한 코드(value) 생성
      • 예시) USER(”ROLE_USER”)
    • 스프링 시큐리티의 권한 코드는 항상 “ROLE_”로 시작해야 한다.
    • String value 매개변수로 받는 생성자 생성
    • 관련 함수
      • hasRole(String)
        • 사용자가 주어진 역할이 있다면 접근을 허용
        • 예시) hasRole("USER")
      • hasAuthority(String)
        • 사용자가 주어진 권한이 있다면 접근을 허용
        • 예시) hasAuthority("ROLE_USER")
    • 예시
      • @Getter
        public enum UserRole {
            USER("ROLE_USER"),
            ADMIN("ROLE_ADMIN");
        
            private final String value;
        
            UserRole(String value) {
                this.value = value;
            }
        }
  • SecurityService.java 생성
    • 클래스 어노테이션 주입
      • @Service
    • implements UserDetailsService
    • loadUserByUsername 메소드 오버라이드
      • 사용자 아이디를 통해 사용자 정보와 권한을 스프링 시큐리티에 전달해준다.
    • 예시
      • @Service
        @RequiredArgsConstructor
        public class SecurityService implements UserDetailsService {
            private final MemberRepository memberRepository;
        
            // 사용자 아이디를 통해 사용자 정보와 권한을 스프링 시큐리티에 전달해준다.
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                Optional<MemberEntity> optional = this.memberRepository.findByUserId(username);
                if(optional.isEmpty()) {
                    throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");
                }
                MemberEntity memberEntity = optional.get();
        
                List<GrantedAuthority> authorities = new ArrayList<>();
                if("admin".equals(username)) {
                    authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
                } else {
                    authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
                }
                return new User(memberEntity.getUsername(), memberEntity.getPassword(), authorities);
            }
        }

 

728x90