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 |