ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링 인 액션] Chapter 4 - 스프링 시큐리티 :: 스프링 시큐리티 구성하기
    개발서적읽기/Spring in Action 제 5판 2020. 7. 30. 10:07



    장황한 XML 기반의 구성을 포함해서 


    그동안 스프링 시큐리티를 구성하는 방법은 여러가지가 있었다.


    다행스럽게도 최근의 여러 스프링 시큐리티 버전에서는 훨씬 더 알기 쉬운 


    자바 기반의 구성을 지원한다.


    이번 장이 끝나기 전까지 타코 클라우드 보안의 모든 요구사항은 


    자바 기반의 스프링 시큐리티 구성으로 구현하게 될 것이다.


    우선 기본 구성 클래스를 작성해보자.

    package tacos.security;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

    @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()
    .httpBasic();
    }

    @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");
    }
    }

    SecurityConfig는 사용자의 HTTP 요청 경로에 대해 접근 제한과 같은 보안 관련 처리를


    우리가 원하는 대로 할 수 있게 해준다.


    서버를 실행해서 http://localhost:8080에 접속해 보자.


    기본 페이지가 잘 실행될 것이다.


    하지만 http://localhost:8080/design에 접속하면 


    스프링 시큐리티 HTTP 기본 인증 대화상자 대신 다른 HTTP 로그인 대화상자를 볼 수 있다.


    이 대화상자에 user1 / password1 을 입력하면 로그인할 수 있다.


    하지만 지금까지 봤던 두 대화상자 모두 타코 애플리케이션에서 사용하기엔 적합하지 않다.


    새로운 로그인 페이지를 만들어보자.


    보안을 테스트할 때는 웹 브라우저를 private 또는 incognito 모드로 설정하는 것이 좋다.


    왜냐하면 사용자의 검색 세션에 관한 데이터인 쿠키, 임시 인터넷 파일,


    열어 본 페이지 목록 및 기타 데이터를 저장하지 못하도록 할 수 있기 때문이다.


    따라서 브라우저의 창을 열 때마다 이전 세션의 사용 기록이 반영되지 않는


    새로운 세션으로 시작된다. 단, 애플리케이션을 테스트할 때 매번 로그인을 해야 한다.


    그러나 보안 관련 변경이 확실하게 적용됐는지는 분명히 확인할 수 있다.


    타코 클라우드 애플리케이션의 로그인 페이지를 생성하고 보안을 구성하기에 앞서


    먼저 알아 둘 것이 있다. 즉, 한 명 이상의 사용자를 처리할 수 있도록 사용자 정보를


    유지, 관리하는 사용자 스토어를 구성하는 것이다. 스프링 시큐리티에서는 여러 가지의


    사용자 스토어 구성 방법을 제공한다.


    - 인메모리 사용자 스토어


    - JDBC 기반 사용자 스토어


    - LDAP 기반 사용자 스토어


    - 커스텀 사용자 명세 서비스


    SecurityConfig 클래스는 보안 구성 클래스인 WebSecurityConfigurerAdapter의 서브 클래스다.


    그리고 두 개의 configure()를 오버라이딩하고 있다.


    configure(HttpSecurity)는 HTTP 보안을 구성하는 메서드다.


    그리고 configure(AuthenticationManagerBuilder)는 사용자 인증 정보를 구성하는 메서드이며


    위의 사용자 스토어중 어떤 것을 선택하든 이 메서드에서 구성한다.


    우선 configure(AuthenticationManagerBuilder)를 오버라이딩하여 


    인메모리 DB에 사용자 스토어를 구성해보자.




    ■인메모리 사용자 스토어


    사용자 정보를 유지, 관리할 수 있는 곳 중 하나가 메모리다.


    만일 변경이 필요 없는 사용자만 미리 정해놓고 애플리케이션을 사용한다면


    아예 보안 구성 코드 내부에 정의할 수 있을 것이다.


    예를 들어 "user1"과 "user2"라는 사용자를 인메모리 사용자 스토에어 등록하는 방법이다.


    (위 코드와 동일하다)

    @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");
    }

    AuthenticationManagerBuilder는 인증 명세를 구성하기 위해


    빌더 형태의 API를 사용한다. 


    이때는 inMemoryAuthentication()을 사용하여 


    보안 구성 자체에 사용자 정보를 직접 지정할 수 있다.


    권한 부여는 authorities()를 사용해도 되고 roles()를 사용해도 된다.


    스프링 5부터는 반드시 비밀번호를 암호화해야 하므로 만일 password()를 호출하여


    암호화하지 않으면 접근 거부(HTTP 403) 또는 Internal Server Error(HTTP 500) 가 발생한다.


    그러나 인메모리 사용자 스토어의 간단한 테스트를 위해서 {noop}를 지정했다.


    이는 비밀번호를 암호화하지 않는다는 의미다.


    인메모리 사용자 스토어는 테스트 목적이나 간단한 애플리케이션에는 편하다.


    하지만 사용자 정보의 추가나 변경이 쉽지 않다.


    즉, 사용자의 추가, 삭제, 변경을 해야 한다면 보안 구성 코드를 변경한 후 


    애플리케이션을 다시 빌드하고 배포, 설치해야 한다.


    타코 클라우드 애플리케이션의 경우는 고객 스스로 사용자로 등록하고


    자신의 정보를 변경할 수 있어야 한다. 따라서 인메모리 사용자 스토어에는 적합하지 않다.


    지금부터는 DB로 지원되는 사용자 스토어를 알아보자.




    ■JDBC 기반의 사용자 스토어


    사용자 정보는 RDB로 유지, 관리되는 경우가 많다.


    따라서 JDBC 기반의 사용자 스토어가 적합하다.

        @Autowired
    DataSource dataSource;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
    .dataSource(dataSource);
    // auth.inMemoryAuthentication()
    // .withUser("user1")
    // .password("{noop}password1")
    // .authorities("ROLE_USER")
    // .and()
    // .withUser("user2")
    // .password("{noop}password2")
    // .authorities("ROLE_USER");
    }

    스프링 시큐리티의 기본 사용자 쿼리를 대체하기


    스프링 시큐리티의 사용자 정보 DB 스키마를 사용할 때는 


    방금 전에 작성한 configure()면 충분하다.


    사용자 정보를 저장하는 테이블과 열이 정해져 있고 쿼리가 미리 생성되어 있기 때문이다.


    즉, 사용자 정보를 찾을 때 스프링 시큐리티의 내부 코드에서는 


    기본적으로 다음 쿼리를 수행한다.

    public static final String DEF_USERS_BY_USERNAME_QUERY =
    "select username,password,enabled " +
    "from users " +
    "where username = ?";

    public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
    "select username, authority " +
    "from authorities " +
    "where username = ?";
    public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
    "select g.id, g.group_name, ga.authority " +
    "from authorities g, group_members gm, group_authorities ga " +
    "where gm.username = ?" +
    "and g.id = ga.group_id" +
    "and g.id = gm.group_id";

    이것을 보면 내부적으로 기본 생성되는 테이블과 열의 이름을 알 수 있다.


    사용자 정보는 users 테이블에, 권한은 authorities 테이블에, 그룹의 사용자는 


    group_members 테이블에, 그룹의 권한은 group_authorities 테이블에 있다.


    첫 번째 쿼리는 사용자 인증에 사용된다.


    두 번째 쿼리는 해당 사용자에게 부여된 권한을 찾는다.


    마지막 쿼리에서는 해당 사용자가 속한 그룹과 그룹 권한을 찾는다.


    이처럼 스프링 시큐리티에 사전 지정된 DB 테이블과 SQL 쿼리를 사용하려면


    관련 테이블을 생성하고 사용자 데이터를 추가해야 한다.


    resources 폴더 아래에 schema.sql 파일을 만들고 아래 테이블 정의를 추가하자.

    drop table if exists users;
    drop table if exists authorities;
    drop table if exists ix_auth_username;

    craete table if not exists users(
    username varchar2(50) not null primary key,
    password varchar2(50) not null,
    enabled char(1) default '1'
    );

    craete table if not exists authorities(
    username varchar2(50) not null,
    authority varchar2(50) not null,
    constraint fk_authorities_users
    foreign key(username) references users(username)
    );

    craete table if not exists ix_auth_username(
    on authorities (username, authority)
    );

    그리고 data.sql 파일도 추가하자

    insert into users (username, password) values ('user1', 'password1');
    insert into users (username, password) values ('user2', 'password2');

    insert into authorities (username, authority)
    values ('user1', 'ROLE_USER');
    insert into authorities (username, authority)
    values ('user2', 'ROLE_USER');

    commit;

    이제 서버를 재시작 후 http://localhost:8080/design 에 접속하여 


    user1 / password1 을 입력해보자.


    There is no PasswordEncoder mapped for the id "null" 이러한 에러가 날 것이다. 왜냐하면 스프링 시큐리티 5 버전부터는


    의무적으로 PasswordEncoder를 사용해서 비밀번호를 암호화하기 때문이다.


    (DB 테이블에는 암호화되지 않은 비밀번호가 저장되어있다.)


    따라서 제대로 테스트 하려면 비밀번호를 암호화하지 않는


    PasswordEncoder를 임시로 사용해야 한다.


    이는 본래 취지와 어긋나지만 테스트를 위해 어쩔 수 없다.


    우선 나중에 추가하기로 하자.


    지금까지 했던 것처럼 스프링 시큐리티에 사전 지정된 DB 테이블과


    SQL 쿼리를 사용하고 사용자 데이터도 저장했다면 이대로 사용할 수 있다.


    그러나 스프링 시큐리티에서 지정한 테이블이나 열 말고 다른 테이블이나 열을 사용하려면


    스프링 시큐리티의 SQL 쿼리를 우리 SQL 쿼리로 대체하여 사용할 수 있다.

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
    .dataSource(dataSource)
    .usersByUsernameQuery(
    "select username, password, enabled from users " +
    "where username=?")
    .authoritiesByUsernameQuery(
    "select username, authority from authorities " +
    "where username=?");
    }

    이 쿼리에서 사용하는 테이블의 이름은 스프링 시큐리티의 기본 DB 테이블과


    달라도 된다. 그러나 테이블이 갖는 열의 데이터 타입과 길이는 일치해야 한다.


    한편, groupAuthoritiesByUsername()을 호출하여 그룹 권한 쿼리도 대체할 수 있다.


    스프링 시큐리티의 기본 SQL 쿼리를 커스터마이징하려면 다음 사항을 지켜야 한다.


    - where절에 사용되는 매개변수는 하나이며 username이어야 한다.


    - 사용자 정보 인증 쿼리에서는 username, password, enabled 열의 값을 반환해야 한다.


    - 사용자 권한 쿼리에서는 해당 사용자 이름과 부여된 권한을 포함하는


    0 또는 다수의 행을 반환할 수 있다.


    - 그리고 그룹 권한 쿼리에서는 각각 그룹 id, 그룹 이름, 권한열을 갖는


    0 또는 다수의 행을 반환할 수 있다.



    암호화된 비밀번호 사용하기


    비밀번호를 DB에 저장할 때와 사용자가 입력한 비밀번호는 같은 암호화 알고리즘을


    사용해서 암호화해야 한다.


    비밀번호를 암호화할 때는 다음과 같이 passwordEncoder()를 호출하여


    비밀번호 인코더를 지정한다.

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
    .dataSource(dataSource)
    .usersByUsernameQuery(
    "select username, password, enabled from users " +
    "where username=?")
    .authoritiesByUsernameQuery(
    "select username, authority from authorities " +
    "where username=?")
    .passwordEncoder(new BCryptPasswordEncoder());
    }

    passwordEncoder()는 스프링 시큐리티의 PasswordEncoder 인터페이스를 구현하는


    어떤 객체도 인자로 받을 수 있다.


    암호화 알고리즘을 구현한 스프링 시큐리티의 모듈에는 다음과 같은 구현 클래스가 있다.


    - BCryptPasswordEncoder : bcrypt를 해싱 암호화한다.


    - NoOpPasswordEncoder : 암호화하지 않는다.


    - Pbkdf2PasswordEncoder : PBKDF2를 암호화한다.


    - SCryptPasswordEncoder : scrypt를 해싱 암호화한다.


    - StandardPasswordEncoder : SHA-256을 해싱 암호화한다.


    어떤 비밀번호 인코더를 사용하든, 일단 암호화되어 DB에 저장된 


    비밀번호는 해독되지 않는다.


    대신 로그인 시에 사용자가 입력한 비밀번호와 동일한 알고리즘을 사용해서 암호화된다.


    그다음에 DB의 암호화된 비밀번호와 비교되며


    이 일은 PasswordEncoder의 matches()에서 수행된다.


    현재 상태에서 애플리케이션을 다시 시작시킨 후 


    다시 http://localhost:8080/design에 접속해보자.


    그리고 user1 / password1 을 다시 입력해보면 에러는 나지 않는다.


    하지만 로그인 대화상자가 다시 나타난다.


    DB에 저장된 비밀번호가 암호화되지 않았기 때문이다.


    따라서 PasswordEncoder 인터페이스를 구현한, 비밀번호를 암호화하지 않는 클래스를


    임시로 만들어서 사용해야 한다.

    package tacos.security;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

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

    public class NoEncodingPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
    return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
    return charSequence.toString().equals(s);
    }
    }
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
    .dataSource(dataSource)
    .usersByUsernameQuery(
    "select username, password, enabled from users " +
    "where username=?")
    .authoritiesByUsernameQuery(
    "select username, authority from authorities " +
    "where username=?")
    //.passwordEncoder(new BCryptPasswordEncoder());
    .passwordEncoder(new NoEncodingPasswordEncoder());
    }

    이제는 정상적으로 로그인할 수 있다.


    타코 클라우드 사용자 정보는 DB에서 유지, 관리할 것이다.


    그러나 JDBC 기반으로 인증하는 jdbcAuthentication() 대신 다른 인증 방법을 사용해보자.


    우선 그 전에 또 다른 사용자 스토어인 


    LDAP(Lightweight Directory Acess Protocol)를 알아보자.




    ■LDAP 기반 사용자 스토어


    이 챕터는 건너뛴다!




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


    3장에서는 모든 데이터의 persistence를 처리하기 위해 스프링 데이터 JPA를 사용하였다.


    따라서 사용자 데이터도 같은 방법으로 persistence를 처리하는 것이 좋을 것이다.


    이 경우 결국 데이터는 RDB에 저장될 것이므로 JDBC 기반 인증을 사용할 수 있다.


    그러나 사용자 정보의 저장은 스프링 데이터 repository를 사용하는 것이 더 좋을 것이다.


    일단 가장 중요한 것부터 먼저 하자.


    사용자 정보를 저장하는 도메인 객체와 repository 인터페이스를 생성한다.


    사용자 도메인 객체와 persistence 정의하기


    애플리케이션을 사용해서 타코 클라우드 고객이 등록할 때는 사용자 이름과 


    비밀번호 외에 전체 이름, 주소, 전화번호도 제공해야 한다.


    이 정보는 주문 폼에 미리 보여주기 위해 사용되지만,


    이외의 다양한 목적으로도 사용할 수 있다.


    사용자를 나타내는 User클래스를 만들어보자.

    package tacos;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

    import lombok.AccessLevel;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.RequiredArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;

    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import java.util.Arrays;
    import java.util.Collection;

    @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 String getPassword() {
    // return null;
    // }
    //
    // @Override
    // public String getUsername() {
    // return null;
    // }

    @Override
    public boolean isAccountNonExpired() {
    return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    return true;
    }

    @Override
    public boolean isEnabled() {
    return true;
    }
    }

    몇 가지 속성 정의와 더불어 User 클래스는 


    스프링 시큐리티의 UserDetails 인터페이스를 구현한다.


    UserDetails를 구현한 User 클래스는 기본 사용자 정보를 프레임워크에 제공한다.


    예를 들어, 해당 사용자에게 부여된 권한과 해당 사용자 계정을 사용할 수 있는 지 등이다.


    getAuthorities()는 해당 사용자에게 부여된 권한을 저장한 컬렉션을 반환한다.


    메서드 이름이 is로 시작하고 Expired로 끝나는 다양한 메서드들은 


    해당 사용자 계정의 활성화 또는 비활성화 여부를 나타내는 boolean 값을 반환한다.


    User가 정의되었으므로 이제는 다음과 같이 repository 인터페이스를 정의할 수 있다.

    package tacos.data;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

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

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

    CrudRepository 인터페이스를 확장하여 제공된 CRUD 연산에 추가하여


    findByUsername()을 추가로 정의했다.


    그리고 스프링 데이터 JPA는 UserRepository 인터페이스의 구현체를 


    런타임에 자동으로 생성한다.


    사용자 명세 서비스 생성하기


    스프링 시큐리티의 UserDetailsService는 다음과 같이 간단한 인터페이스다.

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //

    package org.springframework.security.core.userdetails;

    public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
    }

    이 코드를 보면 알 수 있듯이, 이 인터페이스를 구현하는 클래스의 메서드에는


    사용자 이름이 인자로 전달된다.


    그리고 메서드 실행 후 UserDetails 객체가 반환되거나


    또는 해당 사용자 이름이 없으면 UsernameNotFoundException을 발생시킨다.


    User 클래스에서는 UserDetails를 구현하고 


    UserRepository에서는 findByUsername()을 제공하므로


    우리의 UserDetailsService 구현 클래스에서 사용해야 하는 모든 것이 준비됐다.

    package tacos.security;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

    import org.springframework.beans.factory.annotation.Autowired;
    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.Service;
    import tacos.User;
    import tacos.data.UserRepository;

    @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'");
    }
    }

    loadByUsername()은 절대로 null을 반환하지 않는다는 규칙이 있다.


    따라서 만일 findByUsername()에서 null을 반환하면 loadByUsername()은


    UsernameNotFoundException을 발생시키며 그렇지 않으면 찾은 User가 반환된다.


    UserRepositoryUserDetailsService 클래스에는 @Service 애노테이션이 지정되어 있다.


    이것은 스프링 스트레오타입 애노테이션 중 하나이며


    스프링이 컴포넌트 검색을 해준다는 것을 나타낸다.


    스프링 시큐리티 구성을 계속 해보자.


    SecurityConfig 클래스의 configure(AuthenticationManagerBuilder) 메소드를 


    다음과 같이 수정하자

    package tacos.security;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;

    import javax.sql.DataSource;

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;

    @Autowired
    private UserDetailsService userRepositoryUserDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .antMatchers("/design", "/orders")
    .access("hasRole('ROLE_USER')")
    .antMatchers("/", "/**")
    .access("permitAll")
    .and()
    .httpBasic();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userRepositoryUserDetailsService);
    }
    }

    그 다음에 JDBC 기반 인증에서 했던 것처럼, 비밀번호가 암호화되어 


    DB에 저장될 수 있도록 비밀번호 인코더를 구성해야 한다.


    우선 PasswordEncoder 타입의 빈을 선언하고 auth에 주입한다.

    package tacos.security;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;

    import javax.sql.DataSource;

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;

    @Autowired
    private UserDetailsService userRepositoryUserDetailsService;

    @Bean
    public PasswordEncoder encoder() {
    return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .antMatchers("/design", "/orders")
    .access("hasRole('ROLE_USER')")
    .antMatchers("/", "/**")
    .access("permitAll")
    .and()
    .httpBasic();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userRepositoryUserDetailsService)
    .passwordEncoder(encoder());
    }
    }

    사용자 등록하기


    스프링 시큐리티에서는 보안의 많은 관점을 알아서 처리해 준다.


    그러나 사용자 등록 절차에는 직접 개입하지 않는다.


    따라서 이것을 처리하기 위한 스프링 MVC 코드를 작성할 것이다.

    package tacos.security;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

    import org.springframework.security.crypto.password.PasswordEncoder;
    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;
    import tacos.data.UserRepository;

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


    package tacos.security;
    /*
    * @USER JungHyun
    * @DATE 2020-07-30
    * @DESCRIPTION
    */

    import lombok.Data;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import tacos.User;

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

    그리고 templates 하위에 registration.html 파일을 만든다.

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org">
    <head>
    <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>

    이렇게 해서 타코 클라우드 애플리케이션의 사용자 등록과 인증 지원이 완성되었다.


    하지만 지금은 애플리케이션을 시작해도 등록 페이지를 볼 수 없다.


    기본적으로 모든 웹 요청은 인증이 필요하기 때문이다.


    이 문제를 해결하기 위해 다음 포스팅에서는 웹 요청의 보안을 처리하는 방법을 살펴본다.

    댓글

Designed by Tistory.