반응형

 

도서 'Spring in Action' 제 5판을 보고 책 내용과 그 이외의 부족한 부분을 채워가며 공부한 내용입니다.


1. Spring Security 구성하기

※ 보안을 테스트 할 경우 웹 브라우저를 시크릿 모드(구글 크롬)로 하는 것이 좋음

  • 사용자의 검색 세션에 관한 데이터인 쿠키. 임시 파일 인터넷, 열어본 페이지 목록 및 기타 데이터를 저장하지 못함.
  • Ctrl+Shift+N

※ 스프링 부트 보안 스타터(Spring boot starter security)

  • 모든 HTTP 요청 경로는 인증(authentication)되어야 함
  • 어떤 특정 역할이나 권한이 없음
  • 로그인 페이지가 따로 없음
  • 스프링 시큐리티의 HTTP 기본 인증을 사용해서 인증
    • 기본 로그인 페이지를 Spring Security 내부에서 제공
  • 사용자는 하나만 있으며(user id가 1개), 이름은 user고 비밀번호는 암호화해주며 log를 통해 제공받음
  • 서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 진행

 

 

1. 로그인 페이지 생성 전 사용자 정보 유지, 관리를 위한 사용자 스토어 구성

사용자 스토어 구성 방법

  • 인메모리(in-memory) 사용자 스토어
  • JDBC 기반 사용자 스토어
  • LDAP 기반 사용자 스토어
  • 커스텀 사용자 명세 서비스

<보안 구성 클래스인 WebSecurity ConfigurerAdapter의 서브 클래스>

SpringSecutiry 의존성을 추가하게 되면 기본적으로 WebSecurityConfigurerAdapter 클래스가 실행. WebSecurityConfigurerAdapter 는 SpringSecutiry 의 웹(http) 보안 기능 초기화 및 설정들을 당담하는 내용이 담겨있고 인증/인가 관련 설정을 제공.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests()
		.antMatchers("/design", "/orders")
		.access("hasRole('ROLE_USER')")
		.antMatchers("/", "/**").access("permitAll")
		.and()
		.formLogin()
		.loginPage("/login")
		.and()
		.logout()
		.logoutSuccessUrl("/")
		.and()
		.csrf();
	}
    
    @Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth
		.userDetailsService(userDetailsService)
		.passwordEncoder(encoder());
		
		// 아래 코드는 책의 LDAP 기반 사용자 스토어 예제 코드입니다.
		/*auth
		.ldapAuthentication()
		.userSearchBase("ou=people")
		.userSearchFilter("(uid={0})")
		.groupSearchBase("ou=groups")
		.groupSearchFilter("member={0}")
		.contextSource()
		.root("dc=tacocloud,dc=com")
		.ldif("classpath:users.ldif")
		.and()
		.passwordCompare()
		.passwordEncoder(new BCryptPasswordEncoder())
		.passwordAttribute("userPasscode");*/
		
		
		// 아래 코드는 책의 JDBC 기반 사용자 스토어 예제 코드입니다.
		/*@Autowired
	    DataSource dataSource;
	    
		auth
		.jdbcAuthentication()
		.dataSource(dataSource)
		.usersByUsernameQuery(
			"select username, password, enabled from users " +
			"where username=?")
		.authoritiesByUsernameQuery(
			"select username, authority from authorities " +
			"where username=?")
		.passwordEncoder(new NoEncodingPasswordEncoder());*/
		
		// 아래 코드는 책의 인메모리 기반 사용자 스토어 예제 코드입니다.
		/*auth.inMemoryAuthentication()
		.withUser("user1")
		.password("{noop}password1")
		.authorities("ROLE_USER")
		.and()
		.withUser("user2")
		.password("{noop}password2")
		.authorities("ROLE_USER");*/
	}
}

 

<코드 분석>

  • configure(HttpSecurity) -> HTTP 보안을 구성하는 메서드
  • 위의 사용자 스토어 중 어떤 것을 선택하는 해당 메서드에서 구성
  • configure(AuthentixationManagerBuilder)
    • 오버라이딩 메서드
    • 인증을 하기 위해 사용자를 찾는 방법을 지정하는 코드 작성
    • AuthentixationManagerBuilder는 빌드 명세를 위해 사용

 

사용자 스토어(위의 SecurityConfig 에서 뽑아서 코드를 가져올 예정)

인메모리

사용자 정보를 유지/관리 할 수 있는 곳 중 하나가 메모리 변경이 필요없는 사용자를 정해놓는 용도로 사용 ⇒ 테스트 목적
@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
		
		// 아래 코드는 책의 인메모리 기반 사용자 스토어 예제 코드입니다.
		/*auth.inMemoryAuthentication()
		.withUser("user1")
		.password("{noop}password1")
		.authorities("ROLE_USER")
		.and()
		.withUser("user2")
		.password("{noop}password2")
		.authorities("ROLE_USER");*/
	}
    • auth.inMemoryAuthenticaiton()
      • 보안 구성 자체에 사용자 정보를 직접 입력
    • .withuser("사용자 이름")
      • 해당 사용자의 구성 시작
    • .password("패스워드"), .authorities()
      • {noop} 지정 시 비밀번호를 암호화 하지 않을 수 있음
      • 각각 비밀번호와 부여권한(granted authority)를 각각의 메서드의 인자로 넣어 호출
      • .authorities("ROLE_USER") 대신 .roles("USER")를 사용해도 됨
      • 권한의 명칭은 USER 대신 우리가 원하는 어떤 것도 지정 가능

 

 

JDBC 기반의 사용자 스토어

사용자 정보를 RDBMS로 유지/관리 하려는 목적으로 사용. 기본으로 제공하는 Query를 커스터마이징 가능
  • 사용자 검색 : username, password, enabled
  • 권한 검색 : username, authority
  • 그룹 검색 : groupid, groupname, groupautority
@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
			
		// 아래 코드는 책의 JDBC 기반 사용자 스토어 예제 코드입니다.
		/*@Autowired
	    DataSource dataSource;
	    
		auth
		.jdbcAuthentication()
		.dataSource(dataSource)
		.usersByUsernameQuery(
			"select username, password, enabled from users " +
			"where username=?")
		.authoritiesByUsernameQuery(
			"select username, authority from authorities " +
			"where username=?")
		.passwordEncoder(new NoEncodingPasswordEncoder());*/
		
	}
  • .jdbcAuthentication()
    • AuthenticationManagerBuilder의 메서드
  • .dataSource
    • 데이터베이스를 액세스 하는 방법을 알 수 있도록 dataSource()메서드 호출
    • @Autowired를 이용해 DataSource 자동 주입 
  • 사전 지정된 데이터베이스 테이블과 SQL 쿼리를 사용하려면 관련 테이블을 만들고 사용자 데이터 추가
    • SQL 쿼리를 우리 SQL 쿼리로 대체 가능
    • 테이블명은 달라도 되지만 테이블이 갖는 열의 데이터 타입과 길이는 기본 데이터베이스 테이블과 일치해야 함
    • .usersByUsernameQuery("select username, password, enabled from users " + "where username=?")
      • 해당 사용자의 이름, 비밀번호, 사용가능한 사용자인지를 나타내는 활성화 여부를 검색
      • 해당 정보는 사용자 인증에 사용됨
    • .authoritiesByUsernameQuery("select username, authority from authorities " + "where username=?")
      • 해당 사용자에게 부여된 권한을 찾음
    • .passwordEncoder(new NoEncodingPasswordEncoder());
      • 비밀번호는 데이터베이스에 저장할 때와 사용자가 입력한 비밀번호는 모두 같은 암호화 알고리즘을 사용해서 암호화해야함
      • 인코더(encoder)를 지정
      • 암호화 알고리즘을 구현한 스프링 시큐리티의 모듈에는 아래의 구현 Class 포함
        1. BCryptPasswordEncoder : bcrypt를 해싱 암호화
        2. NoOpPasswordEncoder : 암호화 하지 않음
        3. Pbkdf2PasswordEncoder : Pbkdf2를 암호화
        4. SCryptPasswordEncoder : scrypt를 해싱 암호화
        5. StandardPasswordEncoder : SHA-256 해싱 암호화

데이터베이스 스키마 - 사전 지정된 사용자 및 권한 테이블과 동일한 테이블 생성
사용자 데이터 추가

 

※ PasswordEncoder 인터페이스

public interface PasswordEncoder {
	String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
}

<코드 분석>

  • 어떤 비밀번호 인코더를 사용하든 일단 암호화되어 데이터베이스에 저장된 비밀번호는 암호가 해독되지 않음
  • 대신 로그인 시 사용자가 입력한 비밀번호와 동일한 알고리즘을 사용해서 암호화 됨
  • 데이터베이스의 암호화된 비밀번호와 비교되며, 해당 일은 PasswordEncoder()의 matches() 메서드에서 수행

 

<실제 구현 예시 - 단 해당 클래스는 비밀번호를 암호화 하지 않는 클래스>

package tacos.security;

import org.springframework.security.crypto.password.PasswordEncoder;

import tacos.security.NoEncodingPasswordEncoder;

public class NoEncodingPasswordEncoder implements PasswordEncoder {
	
	@Override
	public String encode(CharSequence rawPwd) {
		return rawPwd.toString();
	}
	
	@Override
	public boolean matches(CharSequence rawPwd, String encodedPwd) {
		return rawPwd.toString().equals(encodedPwd);
	}
}

 

LDAP 기반의 사용자 스토어

Lightweight Directory Access Protocol
 : 네트워크 상의 디렉토리 서비스 표준인 X.500의 DAP를 기반으로한 경량화(Lightweight) 된 DAP 버전으로 TCP/IP 계층과 관련되어있음.
  • 주로 사용자, 시스템, 네트워크, 서비스, 애플리케이션 등의 정보를 트리 구조로 저장하여 조회하거나 관리하는 용도나 특정 영역에서 이용자명과 패스워드를 확인하여 인증하는 용도.
@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
		
		// 아래 코드는 책의 LDAP 기반 사용자 스토어 예제 코드입니다.
		/*auth
		.ldapAuthentication()
		.userSearchBase("ou=people") //사용자를 찾기위한 기준점 쿼리
		.userSearchFilter("(uid={0})") //사용자를 찾기위한 필터
		.groupSearchBase("ou=groups") //그룹을 찾기위한 기준점 쿼리
		.groupSearchFilter("member={0}") //그룹을 찾기위한 필터
		.contextSource()
		.root("dc=tacocloud,dc=com") //ldap 서버 루트 경로 지정
		.ldif("classpath:users.ldif") //ldif 파일을 지정
		.and()
		.passwordCompare()
		.passwordEncoder(new BCryptPasswordEncoder())
		.passwordAttribute("userPasscode");*/
	}

<코드 분석>

  • .ldapAuthentication()
    • LDAP 기반 인증으로 스프링 Security 구성을 위해서는 ldapAuthentication() 메서드를 사용가능
    • .jdbcAuthentication()처럼 사용할 수 있게 해줌
  • .userSearchBase("ou=people")
    • 사용자를 찾기 위한 기준점 쿼리 제공
    • 비슷하게 .groupSearchBase() 는 그룹을 찾기 위한 기준점 쿼리 지정
    • 해당 코드는 root부터 검색하지 않음(그룹을 찾기 위한 기준점 쿼리를 지정)
    • 사용자는 people 구성 단위(Organizational Unit, OU), 그룹은 groups 구성단위부터 검색 시작

 

LDAP의 기본 인증 전략 = 사용자가 직접 LDAP 서버에서 인증 받도록 하는 것

LDAP - 비밀번호 비교 구성하기

  • 입력된 비밀번호를 LDAP 디렉터리에 전송한 후, 이 비밀번호를 사용자의 비밀번호 속성 값과 비교하도록 LDAP 서버에 요청(이 때, 비밀번호 비교는 LDAP서버에서 수행되므로 실제 비밀번호는 노출 X)
@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth
		.ldapAuthentication()
		.passwordEncoder(encoder());
        
		.ldapAuthentication()
		.userSearchBase("ou=people")
		.userSearchFilter("(uid={0})")
		.groupSearchBase("ou=groups")
		.groupSearchFilter("member={0}")
	
		.passwordCompare()
		.passwordEncoder(new BCryptPasswordEncoder())
		.passwordAttribute("userPasscode");*/
  • 로그인 폼에 입력된 비밀번호가 사용자의 LDAP 서버에 있는 userPassword 속성값과 비교
  • .passwordCompare()
    • 비밀번호를 비교하는 방법
  • .passwordEncoder(new BCryptPasswordEncoder())
    • 비밀번호는 데이터베이스에 저장할 때와 사용자가 입력한 비밀번호는 모두 같은 암호화 알고리즘을 사용해서 암호화해야함
  • passwordAttribute() -> 비밀번호 속성의 이름 지정 가능
    • .passwordAttribute("userPasscode")
    • 전달된 비밀번호는 userPasscode 속성 값이 비교되어야 한다는 것을 지정
    • 비밀번호 속성 이름을 변경하지 않을 때 기본적으로 userPassword가 됨

 

LDAP - 원격 LDAP 서버 참조

  • LDAP 서버는 기본적으로 스프링 시큐리티의 LDAP 인증에서는 로컬 호스트(local host)의 33389 포트로 LDAP 서버가 접속된다고 간주
  • 만약 LDAP서버가 다른 컴퓨터에서 실행 중이라면 contextSource()를 사용해 해당 서버의 위치를 구성
@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth
		.ldapAuthentication()
		.passwordEncoder(encoder());

		.ldapAuthentication()
		.userSearchBase("ou=people")
		.userSearchFilter("(uid={0})")
		.groupSearchBase("ou=groups")
		.groupSearchFilter("member={0}")
        
		.passwordCompare()
		.passwordEncoder(new BCryptPasswordEncoder())
		.passwordAttribute("userPasscode");
        	.contextSource().url("ldap://tacocloud.com:389/dc=tacocloud,dc=com")
	}
  • contextSource() 메서드
    • ContextSourceBuilder 반환
      • url() 메서드를 제공
      • LDAP 서버의 위치를 지정할 수 있음

 

LDAP - 내장된 LDAP 서버 구성

  • 인증을 기다리는 LDAP 서버가 없는 경우 스프링 시큐리티에서 제공하는 내장 LDAP 서버를 사용
  • 내장된 LDAP 서버를 사용할 때는 원격 LDAP 서버의 URL을 설정하는 대신 root() 메서드를 사용해 내장 LDAP 서버의 루트경로를 지정 가능
@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth
		.ldapAuthentication()
		.passwordEncoder(encoder());
		
		.ldapAuthentication()
		.userSearchBase("ou=people")
		.userSearchFilter("(uid={0})")
		.groupSearchBase("ou=groups")
		.groupSearchFilter("member={0}")
		.contextSource()
		.root("dc=tacocloud,dc=com")
		.ldif("classpath:users.ldif")
		.and()
		.passwordCompare()
		.passwordEncoder(new BCryptPasswordEncoder())
		.passwordAttribute("userPasscode");
    }
  • LDAP 서버 시작
    • classpath에서 찾을 수 있는 LDIF(LDAP Data Interchange Format) 파일로부터 데이터를 로드
    • LDIF는 일반 텍스트 파일에 LDAP 데이터를 나타내느 표준화된 방법
    • 각 레코드는 하나 이상의 줄로 구성
    • 각 줄은 한 쌍으로 된 name:value 를 포함
    • .ldif("classpath:users.ldif") 파일을 찾아 LDAP 서버로 데이터를 로드하라고 요청

<내부 저장소에 있는 users.ldif>

dn: ou=groups,dc=tacocloud,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=tacocloud,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=tacocloud,ou=people,dc=tacocloud,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: GD Hong
sn: Hong
uid: user1
userPasscode: password1

dn: uid=tacocloud,ou=people,dc=tacocloud,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: MS Park
sn: Park
uid: user2
userPasscode: password2

dn: cn=USER,ou=groups,dc=tacocloud,dc=com
objectclass: top
objectclass: groupOfNames
cn: USER
member: uid=user1,ou=people,dc=tacocloud,dc=com
member: uid=user2,ou=people,dc=tacocloud,dc=com
  • 현재 위의 방식 그래도 진행 시 로그인이 불가
    • LDIF 파일에서 각 passCode 값이 암호화 되지 않은 것이기 때문에 사용자가 입력한 비밀번호를 LDAP 서버에서 암호화 하여 비교하면 일치하지 않기 때문
    • 미리 정해둔 passCode를 BCryptPasswordEncoder로 암호화한 결과값을 알아낸 후 LDIF 파일 사용자의 passCode 값으로 교체할 필요가 있음

사용자 인증의 커스터마이징

스프링에 내장된 사용자 스토어 외 사용자 명세 서비스를 커스터마이징해서 사용이 가능 
UserDetails를 구현받는 엔티티를 생성해야함.

사용자 도메인 객체와 퍼시스턴스 정의

@Entity
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
public class User implements UserDetails {
	private static final long serialVersionUID = 1L;
	
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	
	private final String username;
	private final String password;
	private final String fullname;
	private final String street;
	private final String city;
	private final String state;
	private final String zip;
	private final String phoneNumber;
	
	@Override
	public Collection<? extends
			GrantedAuthority> getAuthorities() {
		return Arrays.asList(new
				SimpleGrantedAuthority("ROLE_USER"));
	}
	
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}
	
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}
	
	@Override
	public boolean isEnabled() {
		return true;
	}
}

<코드 분석>

  • 스프링 Security의 UserDetails 인터페이스 구현
    • 기본 사용자 정보를 프레임워크에 제공
    • 해당 사용자에게 부여된 권한과 해당 사용자 계정을 사용할 수 있는지의 여부
  • getAuthorities()
    • 해당 사용자에게 부여된 권한을 저장한 컬렉션을 반환
  • is~Expired()
    • 오버라이드 함수
    • is~Expired()인 함수는 해당 사용자 계정의 활성화 또는 비활성화 여부를 나타내는 Boolean값을 반환
    • 현재는 클라우드에서 사용자를 비활성화 할 필요가 없으므로 true로 반환

참고: UserDetails

  • 메소드명리턴타입설명
getAuthorities()  Collection<? extends   GrantedAuthority>  계정이 갖고있는 권한 목록을 리턴한다.
 getPassword()  String  계정의 비밀번호를 리턴한다.
 getUsername()  String  계정의 이름을 리턴한다.
 isAccountNonExpired()  boolean  계정이 만료되지 않았는 지 리턴한다. (true: 만료안됨)
 isAccountNonLocked()  boolean  계정이 잠겨있지 않았는 지 리턴한다. (true: 잠기지 않음)
 isCredentialNonExpired()  boolean  비밀번호가 만료되지 않았는 지 리턴한다. (true: 만료안됨)
 isEnabled()  boolean  계정이 활성화(사용가능)인 지 리턴한다. (true: 활성화)

 

<UserRepository 인터페이스>

package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.User;

public interface UserRepository extends CrudRepository<User, Long> {
	User findByUsername(String username);
}

<코드 분석>

  • CRUD 연산에 추가하여, UserRepository는 findByUsername() 메서드를 추가 정의
    • 사용자의 이름(id)로 유저를 찾기 위해 사용자 명세 서비스에서 이용

 

<UserDetailsService>

public interface UserDetailsService{
	UserDetails loudUserByUsername(String name) throws UsernameNotFoundException;
}
  • 사용자 이름이 인자로 전달

UserDetails 인터페이스 정보

 

 

<커스텀 사용자 명세 서비스 정의>

@Service
public class UserRepositoryUserDetailsService
		implements UserDetailsService {
	private UserRepository userRepo;
	
	@Autowired
	public UserRepositoryUserDetailsService(UserRepository userRepo) {
		this.userRepo = userRepo;
	}
	
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		User user = userRepo.findByUsername(username);
		if (user != null) {
			return user;
		}
		throw new UsernameNotFoundException(
				"User '" + username + "' not found");
	}
}

<코드 분석>

  • @Service
    • 스프링의 스테레오타입 어노테이션 중 하나
    • 스프링이 컴포넌트 검색을 해준다는 것을 의미(빈으로 선언할 필요 x)
  • 생성자를 통해서 UserRepository의 인스턴스 주입
  • loadUserByUsername(String username)
    • UserRepository 인스턴스의 findByUsername() 호출 -> User 찾기

User 정보

 

 

<SecurityConfig 추가 작성>

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests()
		.antMatchers("/design", "/orders")
		.access("hasRole('ROLE_USER')")
		.antMatchers("/", "/**").access("permitAll")
		.and()
		.formLogin()
		.loginPage("/login")
		.and()
		.logout()
		.logoutSuccessUrl("/")
		.and()
		.csrf();
	}

	@Autowired
	private UserDetailsService userDetailsService;
	
	@Bean
	public PasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth
		.userDetailsService(userDetailsService)
		.passwordEncoder(encoder());
	}
}
  • SecurityConfig로 자동 주입된 UserDetailsService 인스턴스를 인자로 전달하여 userDetailsService() 메서드 호출
  • passwordEncoder(encoder())
    • encoder()에 @Bean 어노테이션이 지정되었음
    • encoder() 메서드가 생성한 BCryptPasswordEncoder 인스턴스가 스프링 애플리케이션 컨텍스트에 등록, 관리
    • 이 인스턴스가 애플리케이션 컨텍스트로부터 주입되어 반환
    • 우리가 원하는 종류의 PasswordEncoder 빈 객체를 스프링의 관리하에 사용가능
    • (클래스와 클래스 인스턴스 생성 및 주입의 전 과정을 스프링이 관리하는 @Component 와는 의미가 다름)

 

사용자 등록하기

SpringSecurity5부터 의무적으로 PasswordEncoder를 이용하여 암호화를 하지 않으면 오류가 발생함.
암호화를 하면 DB에 저장될 때 암호화된 password가 저장이 됨.
ID CITY FULLNAME PASSWORD PHONE_NUMBER STATE STREET USERNAME ZIP
1 Seoul aaa $2a$10$GtCiKop4o9Vlm7CoKryHCuXVehCyNYB5krnMJBbfOylkVInzinc7K 000 sss bb aa aaa
  • 패스워드는 암호화 된 것을 볼 수 있음
  • 스프링 세큐리티는 사용자 등록 절차에는 직접 개입하지 않음

<RegistrationController>

@Controller
@RequestMapping("/register")
public class RegistrationController {
	private UserRepository userRepo;
	private PasswordEncoder passwordEncoder;
	
	public RegistrationController(
			UserRepository userRepo, PasswordEncoder passwordEncoder) {
		this.userRepo = userRepo;
		this.passwordEncoder = passwordEncoder;
	}
	
	@GetMapping
	public String registerForm() {
		return "registration";
	}
	
	@PostMapping
	public String processRegistration(RegistrationForm form) {
		userRepo.save(form.toUser(passwordEncoder));
		return "redirect:/login";
	}
}

<코드 분석>

  • @Controller -> 컴포넌트 자동 검색
  • @RequestMapping("/register")
    • /register 경로의 웹 요청 처리
  • GET 요청 -> registerForm()
    • 논리 뷰의 이름인 registration 반환
    • registration.html은 등록 폼이 제출되면 HTTP POST 요청이 처리됨
    • POST 요청 -> processRegistration(RegistrationForm form)

<registration.html>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	  xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="EUC-KR">
    <title>Taco Cloud</title>
  </head>
  
  <body>
	<h1>Register</h1>
	<img th:src="@{/images/TacoCloud.png}" />
	<form method="POST" th:action="@{/register}" id="registerForm">
		<label for="username">Username: </label> 
		<input type="text" name="username" /><br /> 
		
		<label for="password">Password: </label> 
		<input type="password" name="password" /><br /> 
		
		<label for="confirm">Confirm password: </label> 
		<input type="password" name="confirm" /><br /> 
		
		<label for="fullname">Full name: </label> 
		<input type="text" name="fullname" /><br />
		
		<label for="street">Street: </label> 
		<input type="text" name="street" /><br />
		
		<label for="city">City: </label> 
		<input type="text" name="city" /><br />
		
		<label for="state">State: </label> 
		<input type="text" name="state" /><br />
		
		<label for="zip">Zip: </label> 
		<input type="text" name="zip" /><br />
		
		<label for="phone">Phone: </label> 
		<input type="text" name="phone" /><br />
		
		<input type="submit" value="Register" />
	</form>
  </body>
</html>

<RegistrationForm>

@Data
public class RegistrationForm {
	
	private String username;
	private String password;
	private String fullname;
	private String street;
	private String city;
	private String state;
	private String zip;
	private String phone;
	
	public User toUser(PasswordEncoder passwordEncoder) {
		return new User(
				username, passwordEncoder.encode(password),
				fullname, street, city, state, zip, phone);
	}
}

<코드 분석>

  • toUser
    • RegistrationFrom의 속성 값을 갖는 새로운 User 객체 생성
  • RegistrationController에서 processRegistration()메서드에서 .save() 를 통해 저장
public class NoEncodingPasswordEncoder implements PasswordEncoder {
	
	@Override
	public String encode(CharSequence rawPwd) {
		return rawPwd.toString();
	}
	
	@Override
	public boolean matches(CharSequence rawPwd, String encodedPwd) {
		return rawPwd.toString().equals(encodedPwd);
	}
}

 


참고 자료

1. 크레이그 월즈, Spring in Action, Fifth Edition(출판지 : 제이펍, 2020)

2. https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/UserDetails.html

3. https://docs.spring.io/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/core/userdetails/User.html

반응형

'Spring' 카테고리의 다른 글

Spring - 구성 속성  (0) 2022.05.30
Spring Security 4  (0) 2022.05.26
JDBC - 데이터 UPDATE, INSERT, DELETE  (0) 2022.05.23
JDBC - DBMS 조작 기본 지식, SELECT  (0) 2022.05.21
JDBC - DataSource를 이용한 데이터 연결  (0) 2022.05.20

+ Recent posts