kok202
Security 기초 (2)

2019. 7. 13. 04:33[정리] 기능별 개념 정리/Security + OAuth

강의 출처

강의 문서

 

해당 강의의 목적

기초(1) 에 이어서 진행.

1. MVC 를 @Controller 으로 구현한다. -> MvcConfig 클래스 삭제

2. UserDetailService 를 좀 더 현실적으로 만든다.

 

 

 

 

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home", "/member").permitAll() // 이 경로는 허용한다
                .antMatchers("/admin/**").hasRole(Account.ROLE_ADMIN) // 이 경로의 접근은 Role 이 ADMIN 이여야만 한다.
                .anyRequest().authenticated() // 나머지 요청은 인증이 필요하다.
                .and()
            .formLogin()
                .loginPage("/login").permitAll() // 이 경로는 로그인 폼이므로 허용한다.
                .and()
            .logout()
                .logoutSuccessUrl("/home")
                .permitAll();
    }
}

Account

@Data
@Entity("account")
public class Account {
    pirvate final static String ROLE_ADMIN = "ROLE_ADMIN";
    pirvate final static String ROLE_USER = "ROLE_USER";

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private long id;
    @Column(unique=true)
    private String email;
    private String password;
    private String role;
}

AccountRepository

public interface AccountRepository implements JPARepository<Long, Account>{
    public Account findByEmail(String email);
}

AccountService

@Service
public class AccountService implements UserDetailsService{
    @Autowired
    private AccountRepository accountRepository;
    
    public Account save(Account account){
        account.setRole(Account.ROLE_USER);
        return accountRepository.save(account);
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        Account account = accounts.findByEmail(username);
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        authorities.add(new SimpleGrantedAuthority(account.getRole()));
        User user = new User(account.getEmail(), account.getPasword(), authorities);
        return user; 
    }
}

User 는 UserDetails 를 구현해놓은 기본 구현체이다. UserDetails 는 로그인 기능을 구현하기위해 대부분의 기업에서 사용하는 보편적인 정보를 추상화해 놓은 인터페이스다. 즉 UserDetails 을 만드는 과정은 Spring security 가 제공하는 로그인 인터페이스에 개발자의 커스텀한 회원 스키마를 맵핑하는 과정이다.

 

AccountController

@Controller
public class AccountController{
    @Autowired
    private AccountService accountService;

    @PostMapping("/account")
    public Account create(@RequestBody Account account) {
        return accountService.save(account);
    }
}

 

 

 

사용자 생성 요청

curl -XPOST http://localhost:8080/member \
     -H 'Content-Type: application/json' \ 
     -d '{"email"="kok202@mail.com", "password"="123456"}'

사용자 생성 응답

{"id"=1, "email"="kok202@mail.com", "password"="123456", "role"="ROLE_USER"}

 

이제 로그인을 해보려고 하면 로그인이 되지 않는다.

사용자가 로그인 시나리오를 따라가보자.

1. kok202@mail.com / 123456 으로 접속을 요청받는다.

2. Spring security 는 UserDetailsService 인 인터페이스를 주입 받아 갖고 있다.

3. Spring security 가 갖고 있는 UserDetailsService 는 AccountService 이다.

4. Spring security 는 AccountService 안의 loadUserByUsername("kok202@mail.com") 을 호출한다.

5. loadUserByUsername("kok202@mail.com") 의 요청 결과로 UserDetails 가 반환된다.

6. loadUserByUsername("kok202@mail.com") 의 요청 결과에 .getPassword() 를 한다.

7. 이 때 불러온 값에 어떤 해쉬 암호화를 사용했는지 확인한다.

8. 그 암호화 방법으로 123456 을 해쉬로 돌려본다.

9. 두 개의 값이 일치하는지 확인한다.

 

그런데 현재의 프로젝트에서는 암호화해서 password 를 저장하지 않고있다. 그래서 매칭되는 PasswordEncoder 가 없다는 에러가 발생한다. 참고로 PasswordEncoder 를 사용하여 해쉬 함수를 돌리면, 해쉬의 결과에 어떤 암호화 방식을 사용했는지 접두어로 적어서 반환한다. (ex. {bcrypt} ) Spring security는 이를 통해 7 번 과정을 수행할 수있다. 더불어 당연하게도 DB 에 패스워드를 평문으로 저장하는 짓은 미친 짓이다.

 

 

 

 

 

 


PasswordEncoder

1. PasswordEncoder 를 Bean 으로 생성한다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home", "/member").permitAll() // 이 경로는 허용한다
                .antMatchers("/admin/**").hasRole(Account.ROLE_ADMIN) // 이 경로의 접근은 Role 이 ADMIN 이여야만 한다.
                .anyRequest().authenticated() // 나머지 요청은 인증이 필요하다.
                .and()
            .formLogin()
                .loginPage("/login").permitAll() // 이 경로는 로그인 폼이므로 허용한다.
                .and()
            .logout()
                .logoutSuccessUrl("/home")
                .permitAll();
    }
    
    @Bean
    PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

2. Account 를 저장할 때 password 를 암호화해서 저장한다.

@Service
public class AccountService implements UserDetailsService{
    @Autowired
    private AccountRepository accountRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public Account save(Account account){
        account.setRole(Account.ROLE_USER);
        account.setPassword(passwordEncoder.encode(account))
        return accountRepository.save(account);
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        Account account = accounts.findByEmail(username);
        UserDetails user
        return null;
    }
}

 

 

 

사용자 생성 요청

curl -XPOST http://localhost:8080/member \
     -H 'Content-Type: application/json' \ 
     -d '{"email"="kok202@mail.com", "password"="123456"}'

사용자 생성 응답

{"id"=1, "email"="kok202@mail.com", "password"="{bcrypt}$2a$10$dIUF15sVCG18vruni20rUOXXIkxm1T6RARTycQ3QvriQUCM0oANfm", "role"="ROLE_USER"}

 

 

 

 

 


부록 A.

UserDetails 를 커스터마이징 할 수도 있다. 

@Service
public class AccountService implements UserDetailsService{
    @Autowired
    private AccountRepository accountRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public Account save(Account account){
        account.setRole(Account.ROLE_USER);
        account.setPassword(passwordEncoder.encode(account))
        return accountRepository.save(account);
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        Account account = accounts.findByEmail(username);
        UserDetails userDetails = new UserDetails(){
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                authorities.add(new SimpleGrantedAuthority(account.getRole()));
                return authorities;
            }

            @Override
            public String getPassword() {
                return account.getPassword();
            }

            @Override
            public String getUsername() {
                return account.getEmail();
            }

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

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

            @Override
            public boolean isCredentialsNonExpired() {
                return true; // 30일마다 패스워드 바꾸고 싶으면 이런 설정을 건드려주면된다.
            }

            @Override
            public boolean isEnabled() {
                return true; // 오래됬을 경우 계정을 비활성화 하고 싶으면 이를 사용하면 된다.
            }
        }
        return userDetails;
    }
}

 

부록 B.

Account 가 UserDetails 를 상속하게 구현할 수도 있을 것이다.

@Data
@Entity("account")
public class Account implements UserDetails{
    pirvate final static String ROLE_ADMIN = "ROLE_ADMIN";
    pirvate final static String ROLE_USER = "ROLE_USER";

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private long id;
    @Column(unique=true)
    private String email;
    private String password;
    private String role;
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        authorities.add(new SimpleGrantedAuthority(account.getRole()));
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.email;
    }

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

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

    @Override
    public boolean isCredentialsNonExpired() {
        return true; // 30일마다 패스워드 바꾸고 싶으면 이런 설정을 건드려주면된다.
    }

    @Override
    public boolean isEnabled() {
        return true; // 오래됬을 경우 계정을 비활성화 하고 싶으면 이를 사용하면 된다.
    }
}
@Service
public class AccountService implements UserDetailsService{
    @Autowired
    private AccountRepository accountRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public Account save(Account account){
        account.setRole(Account.ROLE_USER);
        account.setPassword(passwordEncoder.encode(account))
        return accountRepository.save(account);
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        Account account = accounts.findByEmail(username);
        return account;
    }
}

 

 

 

 

 

'[정리] 기능별 개념 정리 > Security + OAuth' 카테고리의 다른 글

OAuth 그림 요약  (0) 2019.08.23
스프링 시큐리티 주요 인터페이스  (0) 2019.08.03
스프링 시큐리티 개요  (0) 2019.08.03
Security 기초 (1)  (0) 2019.07.13
Security OAuth2 강의 정리  (0) 2019.07.13