우주먼지
article thumbnail
Published 2022. 11. 18. 19:53
Spring Security 기본 구조 Framework/Spring

💡 Spring Security

어플리케이션에 Spring Security가 없을때 중요한 요소가 빠져있다.
- Authentication (인증)
- Authorization (인가)
- 웹 보안 취약점에 대한 방지

Spring Security를 사용하는 이유
- 특정 보안 요구사항을 만족하기 위한 Customizing 용이
- 유연한 확장
- 보안기능이 검증된 신뢰할만한 보안 프레임워크

Spring Security의 로그인 인증 방식
- Form Login 방식 : SSR 방식의 어플리케이션에서 주로 사용

 

Security를 적용하여 보안 취약으로 인한 사고 방지 방법

  • SSL 적용
  • Role 별 권한 적용
  • 많은 유형의 사용자 인증 기능
  • 민감 정보 암호화
  • resource ACL
  • 알려진 웹 공격 차단

 

Spring Security 용어

  • Principal (주체)
    • 작업수행 사용자, 시스템 등 인증완료된 사용자의 계정 정보를 의미
  • Authentication (인증)
    • 인증의 정상적 수행을 위한 Credential(Password 등) 정보가 필요함
  • Authorization (인가)
    • 인증을 통과한 사용자에게 특정 리소스에 대한 권한을 Role 형태로 ACL
  • Access Controller (접근제어)
    • resource ACL

💡 동작 과정

 

Security Configuration - Inmemory User 설정 (테스트 용도)

  • UserDetailsManager
    • UserDetails 인터페이스의 구현체인 User 작성
  • User
    • withDefaultPasswordEncoder()  <- Deprecated (유저를 고정해서 사용하지 말라는 의미의 Deprecated)
      • password Incoding
    • roels()
      • 유저 레벨 설정
  • InMemoryUserDetailsManager객체 리턴
    // Spring Security의 설정 정보 작성

    @Bean
    public UserDetailManager user() {

        /** 이 방식은 데모 & 테스트 시 유용하게 사용할 수 있으며, 실무에서 사용 X 
         *  사용자 인증을 위한 계정정보를 메모리에 고정
         *
         * 1. UserDetails를 관리하는 UserDetailsManager 인터페이스 타입 선언
         * 2. UserDetails 인터페이스의 구현체인 User 클래스를 사용하여 User의 인증 정보 생성
         * 3. withDefaultPasswordEncoder를 사용한 패스워드의 암호화 적용
         * 4. roles() - 유저의 역할 지정
         * 5. 메모리상으로 UserDetails를 관리하므로 InMemoryUserDetailsManager 구현체를 사용하여 객체를 빈으로 등록
         */

        UserDetails user =
                User.withDefaultPasswordEncoder() // 패스워드 암호화
                        .username("abc@abc.com")
                        .password("1234")
                        .roles("USER")
                        .build();

        UserDetails admin =
                User.withDefaultPasswordEncoder() // 패스워드 암호화
                        .username("admin@abc.com")
                        .password("1234")
                        .roles("ADMIN")
                        .build();

        return new InMemoryUserDetailsManager(user, admin);
    }

 

Security Configuration  - Filter 설정 (Custom Login Page 지정)

  • SecurityFilterChain 의 파라미터로 HttpSecurity 사용하며 Exception을 던짐
  • Http Option
    • frameOptions() - html 태그 중 <frame>,<iframe>,<object> 태그 페이지 렌더링 여부 결정
    • sameOrigin() - 동일 출처의 request만 렌더링
    • csrf().disable() - crsf 공격 방지 disable
    • formlogin() - 로그인 방식 지정
    • loginPage() - 로그인 페이지를 렌더링할 view 지정
    • loginProcessingUrl() - 로그인 인증을 수행할 form.action의 URL과 매핑되는 URL 지정
    • failureUrl() - 로그인 실패 시 리다이렉트될 view 지정
    • logout() - 로그아웃 설정을 위한 LogoutConfigurer 리턴
    • logoutUrl() - 로그아웃 페이지를 렌더링할 view 지정
    • logoutSuccessUrl - 로그인 성공 시 리다이렉트 될 경로 지정
    • and() - 보안설정 체인화
    • authorizeHttpRequests() - 요청이 들어올때 ACL 수행
    • anyRequest().permitAll() - 모든 요청에 대한 접근 허용
    // Custom Login Page 지정
    
    @Bean
    public SecurityFilterChain CustomLoginPage(HttpSecurity lendering) throws Exception {

        /** HTTP Security를 통한 HTTP Request 보안 설정
         * 
         * 1. csrf 공격 방어 disable
         * 2. 인증방법 지정 - form login
         * 3. 파라미터 -> AuthController의 AuthForm()에 URL 요청 전송
         * 4. 로그인 인증 요청을 수행할 요청 URL 지정, login.html - form 태그 - action의 URL과 동일
         * 5. 인증 실패 시 리다이렉트 할 URI 지정
         * 6. 보안 설정을 메소드 체인 형태로 구성 가능
         * 7. 접근 권한 체크 정의
         * 8-9. 클라이언트의 모든 요청에 대해 접근 허용
         */

        lendering
                .csrf().disable() // 1
                .formLogin() // 2
                .loginPage("/secure/auth-form") // 3
                .loginProcessingUrl("/process_login") // 4
                .failureUrl("/secure/auth-form?error") // 5
                .and() // 6
                .authorizeHttpRequests() // 7
                .anyRequest() // 8
                .permitAll(); // 9

        return lendering.build();
    }

 

Security Configuration - Request URL 접근 권한 부여

    // Request URI 접근 권한 부여

    @Bean
    public SecurityFilterChain AuthorizeRequest(HttpSecurity authorize) throws Exception
    {

        /** exceptionHandling() 부터 설명
         * 
         * 1. 권한없는 사용자가 특정 requestURL에 접근시 표시할 에러 페이지 렌더링 & Exception 처리
         * 2. 람다 표현식을 통한 request URI 접근 권한 부여 - antMatchers 순서 주의 (낮은 권한 순으로 작성 필수)
         * 3. '*'이 1개일 경우 하위 URL의 depth가 1인 URL만 허용
         * 4. 단일 페이지 접근 지정
         * 5. 모든 URL 허용
         */

        authorize
                .csrf().disable()
                .formLogin()
                .loginPage("/secure/auth-form")
                .loginProcessingUrl("/process_login")
                .failureUrl("/secure/auth-form?error")
                .and()
                .exceptionHandling().accessDeniedPage("/secure/denied") // 1
                .and()
                .authorizeHttpRequests(authorize -> authorize // 2
                        .antMatchers("/orders/**").hasRole("ADMIN") // 3
                        .antMatchers("/members/my-page").hasRole("USER") // 4
                        .antMatchers("/**").permitAll()); // 5
        return authorize.build();
    }

 

 

Security Configuration - LogOut 기능 구현

    // LogOut 기능 구현

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity logout) throws Exception {

        /** logout() 부터 설명
         * 
         * 1. 로그아웃 설정을 위한 LogoutConfigurer 리턴
         * 2. 로그아웃을 진행할 Request URL 지정
         * 3. 로그아웃 후 리다이렉트 할 URL 지정
         */

        logout
                .csrf().disable()
                .formLogin()
                .loginPage("/auths/login-form")
                .loginProcessingUrl("/process_login")
                .failureUrl("/auths/login-form?error")
                .and()
                .logout()                        // 1
                .logoutUrl("/logout")            // 2
                .logoutSuccessUrl("/")  // 3
                .and()
                .exceptionHandling().accessDeniedPage("/auths/access-denied")
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers("/orders/**").hasRole("ADMIN")
                        .antMatchers("/members/my-page").hasRole("USER")
                        .antMatchers("⁄**").permitAll()
                );
        return logout.build();
    }

 

CORS 설정

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        // 모든 출처에 대해 스크립트 기반의 HTTP 통신 허용
        configuration.setAllowedOrigins(Arrays.asList("*"));
        // 파라미터로 지정한 HTTP Method에 대한 HTTP 통신 허용
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELEE", "GET"));

        // CorsConfigurationSource의 구현체인 UrlBasedCorsConfigurationSource 객체 생성
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 모든 URL에 앞에서 구성한 CORS 정책 적용
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

 

Security Configuration - Password Encrypt 설정

  • PasswordEncoder
    • PasswordEncoderFactories.createDelegatingPasswordEncoder() 를 통해
      DelegatingPasswordEncoder가 PasswordEncoder 구현 객체 생성 
    • User 설정에서 미리 선언한 유저의 PW에도 PasswordEncoder를 통한 암호화 적용
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

 

 

정리

  • UserDetailsManager - Spring Security의 User를 관리, 하위 타입 -> InMemoryUserDetailsManager
    • UserDetails 로 User 생성, User 정보 관리
    • USerDetailService - User 정보를 로드하는 인터페이스
  • UserDetailsService - 유저 정보 로드
  • PasswordEncoder - 패스워드 암호화
  • AuthorityUtils - 권한 목록 생성용 클래스
    • 리턴 List<GrantedAuthority>
  • CustomAuthorityUtils
    • List<GrantedAuthority> 를 이용하여 권한 생성
    • 검증에 필요한 필드를 이용해 유저의 권한을 생성, 매핑
// 유형별 권한 생성

@Component
public class CustomAuthorityUtils {

//    @Value("${mail.address.admin}")
//    private String adminMailAddress;

    // 메모리 저장용
    private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
    private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");

    // DB 저장용
    private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
    private final List<String> USER_ROLES_STRING = List.of("USER");



    // 메모리 상의 Role 기반으로 권한 생성
    public List<GrantedAuthority> createAuthorities(String email) {

        if (email.equals(adminMailAddress)) {
            return ADMIN_ROLES;
        }
        return USER_ROLES;
    }

    // DB에 저장된 Role을 기반으로 권한 정보 생성
    public List<GrantedAuthority> createAuthorities(List<String> roles) {

        List<GrantedAuthority> authorities = roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_"+role))
                .collect(Collectors.toList());
        return authorities;
    }

    // DB 저장용 Role 생성 검증 요소는 알아서 필드 선정
    public List<String> createRoles(String email) {

        if (email.equals(adminMailAddress)) {
            return ADMIN_ROLES_STRING;
        }
        return USER_ROLES_STRING;
    }
}
  • CustomUserDetails implements UserDetailsService
    • DB에서 User를 조회하고, 조회한 USer의 권한 정보를 생성하기 위해 MemberRepository와
      HelloAuthorityUtils 클래스를 주입
/* DB조회 멤버 -> Spring Security User로의 변환과정 캡슐화 */

@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final CustomAuthorityUtils authorityUtils;

    // User의 Role 권한을 생성하기 위해 DI 받음
    public CustomUserDetailsService(MemberRepository memberRepository, CustomAuthorityUtils authorityUtils) {
        this.memberRepository = memberRepository;
        this.authorityUtils = authorityUtils;
    }


    // DB의 정보를 기반으로 유저의 인증 처리

    @Override // UserDetailsService의 구현 메서드
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Optional<Member> optMember = memberRepository.findByEmail(username);

        Member findMember = optMember.orElseThrow(
                () -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

        Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());

        // 리팩터링 포인트
//        return new User(findMember.getEmail(), findMember.getPassword(), authorities);
        return new CustomUserDetails(findMember);
    }

    // 유저 정보 생성 내부 클래스
    private final class CustomUserDetails extends Member implements UserDetails {

        CustomUserDetails(Member member) {
            setMemberId(member.getMemberId());
            setName(member.getName());
            setEmail(member.getEmail());
            setPassword(member.getPassword());
            setRoles(member.getRoles());
        }


        // CustomAuthorityUtils의 메서드를 이용해 User의 권한 정보 생성
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorityUtils.createAuthorities(this.getRoles());
        }

        @Override
        public String getUsername() {
            return getEmail();
        }

        @Override
        public boolean isAccountNonExpired() {
            return true;
        }

        @Override
        public boolean isAccountNonLocked() {
            return true;
        }

        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }

        @Override
        public boolean isEnabled() {
            return true;
        }
    }
}
  • DBMemberService implements MemberService
    • User의 인증 정보를 DB에 저장
/* DB에 User를 등록하기 위한 클래스 */

public class DBMemberService implements CustomMemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final CustomAuthorityUtils authorityUtils;

    public DBMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder, CustomAuthorityUtils authorityUtils) {
        this.memberRepository = memberRepository;
        this.passwordEncoder = passwordEncoder;
        this.authorityUtils = authorityUtils;
    }

    @Override
    public Member createMember(Member member) {
        // 멤버 검증
        verifyExistsEmail(member.getEmail());

        String encryptedPassword = passwordEncoder.encode(member.getPassword());

        member.setPassword(encryptedPassword);

        // 권한 정보를 DB에 저장
        List<String> roles = authorityUtils.createRoles(member.getEmail());
        member.setRoles(roles);

        Member savedMember = memberRepository.save(member);

        System.out.println("# Create Member in DB");
        return savedMember;
    }
}
  • Spring Security에서 SimpleGrantedAuthority 를 사용해 Role 베이스 형태의 권한을 지정할 때
    ‘Roll_’ + 권한명 형태로 지정해야함
  • AuthenticationProvider - 직접 인증 처리
/* 직접 로그인 인증 방식 */

@Component
public class UserAuthenticationProvider implements AuthenticationProvider {

    private final CustomUserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public UserAuthenticationProvider(CustomUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        // 파라미터값을 캐스팅해 UsernamePasswordAuthenticationToken 을 얻음
        UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;

        // 위에서 얻은 토큰
        String username = authToken.getName();
        Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid Name or Password"));

        try {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            String password = userDetails.getPassword();
            verifyCredentials(authToken.getCredentials(), password);

            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();

            return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
        } catch (Exception e) {
            throw new UsernameNotFoundException(e.getMessage());
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }

    private void verifyCredentials(Object credentials, String password) {
        if (!passwordEncoder.matches((String)credentials, password)) {
            throw new BadCredentialsException("Invalid User name or User Password");
        }
    }
}

 

Filter Chain

profile

우주먼지

@o귤o

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그