image

예전보다 월-금 시간이 정말 빨리간다.
그만큼 공부하는 시간을 즐기고 있으니 시간도 빨리가는 것 같다.

오늘도 이번주의 마지막이니 만큼
더 집중해서 공부해보자 !


오늘은 Spring Security에 대해 공부하는 날이다.
다음주에도 계속 공부할 것 같고
오늘은 기본 구조와 웹 요청 처리 흐름에대해 공부해보려한다.

Spring Security

Spring Security란 ?
Spring MVC 기반 애플리케이션의 인증,인가 기능을
지원하는 보안 프레임워크이다.

Interceptor나 Servlet Filter과 같은 보안 기능을
직접 구현할 수 있지만 Spring Security에서 안정적으로 지원하고 있기 때문에
Spring MVC를 기반으로한다면 Spring Security를 이용하는게 안전한 선택이라 할 수 있다.

스프링 시큐리티를 이용한다면 ?

  1. 다양한 유형의 사용자 인증 기능 적용 가능 (폼 로그인, 토큰, OAuth 2기반 인증)
  2. 애플리케이션 사용자의 역할에 따른 권한 레벨 적용
  3. 애플리케이션에서 제공하는 리소스 대한 접근 제어
  4. 민감한 정보에 대한 데이터 암호화
  5. 일반적으로 알려진 웹 보안 공격 차단

스프링 시큐리티 관련 용어를 미리 숙지하고
본격적인 내용을 살펴보면 도움이 될 것이다.

  1. Principal (주체)
    -> 애플리케이션에서 작업을 수행할 수 있는 사용자,디바이스,시스템 등이 될 수 있다.
    인증 프로세스가 성공적으로 수행된 사용자의 계정 정보를 의미함.
  2. Authentication (인증)
    -> 애플리케이션을 사용하는 사용자가 본인이 맞음을 증명하는 절차
  3. Authorization (인가)
    -> 인증이 정상적으로 수행된 사용자에게 하나 이상의 권한을 부여함.
    특정 애플리케이션에 특정 리소스에 접근할 수 있게 허가하는 과정을 의미한다.
  4. Access Control (접근제어)
    -> 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것을 의미한다.
  5. Credential (신원 증명정보)
    -> 인증을 정상적으로 수행하기 위해서 식별하기 위한 정보.


Spring Security 흐름

사용해보기에 앞서
SSR방식으로 테스트를 진행할 예정이다.
별도의 html을 만들어서 테스트를 진행했고
Form Login 방식으로 테스트를 진행했다.

간단한 스프링 시큐리티의 보안 적용 흐름을 알고 넘어가보자.
우리는 보통 웹에서 요청을 보내면 EndPoint를 거쳐 리소스에 접근을 한다.

Servlet 기반의 애플리케이션일 경우 EndPoint에 요청이 도달하기전에 요청을 가로챈 후
어떤 처리를 할 수 있는 적절한 포인트를 제공해줄 수 있는 API가 있다.
그것은 바로 Servlet Filter이다.

Servlet Filter를 잠깐 정리하자면

  1. Java에서 Interface형태로 제공하는 API이다.
  2. 웹요청을 가로채 전처리, 응답을 전달하기전 후처리를 할 수 있다.

이러한 Servlet Filter는 하나 이상의 필터들을 연결해
Filter Chain을 구성할 수 있다.

image
출처 - Spring Security 레퍼런스


Filter Chain의 Filter들은 우리가 구현한 작업들을 실행한 후에
HttpServlet을 거쳐 DispatcherServlet에 요청이 전달되어진다.

DispatcherServlet은 Spring MVC 계층구조할때 많이 보았을 것이다.
클라이언트에서 받은 모든 요청을 적합한 컨트롤러에 위임해주는
Spring MVC에서 아주 중요한 역할을 가지고 있는 녀석이였다.

여하튼 우리가 알아야할 핵심은 DispatcherServlet에 도달하기 전에
Servlet Filter가 가로챈다는 것이다.


그럼 Spring Security와 Servlet Filter와 어떠한 관계가 있냐?

Spring Security에서는 보안과 관련된 Filter를 이용해
클라이언트에 요청을 중간에 가로챈 뒤 추가적으로 작업을 할 수 있도록 도와준다.

image
출처 - Spring Security 레퍼런스


그것을 도와주는 클래스들이 바로 위에있는 아키텍쳐에서 보는
DelegatingFilterProxy와 FilterChainProxy이다.
해당 클래스들은 Filter 인터페이스를 구현하고 있어
Servlet Filter로써의 역할을 수행해한다.

DelegatingFilterProxy는 Servlet Container 영의 Filter와
ApplicationContext에 Bean으로 등록된 Filter들을 연결해주는 역할을 가진다.

FilterChainProxy는 Spring Security를 사용하기 위한 진입점이다.

image
출처 - Spring Security 레퍼런스


위의 그림에서 보듯이 Spring Security의 Filter Chain으로
보안을 위한 작업을 처리하는 필터의 모음을 만들면
FilterChainProxy로 부터 해당 Filter들을 시작할 수 있게되어진다.

한마디로 FilterChainProxy부터 Spring Security에서 제공하는
보안 Filter들이 필요한 작업을 수행해주게 도와주는 클래스라 보면된다.


Spring Security 연습해보기

위의 Spring Security 요청 흐름에 대해 어느정도
이해가 되었다면, 아래 코드들을 보고
어느 부분인지 정확히는 몰라도 흐름은 파악이 가능하다.

우선 Spring Security를 사용하기 위한 의존라이브러리를 추가해야한다.

implementation 'org.springframework.boot:spring-boot-starter-security'

build.gradle에 의존라이브러를 추가시켜 놓으면 준비 끝이다.

처음으로 우선 위에서 보았던
SecurityFilterChain을 구성해보자

@Configuration
public class SecurityConfiguration {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable() // csrf 공격에 대한 설정 비활성화
                .formLogin()// Form 로그인 인증 방법
                .loginPage("/auths/login-form") // 로그인 커스텀 페이지 설정 (우리가만든 경로설정)
                .loginProcessingUrl("/process_login")// 인증요청을 수행할 요청 URL 설정 (html form태그에서 요청)
                .failureUrl("/auths/login-form?error")// 실패시 리다이렉트할 URL 지정
                .and() // 보안설정을 메서드 체인형태로 구성하게 도와줌
                .logout() // 로그아웃 설정위한 LogoutConfigurer를 리턴해줌
                .logoutUrl("/logout") // 로그아웃 요청 url 설정 (html form태그에서 요청)
                .logoutSuccessUrl("/") // 성공시 리다이렉트할 장소
                .and() // 보안설정을 메서드 체인형태로 구성하게 도와줌
                .exceptionHandling().accessDeniedPage("/auths/access-denied") // 권한이 없을때 보낼 요청
                .and() // 보안설정을 메서드 체인형태로 구성하게 도와줌
                .authorizeHttpRequests(authorize ->
                        authorize.antMatchers("/orders/**").hasRole("ADMIN") // 해당 경로에 ADMIN 권한만 접근가능
                                .antMatchers("/members/my-page").hasRole("USER") // 해당 경로에 USER 권한만 접근가능
                                .antMatchers("/**").permitAll() // 앞에지정한 URL을 제외한것 접근 모두가능
                );
//                .authorizeHttpRequests() // 클라이언트의 요청이 들어오면 접근권한을 확인하겠다고 정의
//                .anyRequest() // 클라이언트의 모든 요청에 대해 접근을 허용
//                .permitAll(); // 클라이언트의 모든 요청에 대해 접근을 허용
        return http.build();
    }
}

기본적인 구성방법은 SecurityFilterChain 클래스 타입을 반환하는
메서드를 하나 만들어 filter의 정보를 넣어주는 것이다.
그리고 해당 메서드를 Spring Container에 Bean으로 담아주면된다.

빈으로 담아주면 아까 위에서 얘기했던
DelegatingFilterProxy 클래스에 의해
서블릿 컨테이너 영역의 필터와 스프링 컨테이너의 빈으로 등록된 필터를
연결해주는 브릿가 생긴다고 예상해볼 수 있을 것 같다.

그리고 SSR방식의 FormLogin을 사용한다고 가정했으니
첫번째 필터로는 로그인에 해당하는 내용들
두번째 필터로는 로그아웃에 해당하는 내용들
세번째로는 권한이 없을때 처리하는 내용
네번째로는 클라이언트 요청에따른 권한 설정에 대한 내용
이렇게 각각 필터들을 하나의 필터체인으로 .build(); 하여 반환하면
해당 내용에 따라 인증과 권한 부여가 이루어질 수 있다.


In Memory로 인증 해보기

DB를 이용하지 않는 인증방법을 한번 연습해보자

1). 단순하게 고정해서 등록해 인증과 권한을 주는 방식

SecurityConfiguration 클래스 아래에
UserDetailsManager 반환하는 Bean 객체를 만들어주었다.

UserDetailsManager 라는 인터페이스를
구현한 객체를 Bean으로 등록해주면 해당 정보로 등록된
사용자들은 정보에 해당하는 인증과 권한을 가지게 된다.

 @Bean
 public UserDetailsManager userDetailsService(){
     UserDetails user = User.withDefaultPasswordEncoder() // password 암호화
             .username("dhfif718@naver.com") // 식별하는 사용자 아이디 값
             .password("1111")
             .roles("USER") // 권한지정
             .build();
     UserDetails admin = User.withDefaultPasswordEncoder()
             .username("admin@naver.com")
             .password("2222")
             .roles("ADMIN")
             .build();

     return new InMemoryUserDetailsManager(user,admin);
 }

InMemoryUserDetailsManager 구현체로 구성 정보를 넘겨주면 된다.
구성 정보를 만드는 방법은 UserDetails 인터페이스를 상속받는
User 클래스를 이용해 만들 수있다.

만약 클라이언트에서 form login 방식으로 로그인 요청이 온다면
해당 구성정보를 가진 사용자는 로그인이 되며
권한을 각각 USER, ADMIN으로 부여받게 되어진다.

위에 있는 정보는 DB를 이용한 것도 아니며
단순하게 UserDetailsManager 타입의 Bean을 만들어 주 었을 뿐인데
Spring Security는 해당 정보를 가진 사용자가 로그인을 하면
인증을 해주고 권한을 부여해줄 수 있는 것이다.

여기서 User.withDefaultPasswordEncoder() 메서드를 사용하면
bcrypt 방식으로 password를 암호화해준다. 즉, 1111,2222로 들어온 값을
암호화하여 관리를 한다는 의미이다.

만약 코드로 치게된다면 Deprecated 상태로 변하는데 (가운데줄 쳐짐)
해당 API는 권장하지 않는다는 뜻이다. 이 경우에는 고정으로 사용하고 있기에
고정해서 사용하지 말라는 의미에서 Deprecated로 변한 것을 볼 수 있다.


2). 회원 가입을 이용해 등록하기

기본적으로 password를 암호화를 해야한다.
암호화를 하기위해 위에서 이용했던 bcrpty 방식으로 암호화를
도와주는 API가 있다.

 /*
 * PasswordEncoder Bean 등록
 * 암호화를 해야한다. Spring Security에서 지원하는 Default = bcrypt
 * */
 @Bean
 public PasswordEncoder passwordEncoder(){
     return PasswordEncoderFactories.createDelegatingPasswordEncoder();
 }

SecurityConfiguration 클래스에
PasswordEncoder 인터페이스 타입 객체를 Bean으로 등록해주고
회원가입시 사용할 수있다.

회원가입할 경우 Controller는 구성되어 있다고 가정하고
Service 코드를 확인해보자

/*
*  InMemory 사용을 위한 빈주입
* */
//@Configuration
public class JavaConfigurationV1 {

    @Bean
    public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
        return new InMemoryMemberService(userDetailsManager,passwordEncoder);
    }
}

MemberService라는 인터페이스 타입으로 객체를 Bean으로 등록해준다.

InMemoryMemberSerivce에 우리가 이용해야할
UserDetailsManager와 PasswordEncoder를 매개변수로 넣어
생성자로 반환하게 되어진다.

그럼 Bean으로 InMemoryMemberService을 구현체로
의존성 주입하여 Controller에서 이용이 가능하다..


public class InMemoryMemberService implements MemberService {

   private final UserDetailsManager userDetailsManager;
   private final PasswordEncoder passwordEncoder;

   public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
      this.userDetailsManager = userDetailsManager;
      this.passwordEncoder = passwordEncoder;
   }

   @Override
   public Member createMember(Member member) {

      List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name()); // 권한 USER로 설정

      String encryptedPassword = passwordEncoder.encode(member.getPassword()); // 비밀번호 암호화 진행

      UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities); // username, password, 권한 정보로 유저 생성
      userDetailsManager.createUser(userDetails); // 생성한 유저정보 등록

      return member;
   }

   private List<GrantedAuthority> createAuthorities(String... roles) {

      return Arrays.stream(roles)
              .map(role -> new SimpleGrantedAuthority(role))
              .collect(Collectors.toList());
   }
}

여기서 회원가입을 위한 서비스 로직 createMember
안의 내용을 들여다보면된다.

첫번째로 권한에 대한 내용을 설정하기위해
new SimpleGrantedAuthority();을 이용해 객체를
List 형태로 담아둔 authorities를 만들었다.

두번째로 암호화를 위해 DI받은 passwordEncoder의 메서드인
.encode();를 이용해 비밀번호를 넘겨줘
bcrypt 방식으로 암호화하여 String 타입으로 받아두었다.

마지막으로 new User();를 이용해
위에서 고정으로 구성정보를 구성한 것 처럼

username, password, roles를 생성자로 넘겨
UserDetails 타입의 객체로 만들어
위에서 DI 받은 userDetailsManger의 메서드
createUser();의 매개변수로 넘겨주게 되면
회원 가입을 할때 해당 정보를 가진 Member가 인증과 권한정보를
가지게되어 로그인할때 해당 정보를 가진 사용자가 로그인을 한다면
인증이되어 설정한 ROLE_USER 권한으로 이용이 가능하게되어진다.



위의 1,2번을 한마디로 정리 해보자면 !!

UserDetails 객체를 만들어 줘야한다.
그리고 그 객체를 이용해 UserDetailsManager의 구현체인
InMemoryUserDetailsManager .createUser(); 메서드에
매개변수로 UserDetails 객체를 넘겨주는게 끝이다 !


H2 DB로 인증 해보기

위에서 UserDetailsManager를 이용해 인증과 권한을 가졌다면

DB를 이용하기 위해서는 UserDetailsService 인터페이스를 이용해야한다.
UserDetailsManager가 UserDetailsService를 상속받은 관계이다.

우선 H2 DB 설정과 관련된 부분은 되어있다고 가정하겠다.
문제없이 H2 DataBase를 사용하기 위해서는
기존에 설정했던 SecurityFilterChain에서 한가지 Filter를 추가해줘야한다.

@Configuration
public class SecurityConfiguration {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .headers().frameOptions().sameOrigin()  // <frame> 태그등 렌더링여부 설정, 동일 출처로부터 들어오는 request만 렌더링허용 , H2 사용을 위함
                .and() // 보안설정을 메서드 체인형태로 구성하게 도와줌
                .csrf().disable() // csrf 공격에 대한 설정 비활성화
                .formLogin()// Form 로그인 인증 방법
                .loginPage("/auths/login-form") // 로그인 커스텀 페이지 설정 (우리가만든 경로설정)
                .loginProcessingUrl("/process_login")// 인증요청을 수행할 요청 URL 설정 (html form태그에서 요청)
                .failureUrl("/auths/login-form?error")// 실패시 리다이렉트할 URL 지정
                .and() // 보안설정을 메서드 체인형태로 구성하게 도와줌
                .logout() // 로그아웃 설정위한 LogoutConfigurer를 리턴해줌
                .logoutUrl("/logout") // 로그아웃 요청 url 설정 (html form태그에서 요청)
                .logoutSuccessUrl("/") // 성공시 리다이렉트할 장소
                .and() // 보안설정을 메서드 체인형태로 구성하게 도와줌
                .exceptionHandling().accessDeniedPage("/auths/access-denied") // 권한이 없을때 보낼 요청
                .and() // 보안설정을 메서드 체인형태로 구성하게 도와줌
                .authorizeHttpRequests(authorize ->
                        authorize.antMatchers("/orders/**").hasRole("ADMIN") // 해당 경로에 ADMIN 권한만 접근가능
                                .antMatchers("/members/my-page").hasRole("USER") // 해당 경로에 USER 권한만 접근가능
                                .antMatchers("/**").permitAll() // 앞에지정한 URL을 제외한것 접근 모두가능
                );
//                .authorizeHttpRequests() // 클라이언트의 요청이 들어오면 접근권한을 확인하겠다고 정의
//                .anyRequest() // 클라이언트의 모든 요청에 대해 접근을 허용
//                .permitAll(); // 클라이언트의 모든 요청에 대해 접근을 허용
        return http.build();
    }
}

.headers().frameOptions().sameOrigin()
메서드들이 추가되었고, 추가하지 않으면

image

위의 화면과 같이 정상적으로 H2 DataBase가 표시되지 않을 수 있다.


/*
*  DB 사용을 위한 빈주입
* */
@Configuration
public class JavaConfigurationV2 {

    @Bean
    public MemberService dbMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
        return new DBMemberServiceV1(memberRepository,passwordEncoder);
    }
}

기존에 InMemory를 사용하기 위해 설정했던 구성정보를 지우고
새로운 구성정보를 설정. DBMemberService가 적용되도록 스프링 빈으로 등록했다.

@Transactional
public class DBMemberServiceV1 implements MemberService {

   private final MemberRepository memberRepository;
   private final PasswordEncoder passwordEncoder;

   public DBMemberServiceV1(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
      this.memberRepository = memberRepository;
      this.passwordEncoder = passwordEncoder;
   }

   @Override
   public Member createMember(Member member) {
      verifyExistsEmail(member.getEmail());
      String encryptedPassword = passwordEncoder.encode(member.getPassword()); // 비밀번호 암호화
      member.setPassword(encryptedPassword); // 암호화된 비밀번호 객체에 맵핑

      Member saveMember = memberRepository.save(member); // DB에 저장

      return saveMember;
   }

   private void verifyExistsEmail(String email) {
      Optional<Member> member = memberRepository.findByEmail(email);
      if (member.isPresent())
         throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
   }
}

DBMemberService에서는
비밀번호 암호화를 한 것을 member 객체에 담아
Repository에 저장하게만 구성해주면 끝난다.

인메모리 사용할때에는 UserDetailsManager에다가 저장했지만
이제는 DBMemberService는 회원가입이라는
서비스 로직만 남게 된 것이다.

이제 회원가입을하였고
로그인을 하였을때 UserDetailsManager와 같은 역할을 구현해줘야한다.

/*
* DB사용했을때 인증 처리 적용하기 (인메모리 사용시 기존 UserDetailManager를 사용했었음)
* UserDetailsService는 User정보를 로드하는 핵심적인 인터페이스다.
* UserDetailManager는 UserDetailsService를 상속하고있음
* */
@Component
public class HelloUserDetailsServiceV1 implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final HelloAuthorityUtils authorityUtils;

    public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
        this.memberRepository = memberRepository;
        this.authorityUtils = authorityUtils;
    }

    /*
    * UserDetailService 오버라이딩하여 UserDetails 객채를 만들어 반환하기
    * 이후는 스프링시큐리티가 인증처리를 대신해준다
    * */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 해당 메서드 호출 시점은, login.html에서 로그인 버튼 누를떄이다.
        // 즉, <form action="/process_login" method="post"> 가 실행될떄 호출됨
        Optional<Member> optionalMember = memberRepository.findByEmail(username); // 이메일로 멤버찾기
        Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); // 멤버 찾기 예외 처리
        Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail()); // 이메일 비교후 권한 처리
        UserDetails user1 = new User(findMember.getEmail(), findMember.getPassword(), authorities); // 해당 정보로 User생성자로 UserDetails 만들기
        return user1;
    }
}

우리는 UserDetailsService를 구현할 클래스를 만들어
상속을 받게 만들어준다음. loadUserByUsername 오버라이딩해
재정의를 해서 반환을 UserDetails 객체로 만들어 반환을 해주면된다.

회원가입은 이미 되어있고, 암호화까지해 놓은 상태로 DB에 저장되어있다.
우리가 로그인 페이지에서 form login으로 post 메서드 /process_login을 호출하게되면
우리가 위에서 봤던 흐름대로 EndPoint까지 가기전에
Servlet Filter가 흐름을 가로채서 실행하게 되고
우리는 Spring Security를 사용하고 있기때문에 인증 절차를 가지게된다.

image

인증 절차는 실제로 이러한 흐름을 진행된다고한다.

  1. 사용자가 로그인 정보와 함께 인증 요청을 한다.(Http Request)

  2. AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해
    UsernamePasswordAuthenticationToken의 인증용 객체를 생성한다.

  3. AuthenticationManager의 구현체인 ProviderManager에게 생성한
    UsernamePasswordToken 객체를 전달한다.

  4. AuthenticationManager는 등록된 AuthenticationProvider(들)을 조회하여 인증을 요구한다.

  5. 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.

  6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만든다.

  7. AuthenticationProvider(들)은 UserDetails를 넘겨받고 사용자 정보를 비교한다.

  8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.

  9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환된다.

  10. Authenticaton 객체를 SecurityContext에 저장한다.

출처 - 슬기로운 개발생활


즉, 우리가 구현해줘야할 부분은
5번에서 사용자 정보를 받는 것을 이용해
6번, 받은 정보를 이용해 UserDetails 객체를 반환만 시켜주면
그 이후는 7번 부터 Spring Security가 알아서 진행시켜준다.

이를 토대로 위에 프로그램을 작성해 놓은 것을 보면
오버라이딩한 loadUserByUsername 메서드를 통해서
username을 매개변수로 정보를 받는다.
해당 정보로 DB를 조회한다음, UserDetails 객체를 반환시켜주는 프로그램이다.


위의 얘기를 한마디로 정리 해보자면 !!

UserDetailsService 인터페이스를 구현한 클래스를 만든다.
loadUserByUsername(); 메서드를 오버라이딩하여 매개변수로 username 정보를 받을 수 있다.
username정보를 이용해 DB에 조회를 한다.
조회한 데이터를 토대로 UserDetails 객체를 만들어 반환만 시켜주면 된다.
이후는 Spring Security가 알아서 해줌


오늘 만난 ERROR

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.codestates.member.MemberController required a single bean, but 2 were found:
	- inMemoryMemberService: defined by method 'inMemoryMemberService' in class path resource [com/codestates/config/JavaConfigurationV1.class]
	- dbMemberService: defined by method 'dbMemberService' in class path resource [com/codestates/config/JavaConfigurationV2.class]

오늘 테스트 중 만났던 에러다.
내용은 MemberController에서 싱글빈을 요구한는데 2개가 있다는 것 같은 내용이다.
즉, 같은 타입의 빈이 두개가 등록된 문제라고 생각했다.

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

밑에 아주친절하게 @Primary나 @Qualifier를 사용하라한다.


/*
*  InMemory 사용을 위한 빈주입
* */
@Configuration
public class JavaConfigurationV1 {
    @Bean
    public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
        return new InMemoryMemberService(userDetailsManager,passwordEncoder);
    }
}
/*
*  DB 사용을 위한 빈주입
* */
@Configuration
public class JavaConfigurationV2 {
    @Bean
    public MemberService dbMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
        return new DBMemberService(memberRepository,passwordEncoder);
    }
}

MemberControler에서는 MemberService를 DI받게 설계해놓았고

MemberService라는 인터페이스를 구현한
클래스 inMemoryMemberServie, DBMemberService 두개를
빈으로 등록하려고 시도하니까 발생한 알람인 것 같다.

여기서 @Configuration을 한군데를 지워주면
중복되지 않고 해결 할 수 있다.

하지만 코드는 그대로 유지싶었기에

@Configuration
public class JavaConfigurationV2 {
    @Bean
    @Primary
    public MemberService dbMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
        return new DBMemberService(memberRepository, passwordEncoder);
    }
}

JavaConfigurationV2에 빈등록 메서드에 @Primary를 이용해
빈을 선택해 주입해주도록 정했다.


오늘은 이렇게 스프링 시큐리티의 기본적인
사용방법을 알아보았다.

내부적으로 이루어지는 작업들이 많아 정확한
흐름이 감이 잡히질 않는 것 같다.
글을 적으면서도 내가 정확히 이해한게 맞는지 수십번을 돌아가서 확인하고 반복했다.
확인했다고 한들… 정확한 정보를 적은 것 같지 않은 느낌이든다.
계속 익숙해지게 사용해보면서 사용법에 익숙해져보자

오늘공부는 여기서 끝 !!


오늘의 커피량: ☕️ ☕️
오늘의 점심: 짜장면, 탕수육