implementation 'org.springframework.boot:spring-boot-starter-security'
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/auths")
public class AuthController {
@GetMapping("/login-form")
public String loginForm() {
return "login";
}
@GetMapping("/access-denied")
public String accessDenied() {
return "access-denied";
}
@PostMapping("/login")
public String login() {
System.out.println("Login successfully!");
return "home";
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration_v2 {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // (1)
http
.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 // (2)
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("/**").permitAll()
);
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() { // (3)
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com")
.password("1111")
.roles("USER")
.build();
UserDetails admin =
User.withDefaultPasswordEncoder()
.username("admin@gmail.com")
.password("2222")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() { // (4)
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="layouts/common-layout">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Hello Spring Security Coffee Shop</title>
</head>
<body>
<div class="container" layout:fragment="content">
<form action="/process_login" method="post">
<!-- (1) 로그인 실패에 대한 메시지 표시 -->
<div class="row alert alert-danger center" role="alert" th:if="${param.error != null}">
<div>로그인 인증에 실패했습니다.</div>
</div>
<div class="row">
<div class="col-xs-2">
<input type="email" name="username" class="form-control" placeholder="Email" />
</div>
</div>
<div class="row" style="margin-top: 20px">
<div class="col-xs-2">
<input type="password" name="password" class="form-control" placeholder="Password" />
</div>
</div>
<button class="btn btn-outline-secondary" style="margin-top: 20px">로그인</button>
</form>
<div style="margin-top: 20px">
<a href="/members/register">회원가입</a>
</div>
</div>
</body>
</html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <!-- (1) -->
<body>
<div align="right" th:fragment="header">
<a href="/members/register" class="text-decoration-none">회원가입</a> |
<span sec:authorize="isAuthenticated()"> <!-- (2) -->
<span sec:authorize="hasRole('USER')"> <!-- (3) -->
<a href="/members/my-page" class="text-decoration-none">마이페이지</a> |
</span>
<a href="/logout" class="text-decoration-none">로그아웃</a> <!-- (4) -->
<span th:text="${#authentication.name}">홍길동</span>님 <!-- (5) -->
</span>
<span sec:authorize="!isAuthenticated()"> <!-- (6) -->
<a href="/auths/login-form" class="text-decoration-none">로그인</a> |
</span>
</div>
</body>
</html>
import com.codestates.member.InMemoryMemberService;
import com.codestates.member.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
@Configuration
public class JavaConfiguration {
@Bean
public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager,
PasswordEncoder passwordEncoder) {
return new InMemoryMemberService(userDetailsManager, passwordEncoder);
}
}
import com.codestates.auth.utils.AuthorityUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import java.util.List;
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) {
// (1)
List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());
// (2)
String encryptedPassword = passwordEncoder.encode(member.getPassword());
// (3)
UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);
// (4)
userDetailsManager.createUser(userDetails);
return member;
}
private List<GrantedAuthority> createAuthorities(String... roles) {
// (1-1)
return Arrays.stream(roles)
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // (1)
.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();
}
}
@Configuration
public class JavaConfiguration {
@Bean
public MemberService dbMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
return new DBMemberService(memberRepository, passwordEncoder); (1)
}
}
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Transactional
public class DBMemberService implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
// (1)
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()); // (2)
member.setPassword(encryptedPassword); // (3)
Member savedMember = memberRepository.save(member);
System.out.println("# Create Member in DB");
return savedMember;
}
...
...
}
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV1 implements UserDetailsService { // (1)
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
// (2)
public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
// (3)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
// (4)
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());
// (5)
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
데이터베이스에서 조회한 User 클래스의 객체를 리턴하면 Spring Security가 이 정보를 이용해 인증 절차를 수행
데이터베이스에서 User의 인증 정보만 Spring Security에 넘겨주고, 인증 처리는 Spring Security가 대신 수행
UserDetails :
UserDetailsService에 의해 로드(load)되어 인증을 위해 사용되는 핵심 User 정보를 표현하는 인터페이스
구현체는 Spring Security에서 보안 정보 제공을 목적으로 직접 사용되지는 않고, Authentication 객체로 캡슐화되어 제공
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class HelloAuthorityUtils {
// (1)
@Value("${mail.address.admin}")
private String adminMailAddress;
// (2)
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
// (3)
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
public List<GrantedAuthority> createAuthorities(String email) {
// (4)
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
}
mail:
address:
admin: admin@gmail.com
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV2 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV2(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return new HelloUserDetails(findMember); // (1) 개선된 부분
}
// (2) HelloUserDetails 클래스 추가
private final class HelloUserDetails extends Member implements UserDetails { // (2-1)
// (2-2)
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getEmail()); // (2-3) 리팩토링 포인트
}
// (2-4)
@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;
}
}
}
import com.codestates.audit.Auditable;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member extends Auditable implements Principal{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(length = 100, nullable = false)
private String fullName;
@Column(nullable = false, updatable = false, unique = true)
private String email;
@Column(length = 100, nullable = false)
private String password;
@Enumerated(value = EnumType.STRING)
@Column(length = 20, nullable = false)
private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE;
// (1) User의 권한 정보 테이블과 매핑되는 정보
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles = new ArrayList<>();
...
public enum MemberRole {
ROLE_USER,
ROLE_ADMIN
}
}
@Transactional
public class DBMemberService implements MemberService {
...
...
private final HelloAuthorityUtils authorityUtils;
...
...
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
// (1) Role을 DB에 저장
List<String> roles = authorityUtils.createRoles(member.getEmail());
member.setRoles(roles);
Member savedMember = memberRepository.save(member);
return savedMember;
}
...
...
}
@Component
public class HelloAuthorityUtils {
@Value("${mail.address.admin}")
private String adminMailAddress;
...
...
private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
private final List<String> USER_ROLES_STRING = List.of("USER");
...
...
// (1) DB 저장용
public List<String> createRoles(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES_STRING;
}
return USER_ROLES_STRING;
}
}
@Component
public class HelloUserDetailsServiceV3 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV3(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return new HelloUserDetails(findMember);
}
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
setRoles(member.getRoles()); // (1)
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// (2) DB에 저장된 Role 정보로 User 권한 목록 생성
return authorityUtils.createAuthorities(this.getRoles());
}
...
...
}
}
@Component
public class HelloAuthorityUtils {
@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");
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;
}
// (1) DB에 저장된 Role을 기반으로 권한 정보 생성
public List<GrantedAuthority> createAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // (2)
.collect(Collectors.toList());
return authorities;
}
...
...
}
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider { // (1)
private final HelloUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// (3)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication; // (3-1)
// (3-2)
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
// (3-3)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password); // (3-4)
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); // (3-5)
// (3-6)
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
}
// (2) 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");
}
}
}
(1) AuthenticationProvider 인터페이스의 구현 클래스로 정의
(2) supports(Class<?> authentication)
(3) authenticate(Authentication authentication)에서 우리가 직접 작성한 인증 처리 로직을 이용해 사용자의 인증 여부를 결정
- (3-1) authentication을 캐스팅하여 UsernamePasswordAuthenticationToken로 변환
- (3-2) 사용자의 Username을 얻은 후, 존재하는지 체크
- (3-3) Username이 존재한다면 userDetailsService를 이용해 데이터베이스에서 해당 사용자를 조회
- (3-4) 로그인 정보에 포함된 패스워드(authToken.getCredentials())와 데이터베이스에 저장된 사용자의 패스워드 정보가 일치하는지를 검증
- (3-5) 전 단계에서 검증 과정을 통과했다면 로그인 인증에 성공한 사용자이므로 해당 사용자의 권한을 생성
- (3-6) 인증된 사용자의 인증 정보를 리턴값으로 전달
- 전달된 인증 정보는 내부적으로 Spring Security에서 관리
HelloUserDetailsService를 이용해 인증을 처리할 경우 인증 실패 시,
Spring Security 내부에서 인증 실패에 대한 전용 Exception인 AuthenticationException을 throw 하게 되고
SecurityConfiguration에서 설정한 .failureUrl("/auths/login-form?error") 을 통해 로그인 폼으로 리다이렉트 하면서
“로그인 인증에 실패했습니다.”라는 인증 실패 메시지를 표시함
하지만 Custom AuthenticationProvider를 이용할 경우에는 회원가입 전 인증 실패 시 “Whitelebel Error Page”가 표시이유 : MemberService에서 등록된 회원 정보가 없으면, BusinessLogicException을 throw 하는데 BusinessLogicException이 Cusotm AuthenticationProvider를 거쳐
그대로 Spring Security 내부 영역으로 throw 되기 때문
Spring Security에서는 인증 실패 시, AuthenticationException이 throw 되지 않으면 Exception에 대한 별도의 처리를 하지 않고, 서블릿 컨테이너인 톰캣 쪽으로 이 처리를 넘김서블릿 컨테이너 영역에서 해당 Exception에 대해 “/error” URL로 포워딩하는데 “/error” URL로 포워딩되었을 때 보여줄 뷰 페이지를 별도로 구성하지 않았기 때문에 디폴트 페이지인 “Whitelebel Error Page”를 브라우저에 표시
Cusotm AuthenticationProvider에서 Exception이 발생할 경우, 이 Exception을 catch 해서 AuthenticationException으로 rethrow 하여 해결
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider {
private final HelloUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// AuthenticationException을 rethrow 하는 개선 코드
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User 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 ex) {
throw new UsernameNotFoundException(ex.getMessage()); // (1) AuthenticationException으로 다시 throw 한다.
}
}
@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");
}
}
}
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
다이제스트(Digest) :
원본 메시지를 암호화한 메시지를 의미
Rainbow Attack:
사용자가 패스워드로 사용할만한 문자열들을 미리 목록(Rainbow Table)으로 만들어 놓고, 이 목록에 있는 문자열을 동일한 알고리즘으로 암호화한 후, 탈취한 암호화된 문자열과 서로 비교하는 작업을 통해 패스워드의 원본 문자열을 구함