Spring Security [기본]

손정훈·2023년 3월 16일
0

Spring Security

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

  • Principal (주체) : 일반적으로 인증 프로세스가 성공적으로 수행된 사용자의 계정 정보를 의미
  • Authentication(인증) : 애플리케이션을 사용하는 사용자가 본인이 맞음을 증명하는 절차
  • Authorization(인가) : 인증이 정상적으로 수행된 사용자에게 권한을 부여하여 특정 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정
  • Access Control(접근 제어) : 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것

InMemory User를 사용하여 Spring Security 적용 (데이터베이스 연동 X)

[ build.gradle ]

dependencies {
	...
    
	implementation 'org.springframework.boot:spring-boot-starter-security'   // spring-boot-starter-security 스타터 추가
}

[ InMemory User 인증 ]

@Configuration
public class SecurityConfiguration {
	@Bean
    public UserDetailsManager userDetailsService() {  // 사용자의 인증 정보를 생성
    	UserDetails userDetails =	// 인증된 사용자의 핵심정보를 포함
        	User.withDefaultPasswordEncoder()	// 사용자의 패스워드를 암호화
            	.username("hoon@gmail.com")		// 사용자 식별
                .password("1111")	// 사용자의 password 설정
                .roles("USER")		// 역할 지정
                .build();
        return new InMemoryUserDetailsManager(userDetails);	// 사용자 인증정보 return
    }
}

[ HTTP 보안 구성 & 커스텀 로그인 페이지 설정 ]

@Configuration
public class SecurityConfiguration {
	@Bean
    public SecurityFilterChain filterchain(HttpSecurity http) thows Excepion {	//  HttpSecurity를 통해 HTTP 요청에 대한 보안 설정을 구성
    	http
    		.csrf().disable()	// csrf 공격에 대한 설정을 비활성화
        	.formLogin()		//	기본 인증 방법을 폼 로그인 방식으로 설정
        	.loginPage("/auths/login-form")			// 로그인 페이지를 /auths/login-form 로 설정
        	.loginProcessingUrl("/process_login")	// 로그인 인증 요청을 수행할 URL 설정
        	.failureUrl("/auths/login-form?error")	// 로그인에 실패할 경우 화면 설정
        	.and()				// spring security 보안 설정 메서드를 체인 형태로 구성
        	.authorizeHttpRequests()	// 요청이 들어오면 접근 권한을 확인 하겠다고 정의
        	.anyRequest()		// 클라이언트의 모든 요청에 대한 접근을 허용
        	.permitAll()		// 클라이언트의 모든 요청에 대한 접근을 허용
    	return http.build();
    }
    
    @Bean
    public UserDetailsManager userDetailsService() {
        // 사용자의 인증 정보를 생성
        UserDetails userDetails =
                User.withDefaultPasswordEncoder()    // 사용자 패스워드를 암호화
                        .username("kevin@gmail.com") // 사용자의 usrname을 설정
                        .password("1111")            // 사용자의 password를 설정
                        .roles("USER")               // 사용자의 역할을 지정
                        .build();
        return new InMemoryUserDetailsManager(userDetails);
    }
}

[ request URI 접근 권한 ]

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .formLogin()
            .loginPage("/auths/login-form")
            .loginProcessingUrl("/process_login")
            .failureUrl("/auths/login-form?error")
            .and()
            .logout()                        // 로그아웃에 대한 추가 설정
            .logoutUrl("/logout")            // 로그아웃을 수행하기 위한 request URL을 지정
            .logoutSuccessUrl("/")  // 로그아웃을 성공적으로 수행한 이후 리다이렉트 할 URL을 지정
            .and()
            .exceptionHandling().accessDeniedPage("/auths/access-denied")   // 권한이 없는 사용자가 접근할 경우 에러 처리
            .and()
            .authorizeHttpRequests(authorize -> authorize              // request URI에 대한 접근 권한 부여
                    .antMatchers("/orders/**").hasRole("ADMIN")        // ADMIN ROLE을 부여받은 사용자만 /order로 시작하는 모든 URL에 접근 가능
                    .antMatchers("/members/my-page").hasRole("USER")   // USER ROLE을 부여받은 사용자만 /member/my-page RUL에 접근 가능
                    .antMatchers("⁄**").permitAll()                    // 앞서 지정한 URL 이외의 모든 RUL에 접근 가능
            );
        return http.build();
    }
}

[ passwordEncoder Bean 등록 ]

@Configuration
public class SecurityConfiguration {
	@Bean
    public PasswordEncoder passwordEncoder() {
    	return PasswordEncoderFactories.createDelegatingPasswordEncoder();	// DelegatingPasswordEncoder가 실질적으로 PasswordEncoder 구현 객체를 생성
    }
}

[ InMemoryMemberService 구현 ]

public class InMemoryMemberService implements MemberService {
	private final UserDetailsManager userDetailsManager;
    private final PasswordEncoder passwordEncoder;
    
    public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
    	this.userDetailsManager = userDetatilsManager;	// UserDetailsManager => User를 관리하는 관리자 역할
        this.passwordEncoder = passwordEncoder;	// PasswordEncoder => 패스워드를 암호화
    }
    
    public Member createMember(Member member) {
    	List<GrantedAuthority> authorities = createAuthoritied(Member.MemberRole.ROLE_USER.name());	// User의 권한 목록은 List<GrantedAuthority> 로 생성
    	String encryptedPassword = passwordEncoder.encode(member.getPassword());	// PasswordEncoder를 사용하여 암호화
        UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);	// UserDetails 생성
        userDetatilsManager.createUser(userDetails);	// User 정보 등록
        
        return member;
    }
	
    private List<GrantedAuthority> createAuthorities(String ... roles) {
    	return Arrays.stream(roles)
        		.map(role -> new SimpleGrantedAuthority(role))
                .collect(Collectors.toList());
    }
}

[ PasswordEncoder Bean 등록 ]

@Configuration
public class JavaConfiguration {
    // PasswordEncoder 구현 객체를 생성
    @Bean
    public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
        return new InMemoryMemberService(userDetailsManager, passwordEncoder);
    }
}

[ JavaConfiguration 구성 ]

@Configuration
public class JavaConfiguration {
    // InMemoryMemberService 클래스의 Bean 객체를 생성
    @Bean
    public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager, 
                                               PasswordEncoder passwordEncoder) {
        return new InMemoryMemberService(userDetailsManager, passwordEncoder);
    }
}

[ InMemoryMemberService 구현 ]

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

    public Member createMember(Member member) {
        // User의 권한 목록을 List<GrantedAuthority>로 생성
        List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());

        // User의 패스워드를 암호화
        String encryptedPassword = passwordEncoder.encode(member.getPassword());

        // Spring Security User로 등록하기 위해 UserDetails를 생성
        UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);

        // User를 등록
        userDetailsManager.createUser(userDetails);

        return member;
    }

    private List<GrantedAuthority> createAuthorities(String... roles) {
        return Arrays.stream(roles)
                .map(role -> new SimpleGrantedAuthority(role))
                .collect(Collectors.toList());
    }
}

Custom UserDetailsService 사용 (데이터 베이스 연동 O)

[ SecurityConfiguration 설정 변경 및 추가 ]

@Configuration
public class SecurityConfiguration {
	@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    	http
        	.headers().frameOprions().sameOrigin()	// H2 웹 콘솔을 사용하기 위한 설정
            .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();
    }
}

[ JavaConfiguration의 Bean 등록 변경 ]

@Configuration
public class JavaConfiguration {
    // 데이터베이스에 User의 정보를 저장하기 위해 MemberService 인터페이스의 구현 클래스를 DBMemberService로 변경
    @Bean
    public MemberService dbMemberService(MemberRepository memberRepository,
                                         PasswordEncoder passwordEncoder) {
        return new DBMemberService(memberRepository, passwordEncoder); 
    }
}

[ DBMEmberService 구현 ]

@Transactional
public class DBMemberService implements MemberService {
	private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    
    public DBMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
    	this.memberRepository = memberRepository;
        this.passwordEncoder = passwordEncoder;
    }
    
    public Member createMemeber(Member member) {
    	verifyExistsEmail(member.getEmail());
        String encryptedPassword = passwordEncoder.encode(member.getPassword());	// Password 암호화
        member.setPassword(encryptedPassword);	// 암호화된 Password를 password 필드에 다시 할당
        
        Member savedMember = memberRepository.save(member);	
        
        System.out.println("# create Member in DB");
        return saveMember;
    }
    
    ...
    ...
}

[ Custom 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;
    }
    
    @Override
    public userDetails loadUserByUsername(String username) throws UsernameNotFoundException {	// UserDetailsService 인터페이스를 구현하는 클래스는 loadUserByUsername(String username)이라는 추상 메서드 구현 필요
    	Optional<Member> optionalMember = memberRepository.finByEmail(username);
        Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.Member_NOT_FOUND));
        Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());	// HelloAuthorityUtils를 이용해 데이터베이스에서 조회한 이메일 정보를 Role기반의 권한정보 컬렉션에 생성
        return new User(findMember.getEmail(), finMember.getPassword(), authorities);	
        
    }
}
        

[ HelloAuthorityUtils ]

@Component
public class HelloAuthorityUtils {
	@Value("${mail.address.admin}")	// application.yml에 추가한 프로퍼티를 가져옴
    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) {	// 파라미터로 받은 이메일 정보와 application.yml 파일의 이메일 정보가 동일 하다면 관리자용 권한인 ADMIN_ROLES 리턴
    	if(email.equals(adminMailAddress)) {
        	return ADMIN_ROLES;
        }
        return USER_ROLES;
    }
}

0개의 댓글