본문 바로가기
Spring/Spring Security

[Spring Security] Spring Security의 기본 구조, 동작 방식 (2) - Database User

by jungha_k 2022. 11. 21.

 

 

[Spring Security] Spring Security의 기본 구조, 동작 방식 (1) -InMemory User

Spring Security 흐름 적용을 위해 CSR 방식이 아닌 SSR 방식을 이용함 각각의 화면에 맞는 html 파일이 있다! (타임리프 방식) 화면 설명 ⬇ 커피 보기 화면 - 모든 사용자 접근 가능 전체 주문 목록 보기

wlikeoxy.tistory.com

 

이 글에 이어서 작성!

 

애플리케이션 사용자에 Spring Security 를 적용시키는 방법을 두 가지 배웠다.

이번에는 InMemory User 가 아닌

DB에 저장되는 User 인증 방식을 정리한다.

 


 

Custom UserDetailsService 

: User의 인증정보를 테이블에 저장, 

테이블에 저장된 인증 정보를 이용해 인증 프로세스 진행

 

 

* User  : 인증을 시도하는 주체

(≒ Principal : User의 구체적인 정보 ex.Username)

 

Member 엔티티 - Spring Security 의 User 정보를 포함한다.

 

 

 

1. Security Configuration 설정 변경 및 추가 

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers().frameOptions().sameOrigin() // 동일 출처 request 만 렌더링 허용
            .and()
            .csrf().disable()
            .formLogin()
            .loginPage("/auths/login-form")
            .loginProcessingUrl("/process_login")
            .failureUrl("/auths/login-form?error")
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessUrl("/")
            .and()
            .exceptionHandling().accessDeniedPage("/auths/access-denied")
            .and()
            .authorizeHttpRequests(authorize -> authorize
                    .antMatchers("/orders/**").hasRole("ADMIN")
                    .antMatchers("/members/my-page").hasRole("USER")
                    .antMatchers("⁄**").permitAll()
            );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

 

: 기존 InMemory 방식은 userDetailsService() 메서드에서 User 등록을 해줬지만

이제 User 등록은 DB에서 하게 될 것이므로 userDetailsService() 메서드 제거!

 

 

 

2. JavaConfiguration의 Bean 등록 변경 

@Configuration
public class JavaConfiguration {

	//DI 변경 ✨
	//inMemoryMemberService ➡ dbMemberService
    	//userDetailsManager ➡ memberRepository
    
    @Bean
    public MemberService dbMemberService(MemberRepository memberRepository,
                                         PasswordEncoder passwordEncoder) {
        return new DBMemberService(memberRepository, passwordEncoder);
    }
}

 

 

3. DBMemberService 구현

(이전에 만들어놨던 MemberService 인터페이스의 구현체)

 

@Transactional
public class DBMemberService implements MemberService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    // DI
    public DBMemberService(MemberRepository memberRepository,
                             PasswordEncoder passwordEncoder) {
        this.memberRepository = memberRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());
        //패스워드 암호화
        String encryptedPassword = passwordEncoder.encode(member.getPassword());  
        member.setPassword(encryptedPassword);    //암호화된 패스워드 필드에 할당

        Member savedMember = memberRepository.save(member);

        System.out.println("# Create Member in DB");
        return savedMember;
    }

 

 

4.Custom UserDetailsService 구현 (= HelloUserDetailsService)

: db에서 조회한 User의 인증 정보 기반 ➡ 인증 처리

 

 

기존의 InMemory 방식: InMemoryUserDetailsManager 

= UserDetailsManager 인터페이스의 구현체

 

UserDetailsManager : UserDetailsService 를 상속하는 확장 인터페이스

 

@Component
public class HelloUserDetailsService implements UserDetailsService { //Custom UserDetailService
    private final MemberRepository memberRepository;
    private final HelloAuthorityUtils authorityUtils;

    // DI
    public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
        this.memberRepository = memberRepository;
        this.authorityUtils = authorityUtils; //User Role 정보 생성
    }

    // loadUserByUsername(String username) : 추상 메서드 구현
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> optionalMember = memberRepository.findByEmail(username);
        Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

        //✨ 조회한 회원의 이메일 정보 ➡ 권한 정보 컬렉션 생성(GrantedAuthority)
        Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());

        // Spring Security 가 인증 절차 수행
        return new User(findMember.getEmail(), findMember.getPassword(), authorities);
    }
}

 

: db 가 User의 인증 정보만 Spring Security 에게 넘겨주고,

인증 처리를 Spring Security 가 대신 해준다. 

 

 

* authorityUtils : User의 권한을 매핑, 생성

@Component
public class HelloAuthorityUtils {
    // application.yml 프로퍼티 **
    @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");
    
    public List<GrantedAuthority> createAuthorities(String email) {
        // 파라미터 이메일 주소 = 관리자용 이메일 주소?
        if (email.equals(adminMailAddress)) {
            return ADMIN_ROLES; //관리자 권한 ㄱㄱ
        }
        return USER_ROLES; //아니라면 일반 사용자 권한 ㄱㄱ
    }
}

 

⬇ 

* application.yml  파일에

mail:
  address:
    admin: admin@gmail.com

이런 식으로 관리자용 이메일을 지정해줄 수 있다.

 

 


* HelloUserDetailsService 리턴 부분 리팩토링

 

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> optionalMember = memberRepository.findByEmail(username);
        Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
        Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember);

        // 기존 코드 🔥
        return new User(findMember.getEmail(), findMember.getPassword(), authorities);
        
        ⬇
        
        //개선된 코드 🔥
         return new 💥 HelloUserDetails(findMember);
         
        
        // 💥 HelloUserDetails 클래스 추가
    private final class HelloUserDetails extends Member implements UserDetails { 
    
        HelloUserDetails(Member member) {
            setMemberId(member.getMemberId());
            setName(member.getName());
            setEmail(member.getEmail());
            setPassword(member.getPassword());
        }

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorityUtils.createAuthorities(this.getEmail());  
        }

        @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;
        }
    }
    }

 

HelloUserDetails 클래스 : UserDetails 인터페이스를 구현

: User 정보 변환 과정, User 권한 정보 생성 과정 캡슐화 가능

 


현재까지의 User 권한 정보 :

db에서 관리되는 것이 아닌,  db에서 조회한 User 정보를 기준으로 ➡ 코드 상에서 조건에 맞게 생성했었다!

(yml 프로퍼티와 동일하면 admin~ 아니면 user~)

 

 

User의 권한 정보를 db에서 관리하도록 변경

 

 

처리 과정

  • User의 권한 정보를 저장하기 위한 테이블 생성
  • 회원 가입 시, User의 권한 정보(Role)를 데이터베이스에 저장하는 작업
  • 로그인 인증 시, User의 권한 정보를 데이터베이스에서 조회하는 작업

 

 

1. User의 권한 정보를 저장하기 위한 테이블 생성

 

: User ~ User 권한 정보 간에 관계 맺기 (테이블 간 연관관계)

JPA 를 이용해서 연관 관계 맺기!

 

 

Member 엔티티 클래스 

public class Member extends Auditable implements Principal{
    ...
    ...
    
    // User의 권한 정보 테이블과 매핑되는 정보
    @ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles = new ArrayList<>();

    ...
    
    }

 

* MEMBER_ROLE 엔티티 클래스를 새로 만들어야 하지 않나? 🤔

@ElementCollection : List, Set 과 같은 컬렉션 타입 필드는

별도의 엔티티 클래스 생성하지 않아도 간단하게 매핑 처리 가능! (테이블 자동 생성됨)

 

 

 

2. 회원 가입 시, User의 권한 정보(Role)를 데이터베이스에 저장하는 작업

 

 

DBMemberService 

@Transactional
public class DBMemberService implements MemberService {
    ...
    ...
  
    private final HelloAuthorityUtils authorityUtils;

    ...
    ...

	//회원 등록 시 권한 정보 DB에 저장
    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());
        String encryptedPassword = passwordEncoder.encode(member.getPassword());
        member.setPassword(encryptedPassword);

        // Role을 DB에 저장
        List<String> roles = authorityUtils.createRoles(member.getEmail());
        member.setRoles(roles);

        Member savedMember = memberRepository.save(member);

        return savedMember;
    }
}

 

+ * authorityUtils 에 createRoles 메서드 추가

 

 

3. 로그인 인증 시, User의 권한 정보를 데이터베이스에서 조회하는 작업

 

 

* HelloUserDetailsService 수정된 부분

...
private final class HelloUserDetails extends Member implements UserDetails {
        HelloUserDetails(Member member) {
            setMemberId(member.getMemberId());
            setName(member.getName());
            setEmail(member.getEmail());
            setPassword(member.getPassword());
            
        //HelloUserDetails 가 Member에 List<String> roles 전달
            setRoles(member.getRoles()); 
          
        }

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            // DB에 저장된 Role 정보로 User 권한 목록 생성 ✨
            return authorityUtils.createAuthorities(this.getRoles());
        }
...

 

+ * HelloAuthorityUtils 에 createAuthorities 메서드가 List<String> roles 를 받도록 설정 

 

 


 

위의 Custom UserDetailsService 이용해서 로그인 인증 처리 ?

Spring Security 가 내부적으로 인증 처리!

 

 

* Custom AuthenticationProvider ? 

직접 로그인 인증 처리 하는 방식

 

@Component
//AuthenticationProvider 의 구현체
public class HelloUserAuthenticationProvider implements AuthenticationProvider {  
    private final MemberService memberService;
    private final HelloAuthorityUtils authorityUtils;
    private final PasswordEncoder passwordEncoder;

    public HelloUserAuthenticationProvider(MemberService memberService,
                                           HelloAuthorityUtils authorityUtils,
                                           PasswordEncoder passwordEncoder) {
        this.memberService = memberService;
        this.authorityUtils = authorityUtils;
        this.passwordEncoder = passwordEncoder;
    }

    // 직접 작성한 인증 처리 로직 
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //토큰 얻기
        UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;

        // 해당 사용자의 username 얻기 - 존재하는지 체크 
        String username = authToken.getName();
        Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));

        // 해당 사용자 조회
        Member member = memberService.findMember(username);

        String password = member.getPassword();
        //로그인 정보 패스워드 = db 저장된 사용자 패스워드 일치? 
        verifyCredentials(authToken.getCredentials(), password); 

		//사용자의 권한 생성
        Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(member.getRoles());  

        //인증된 사용자의 인증정보 리턴
        return new UsernamePasswordAuthenticationToken(username, password, authorities);
    }

    // HelloUserAuthenticationProvider가 Username/Password 방식의 
    //인증을 지원한다는 것을 Spring Security에게 알려준다.
    
    @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");
        }
    }
}

 

 


최종본 코드 기반 정리 🔥

 

📁auth

ㄴ 📁utils - HelloAuthorityUtils

- AuthController : 각종 폼과 화면, html 파일들 매핑

 

- HelloUserDetailsService

: UserDetailsService 구현체 (user 정보 로드하는 핵심 인터페이스)

* memberRepository, authorityUtils DI 받음

(db에서 멤버 찾음,  권한 정보 생성 위해)

 

* UserDetails : 로드되는 핵심 User 정보 표현 인터페이스 

 

- HelloUserAuthenticationProvider : Spring Security 가 아닌 직접 로그인 인증 처리 줄 경우 코드

 

 

📁config

- JavaConfiguration

: memberRepository, passwordEncoder, helloAuthorityUtils DI

해당하는 memberService Bean으로 등록

 

- SecurityConfiguration

SecurityFilterChain, HttpSecurity

1) security 관련 폼 설정, 보안 설정, 권한 uri 설정

2) passwordEncoder Bean으로 등록  

 

 

📁member

- MemberService : 인터페이스

- InMemoryMemberService : Inmemory 방식 (db 저장 X, 코드에서 User 생성 및 권한 부여)

- DBMemberService : DB에 User 정보 저장, role(권한)도 저장

- MemberRepository

 

댓글