도서 '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 포함
- BCryptPasswordEncoder : bcrypt를 해싱 암호화
- NoOpPasswordEncoder : 암호화 하지 않음
- Pbkdf2PasswordEncoder : Pbkdf2를 암호화
- SCryptPasswordEncoder : scrypt를 해싱 암호화
- 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 서버의 위치를 지정할 수 있음
- ContextSourceBuilder 반환
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;
}
- 사용자 이름이 인자로 전달
<커스텀 사용자 명세 서비스 정의>
@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 찾기
<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)
'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 |