Spring Boot (Chapter 7)[스프링 부트 시큐리티]


7. 스프링 부트 시큐리티

7.1 스프링 부트 시큐리티 퀵스타트

인증(Authentication), 인가(Authorization)는 어감이 비슷하여 혼동하기 쉽지만 전혀 다른 개념이다. 일반적으로 인증(Authentication)을 통해 사용자를 식별하고 인가(Authorization)를 통해 시스템 자원에 대한 접근을 통제한다.

7.1.1 스프링 부트 시큐리티 적용하기

인증과 인가 관련 코드를 모든 클래스의 메서드마다 적용하면 반복적인 코드들이 여러 곳에 등장 할 수밖에 없다. 유지보수 과정에서 시큐리티 관련 코드를 수정하려고 할 때 또다시 반복적인 작업을 할 수 밖에 없으므로 스프링 시큐리티는 이런 문제를 해결하기 위해 만들어졌다.

스프링 시큐리티 스타터만 추가하면 인증 기능을 사용할 수 있다.

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

7.1.2 시큐리티 커스터마이징하기

가장 먼저 스프링 시큐리티 관련 설정을 위한 환경 설정 클래스를 작성한다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity security) throws Exception {

    }
}

@EnableWebSecurity 어노테이션은 이 클래스로부터 생성된 객체가 시큐리티 설정 파일임을 의미하면서, 동시에 시큐리티를 사용하는데 필요한 수많은 객체를 생성한다.

SecurityConfig 클래스가 상속한 WebSecurityConfigurerAdapter 클래스에는 웹 시큐리티와 관련된 다양한 설정을 추가할 수 있는 configure() 메서드가 있는데 이 메서드를 재정의해서 시큐리티 설정을 커스터마이징 할 수 있다. configure() 메서드는 HttpSecurity 객체를 매개변수로 받는데, 이 HttpSecurity 객체를 이용하여 어플리케이션 자원에 대한 인증과 인가를 제어할 수 있다.

WebSecurityConfigurerAdapter 클래스를 상속한 시큐리티 설정 클래스가 configure 메서드를 Override한 위의 소스가 빈으로 등록되기만 해도 이제 더 이상 어플리케이션에서는 로그인을 강제하지 않는다. (configure에서 인증 인가를 제어하는데 configure를 오버라이드 정의를 하고 아무것도 작성하지 않았기 때문에..)


시큐리티 화면 구성하기

SecurityTestController

@Controller
public class SecurityTestController {

    @GetMapping("/")
    public String index(){
        System.out.println("Index 요청");
        return "index";
    }

    @GetMapping("/member")
    public void forMember(){
        System.out.println("Member 요청");
    }

    @GetMapping("/manager")
    public void forManager(){
        System.out.println("Manager 요청");
    }

    @GetMapping("/admin")
    public void forAdmin(){
        System.out.println("Admin 요청");
    }
}

index.html

<body>
    <h1>인덱스 페이지 입니다.</h1>
</body>

manager.html

<body>
    <h1>Manager 권한을 가진 사용자를 위한 화면입니다.</h1>
    <a th:href="@{/loginSuccess}">뒤로 가기</a>
</body>

admin.html

<body>
    <h1>Admin 권한을 가진 사용자를 위한 화면입니다.</h1>
    <a th:href="@{/loginSuccess}">뒤로 가기</a>
</body>
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security.authorizeRequests().antMatchers("/").permitAll();
        security.authorizeRequests().antMatchers("/member").authenticated();
        security.authorizeRequests().antMatchers("/manager").hasRole("MANAGER");
        security.authorizeRequests().antMatchers("/admin").hasRole("ADMIN");
    }
}

특정 경로에 대해 권한을 가진 사용자만 접근을 허용하려면 AuthorizedUrl 객체의 메서드를 이용해야 한다. AuthorizedUrl는 HttpSecurity의 authorizeRequests() 메서드를 호출했을 때 리턴된 ExpressionInterceptorUrlRegistry의 antMatchers() 메서드를 통해 얻어낼 수 있다.

다음은 HttpSecurity가 제공하는 메서드 중에서 자주 사용하는 주요 메서드를 정리한 것이다.

HttpSecurity가 제공하는 메서드

메서드 사용할 수 있는 메서드 의미
authorizeRequests()   사용자 인증과 권한을 설정
  antMatchers(“URL 패턴”) 매칭되는 url 패턴들에 대한 접근 허용, permitAll()은 모든 사용자에게 접근 허용, hasRole(“권한”)은 특정 권한을 가진 사용자만 접근 허용
formLogin()   로그인 페이지 설정
  loginPage(“/login”) 로그인이 필요한 url로 접근하면 ‘/login’ 화면으로 이동
logout()   로그아웃 페이지 설정
  logoutUrl(“/logout”) 로그아웃을 처리하는 페이지 설정
csrf()   csrf는 크로스 사이트 위조 요청에 대한 설정
  disable() RESTful을 사용하기 위해서는 csrf 기능을 비활성화해야 함

AuthorizedUrl은 빌더 패턴을 사용하기 때문에 다음과 같이 타이핑해도 결과는 동일하다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security.authorizeRequests()
             .antMatchers("/").permitAll()
             .antMatchers("/member").authenticated()
             .antMatchers("/manager").hasRole("MANAGER")
             .antMatchers("/admin").hasRole("ADMIN");
    }
}

’/’를 요청하면 인덱스화면을 볼 수 있다. ‘/member’, ‘/admin’, ‘/manager’는 Access Denied 오류가 발생한다.


사용자 인증하기

로그인에 성공한 사용자에게만 특정 페이지를 보여주기 위해서는 사용자에게 로그인 화면을 제공해야 한다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        //......생략
        security.csrf().disable();

        security.formLogin();
    }
}

HttpSecurity의 formLogin() 메서드는 <form> 태그 기반의 로그인을 지원한다는 설정이다. 이를 이용하면 스프링 부트의 로그인 화면을 사용할 수 있다. ‘/member’, ‘/admin’, ‘/manager’ 접근하면 에러가 발생하는 것이 아니라 로그인 화면으로 이동한다.

만약 스프링 부트가 제공하는 로그인 화면이 아닌 사용자가 직접 작성한 로그인 화면을 제공하고 싶으면 loginPage() 메서드를 추가하여 사용자가 작성한 로그인 화면으로 이동할 수 있다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity security) throws Exception {
       //......생략
        security.csrf().disable();

        security.formLogin().loginPage("/login").defaultSuccessUrl("/loginSuccess", true);
    }
}

추가된 loginPage() 메서드는 로그인에 사용할 화면을 지정한다. 그리고 마지막에 추가한 defaultSuccessUrl 메서드는 로그인에 성공했을 때 이동할 URL을 지정하는데 사용한다.

이제 loginPage() 메서드 설정으로 인해서 인증되지 않은 사용자가 시스템 자원에 접근할 경우, 브라우저는 ‘/login’ 요청을 리다이렉트한다. 따라서 브라우저의 ‘/login’ 요청을 처리할 메서드를 LoginController 클래스에 추가해야 한다.

@Controller
public class LoginController {

	@GetMapping("/login")
	public void login() {
	}

	@GetMapping("loginSuccess")
	public void loginSuccess(){
	}
}

이렇게 정의하면 로그인 페이지가 사용자 정의 로그인 화면으로 이동한다.


메모리 사용자 인증하기

지금까지는 스프링 시큐리티가 제공하는 ‘user’를 이용하여 로그인 인증을 테스트했다. 하지만 실제 시스템에서는 데이터베이스에 저장된 회원 정보를 이용하여 로그인 인증을 처리한다. 이번 실습은 간단하게 메모리에 사용자 정보를 생성하고, 이 정보를 이용해서 로그인 인증을 처리한다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //......생략
    @Autowired
    public void authenticate(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("manager")
                .password("{noop}manager123")
                .roles("MANAGER");

        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("{noop}admin123")
                .roles("ADMIN");
    }
}

AuthenticationManagerBuilder 객체를 @Autowired를 통해 의존성 주입 받은 authenticate() 메서드에서는 인증에 필요한 사용자 정보를 생성한다. AuthenticationManagerBuilder가 지원하는 인증 방식은 메모리, JDBC, LDAP가 있다.

inMemoryAuthentication() 메서드는 메모리에 사용자 정보를 생성하는 메서드다. {noop}은 비밀번호에 대한 암호화 처리를 하지 않겠다는 의미다.


접근 권한 없음 페이지 처리

접근 권한이 없는 사용자가 특정 페이지를 요청하면 에러가 발생한다. 사용자에게 에러 화면 대신 적절한 권한이 없어서 해당 페이지를 볼 수 없다는 메시지를 보여주고 싶으면 역시 SecurityConfig 권한이 없을 때 요청할 URL을 설정해주면 된다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        //......생략
        security.exceptionHandling().accessDeniedPage("/accessDenied");
    }
}
@Controller
public class LoginController {

	@GetMapping("accessDenied")
	public void accessDenied(){
	}
}


로그아웃 처리하기

스프링 시큐리티가 인증을 처리하는 기본 방식은 HttpSession을 기반으로 한다. 따라서 브라우저가 종료되면 자동으로 브라우저와 매핑된 세션도 사라진다. 하지만 브라우저를 종료하지 않은 상태에서 세션을 종료하려면 사용자가 로그아웃 요청을 하고 서버는 해당 세션을 강제로 종료해야 한다. 로그아웃 기능을 추가하기 위해서 SecurityConfig에 로그아웃 관련 설정을 추가한다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity security) throws Exception {
        //......생략
        security.logout().invalidateHttpSession(true).logoutSuccessUrl("/login");
    }
}

logout() 뒤에 invalidateHttpSession(true)를 사용하여 현재 브라우저와 연관된 세션을 강제 종료한다. 만약 관련된 모든 쿠키 정보까지 삭제하고 싶으면 deleteCookies() 메서드를 뒤에 추가로 사용하면 된다. 그리고 logoutSuccessfulUrl() 메서드를 사용하면 로그아웃한 후에 이동할 화면으로 리다이렉트할 수도 있다.

7.2 시큐리티 이해 및 데이터베이스 연동

지금까지 사용한 스프링 부트 시큐리티는 스프링 시큐리티를 기반으로 동작한다. 따라서 스프링 부트의 설정을 커스터마이징하여 사용하려면 스프링 시큐리티의 구조와 용어, 그리고 동작 원리를 정확하게 이해해야 한다.

7.2.1 스프링 시큐리티와 동작 원리

  • 시큐리티 필터

스프링 시큐리티는 서블릿 필터(javax.servlet.Filter)로 개발한 시큐리티 필터에 대한 이해에서 출발한다. 서블릿 필터는 클라이언트의 요청을 가로채서 서블릿이 수행되기 전후에 전처리와 후처리를 수행하거나 요청을 리다이렉트하는 용도로 사용한다.

일반적으로 필터 한 개당 하나의 기능을 처리하기 때문에 여러 기능이 필요한 경우에는 여러 개의 필터를 만들어 필터 체인을 형성하여 사용한다.

springboot_7_1

스프링 시큐리티는 시큐리티와 관련된 다양한 기능들을 필터 체인으로 제공한다. 앞에서 인증되지 않은 사용자가 특정 URL을 요청했을 때, 로그인 화면을 보여주는 기능 같은 것이 바로 이 시큐리티 필터 중 하나에 의해서 처리된 것이다.

다음은 스프링 시큐리티에서 제공하는 수많은 필터 중에서 중요한 필터들만 정리한 것이다.

스프링 시큐리티에서 사용하는 필터

필터 기능
SecurityContextPersistenceFilter SecurityContextRepository에서 SecurityContext 객체를 로딩하여 SecurityContextHolder에 저장하고, 요청처리가 끝나면 제거한다.
LogoutFilter 지정한 경로의 요청이 들어오면 로그아웃하고 지정한 페이지로 이동한다. 이후 필터들을 진행하지 않는다.
UsernamePasswordAuthenticationFilter 로그인 요청이 들어오면, 아이디/비밀번호 기반의 인증을 수행한다. 인증에 성공하면 지정한 페이지로 이동하고 실패하면 로그인 화면을 보여준다.
DefaultLoginPageGeneratingFilter 로그인 요청이 들어오면 기본으로 제공하는 로그인 화면을 출력하고 이후 필터를 진행하지 않는다.
AnonymousAuthenticationFilter 이 필터가 실행되는 시점까지 사용자가 인증을 받지 못했다면 임의 사용자에 해당하는 Authentication 객체를 생성하여 SecurityContext에 설정한다. 생성된 Authentication의 아이디는 “anonymousUser”고, 권한은 “ROLE_ANONYMOUS”이며, 인증되지 않은 상태의 값을 가진다.
SessionManagementFilter 세션 타임아웃, 동시 접근제어, 세션 고정 공격 등을 처리한다.
ExceptionTranslationFilter FilterSecurityInterceptor에서 발생한 예외를 웹에 맞는 응답으로 변환한다. 예를 들어 403 상태 코드를 응답하거나 로그인 페이지로 이동하는 작업을 처리한다.
FilterSecurityInterceptor 현재 사용자가 지정한 경로에 접근할 수 있는지 여부를 검사한다. 권한이 있으면 보안 필터를 통과시켜 자원에 접근할 수 있게 하고, 권한이 없으면 예외를 발생시킨다.

각 필터는 시큐리티 처리를 위한 기능을 제공하며 등록된 순서대로 동작한다. 따라서 위에 있는 SecurityContextPersistenceFilter를 시작으로 FilterSecurityInterceptor까지 순차적으로 동작한다.

  • 스프링 시큐리티 동작 원리

스프링 시큐리티는 시큐리티 필터들의 상호작용에 의해 처리된다고 했다. 그런데 스프링 시큐리티를 구성하는 필터 중에서 가장 중요한 필터는 UsernamePasswordAuthenticationFilter다. UsernamePasswordAuthenticationFilter가 실제로 사용자가 입력한 인증 정보를 이용해서 인증을 처리해주기 때문이다.

그리고 두 번째로 중요한 객체가 FilterSecurityInterceptor인데, FilterSecurityInterceptor는 인증에 성공한 사용자가 해당 리소스에 접근할 권한이 있는지를 검증하기 때문이다. 다음 그림은 이 필터들 중에서 인증 관리 필터와 권한 관리 필터가 인증과 인가를 처리하는 과정을 그림으로 표현한 것이다.

springboot_7_2

사용자가 리소스를 요청하면 가장 먼저 인증 관리 필터가 사용자 요청을 가로챈다. 그리고 인증 관리 필터는 인증 처리와 관련된 필터들을 이용하여 사용자 인증을 처리하는데, 만약 인증되지 않은 사용자가 접근하면 인증에 필요한 정보를 요청한다. 그리고 사용자가 인증 절차(로그인)를 통해 인증 정보를 입력하면 파일이나 데이터베이스에 저장되어 있는 사용자 정보를 읽어 사용자가 입력한 인증 정보를 검증한다. 만약 인증에 실패하면 사용자에게 다시 인증 정보를 요청하고 인증을 통과한 경우에만 사용자가 요청한 리소스로 요청을 전달한다.

그런데 이때 권한 관리 필터가 다시 요청을 가로채서 리소스에 대한 접근 권한을 검증한다. 당연히 이때도 권한을 검증하기 위해 관련된 필터들을 사용하는데, 권한 관리 필터는 리소스별 권한 목록을 참조하여 접근 권한을 검증하고 검증에 실패한 경우에는 권한 없음 페이지를 전송하거나 리소스에 대한 접근을 차단한다.


데이터베이스 연동하기
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource datasource;
    @Autowired
    public void authenticate(AuthenticationManagerBuilder auth) throws Exception {
        String query1 = "select id username, concat('{noop}', password) password, true enabled from member where id=?";
        String query2 = "select id, role from member where id =?";

        auth.jdbcAuthentication()
                .dataSource(datasource)
                .usersByUsernameQuery(query1)
                .authoritiesByUsernameQuery(query2);
    }
}

가장 먼저 데이터소스를 멤버 변수로 선언하고 의존성 주입한 후, 메모리 사용자가 아닌 실제 데이터베이스에 저장된 사욪아로 인증을 처리하기 위해서 authenticate() 메서드를 수정했다. 사용자 인증과 인가를 처리하기 위해서는 두 개의 SQL이 필요하다. 첫 번째 쿼리는 사용자가 입력한 아이디로 사용자 정보를 조회하고, 두 번째 쿼리는 권한 정보를 조회한다.

이 중에서 첫 번째 쿼리가 중요한데, 스프링 시큐리티는 조회한 사용자 정보를 시큐리티에서 내부적으로 사용하는 org.springframework.security.core.userdetails.User 객체에 자동으로 매핑한다. 이때 아이디는 “username”에, 비밀번호는 “password” 변수에 각각 저장하는데, 조회 컬럼의 컬럼 이름이 USERNAME과 PASSWORD로 일치해야 자동으로 매핑된다. 이를 위해 조회 쿼리에 Alias를 각각 적용한 것이다.

데이터베이스에 저장된 사용자로 인증을 처리하기 위해서 기존의 inMemoryAuthentication() 메서드를 jdbcAuthentication()으로 변경했다. 그리고 userByUsernameQuery() 메서드를 이용하여 인증을 처리하도록 한다. 만약 여기에서 인증에 실패하면 다음 단계로 진행되지 않는다. 그리고 인증에 통과한 사용자의 권한을 검증하기 위해 authoritiesByUsernameQuery() 메서드를 호출했다. 인증에 성공했고 접근 권한도 가진 사용자라면 요청한 리소스에 접근할 수 있다.


JPA 연동하기
public enum Role {
	ROLE_ADMIN, ROLE_MANAGER, ROLE_MEMBER
}
@Getter 
@Setter
@ToString
@Entity
@Table(name = "T_MEMBER")
public class Member { 
	@Id 
	private String id; 
	private String password;
	private String name;

	@Enumerated(EnumType.STRING)
	private Role role;
	private boolean enabled;
} 

role 변수는 EnumType.STRING으로 설정했기 때문에 권한에 해당하는 값이 문자열로 저장된다.


사용자가 시스템의 리소스에 접근하기 위해서는 가장 먼저 인증 관리 필터의 검증을 통과해야 한다. 인증 관리 필터가 사용자가 입력한 정보를 토대로 기능을 처리하기 위해서는 사용자 정보가 저장된 UserDetails 객체가 필요하다. 그리고 UserDetails 객체에 실제 데이터베이스에서 검색한 사용자 정보를 저장하는 UserDetailsService 객체도 필요하다.

인증 관리자는 UserDetailsService 객체를 통해 UserDetails 객체를 획득하고 이 UserDetails 객체에서 인증과 인가에 필요한 정보들을 추출하여 사용한다.

스프링 부트는 UserDetailsService를 구현한 클래스를 기본적으로 제공한다. 그리고 이 클래스가 제공하는 UserDetails 객체는 아이디가 ‘user’고 비밀번호는 암호화되어 콘솔에 출력되는 긴 문자열이다.

스프링 부트가 제공하는 UserDetailsService를 커스터마이징하고 싶으면 UserDetailsService를 구현한 클래스를 직접 작성하여 등록하면 된다.

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

@Service
public class BoardUserDetailService implements UserDetailsService {

	@Autowired
	private MemberRepository memberRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// MemberRepository로 회원 정보를 조회
		// UserDetails 타입의 객체로 리턴
		return null;
	}
}

BoardUserDetailService 클래스는 UserDetailsService 인터페이스의 loadUserByUsername() 메서드를 재정의했다. loadUserByUsername() 메서드는 사용자가 아이디를 매개변수로 받아서 회원 정보를 조회하고, 조회 결과를 UserDetails 타입의 객체로 변환하여 리턴한다.

스프링이 제공하는 org.springframework.security.core.userdetails.User 클래스가 바로 UserDetails 인터페이스를 구현한 클래스다. 그런데 문제는 loadUserByUsername() 메서드의 리턴 타입이 UserDetails이기 때문에 memberRepository로 검색한 Member 엔티티를 바로 리턴할 수 없다는 것이다.

검색한 Member 객체를 UserDetails 타입의 객체로 변환하기 위해서 Member 클래스가 UserDetails 인터페이스를 구현하게 하거나, 이미 UserDetails 인터페이스를 구현한 User 클래스를 상속해야 한다. 하지만 Member 클래스에 UserDetails 인터페이스를 추가하면 재정의 해야할 추상 메서드들이 생기기 때문에 바람직하지 않다. 또한 User 클래스를 상속하면 복잡한 생성자를 추가해야 한다.

무엇보다도 두 가지 방법 모두 Member를 순수한 엔티티로 사용하지 않게 되기 때문에 적절한 방법이 아니다. 따라서 이번 실습에는 Member 클래스는 그대로 두고 스프링이 제공하는 User 클래스를 상속하여 새로운 클래스를 정의하는 것으로 구현한다.

public class SecurityUser extends User {
	
	private static final long serialVersionUID = 1L;
	
	public SecurityUser(Member member) {
		super(member.getId(), "{noop}" + member.getPassword(), AuthorityUtils.createAuthorityList(member.getRole().toString()));
	}
}

org.springframework.security.core.userdetails.User 클래스를 상속했으며, User클래스의 생성자를 호출할 때, 검색 결과로 얻은 Member 객체의 값을 전달한다.

이제 SecurityUser 클래스를 이용하도록 BoardUserDetailsService 클래스를 수정한다.

@Service
public class BoardUserDetailsService implements UserDetailsService {

	@Autowired
	private MemberRepository memberRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		Optional<Member> optional = memberRepository.findById(username);

		if(!optional.isPresent()) {
			throw new UsernameNotFoundException(username + "사용자 없음");
		} else {
			Member member = optional.get();
			return new SecurityUser(member);
		}
	}
}

SecurityUser 객체는 org.springframework.security.core.userdetails.User 클래스를 상속했고, User는 UserDetails를 구현한 클래스이므로 문제없이 loadUserByUsername() 메서드의 리턴 타입으로 사용할 수 있다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private BoardUserDetailsService boardUserDetailsService;

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security.userDetailsService(boardUserDetailsService);
    }
}

스프링 시큐리티가 제공하는 기본 UserDetailsService가 아닌 사용자 정의 UserDetailsService를 사용할 수 있도록 userDetailsService()를 이용하여 변경했다.


7.2.3 PasswordEncoder 사용하기

사용자 정보를 조회하여 SecurityUser 객체를 생성할 때 비밀번호에 대한 암호화를 사용하지 않기 위해서 “{noop}”이라는 접두사를 붙여서 사용해왔다.

스프링 시큐리티는 패스워드를 쉽게 암호화할 수 있도록 PasswordEncoder라는 인터페이스를 구현한 클래스들을 제공한다. 따라서 몇 가지 암호화 관련 설정만 추가하면 사용자가 입력한 비밀번호를 암호화하여 처리할 수 있다.

PasswordEncoderFactories

package org.springframework.security.crypto.factory;

public final class PasswordEncoderFactories {

	private PasswordEncoderFactories() {
	}

	@SuppressWarnings("deprecation")
	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}
}

PasswordEncoderFactories의 createDelegatingPasswordEncoder() 메서드로 PasswordEncoder 객체를 요청하면 기본적으로 BCryptPasswordEncoder 객체를 리턴한다. 나머지 PasswordEncoder들은 지원 중단(Deprecated)되어 사용하지 않는다. BCryptPasswordEncoder는 비밀번호 암호화에 특화되어 있으면서 가장 안전한 해시 알고리즘인 BCrypt를 사용한다.

암호화 적용하기

스프링 시큐리티에서 비밀번호를 암호화하는 방법은 매우 간단하다. SecurityConfig 클래스에서 단시 BCryptPasswordEncoder 객체를 리턴하는 passwordEncoder() 메서드만 추가한다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Commit
public class PasswordEncoderTest {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private PasswordEncoder encoder;

    @Test
    public void testInsert(){
        Member member = new Member();
        member.setId("manager");
        member.setPassword(encoder.encode("q1w2e3r4"));
        member.setName("암호화된 매니저");
        member.setRole(Role.ROLE_MANAGER);
        member.setEnabled(true);
        memberRepository.save(member);
    }
}

앞에서 @Bean으로 등록한 PasswordEncoder를 의존성 주입하고 사용자가 입력한 비밀번호를 인코딩하여 저장했다.

manager,{bcrypt}$2a$10$78SezVCq1efZcViPoPmVZuMgo37uzSyFsu3/OP9qHfUABcXKMIIbK,암호화된 매니저,ROLE_MANAGER,true

저장된 결과는 위와 같다.





Reference

  • 스프링 부트 (채규태)

Tag: [ book  programming  spring  framework  springboot  springsecurity  authenticate  authorization  @enumerated  ]