개발자의 오르막

[SpringSecurity #02] 스프링 시큐리티 JPA 연동 본문

SpringFrameWork/SpringSecurity

[SpringSecurity #02] 스프링 시큐리티 JPA 연동

계단 2020. 9. 20. 12:51

# 스프링 시큐리티 JPA 연동하기

 

1. 먼저 JPA와 연동하기 위해 JPA, 데이터베이스인 h2 의 의존성을 추가한다.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'

 

2. 스프링 시큐리티에서 로그인할 계정정보 테이블을 만들기 위해 아래와 같은 트리구조를 형성해준다.

 

 

- Account 도메인 클래스

@Entity
@Getter
@Setter
public class Account {

    @Id
    @GeneratedValue
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    private String role;

}

 

- Account Jpa Repository

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    Account findAccountByUsername(String username);
}

 

- AccountService

@Service
@RequiredArgsConstructor
public class AccountService implements UserDetailsService {

    private final AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findAccountByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException(username);
        }

        return User.builder()
                .username(account.getUsername())
                .password(account.getPassword())
                .roles(account.getRole())
                .build();
    }
}

 

  이때 implements 한 UserDetailsService 는 시큐리티에서 제공하는 인터페이스이다.

  loadUserByUsername 을 Override 를 시킨다.

  이 메소드를 활용하면 데이터베이스에 있는 유저 정보를 로그인에 사용하는 메소드이며,

  재정의를 통해 로그인 유저를 가져오는 방법을 커스텀해줄 수 있다.

  (JPA 활용 안하면 여기에 DAO 를 통해 유저 정보를 가져온다.)

 

  이 메소드는 사용자가 로그인 할 때 입력하는 username 을 통해 계정정보를 가져와서

  UserDetails 라는 타입으로 리턴해주기 위한 메소드이다.

 

  UserDetails 라는 객체를 만들 때 위와 같이 build 형태를 따르며, 이때 데이터에서 가져온

  account 의 username, password, role 를 주입시켜 생성한다.

 

 

 


3. 그럼 이제 테스트를 위한 계정 정보 등록 테스트 코드를 짜준다.

@RestController
@RequiredArgsConstructor
public class AccountController {

    private final AccountRepository accountRepository;
    private final AccountService accountService;

    @GetMapping("/account/{role}/{username}/{password}")
    public Account createAccount(@ModelAttribute Account account) {
        Account newAccount = accountService.createNew(account);
        return newAccount;
    }
}

  이때 url 로 받은 정보들을 Acconut 객체로 맵핑하여 AccountService 에서 저장을 시켜준다.

 

- AccountService

public Account createNew(Account account) {
        account.encodePassword(account);
        return this.accountRepository.save(account);
    }

 

- Account

public void encodePassword(Account account) {
        this.password = "{noop}" + account.getPassword();
}

 

  위의 메소드들을 사용해 사용자에게 입력받은 패스워드를 인코딩하여 정보를 저장하고, 그 객체를 Return 한다.

  * {noop} 은 인코딩을 사용하지 않은 것을 의미한다.

 

 

 

  그리고 위의 url 이 로그인 없이 동작할 수 있도록 아래처럼 설정을 바꿔준다.

 

- SecurityConfig

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/", "/info", "/account/**").permitAll()
                .mvcMatchers("/admin").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .httpBasic();
    }

 

 

  그러면 H2 데이터베이스에 저장된 유저 정보로 로그인이 가능해진다. (일반 유저, 관리자)

 

 


4. 이제 위에서 설정했던 인코딩을 좀더 리팩토링 해보도록 하겠다.

 

   먼저 GongmoApplication 에 PasswordEncoder 를 Bean으로 등록함으로써 어디서든 사용가능하게 한다.

@SpringBootApplication
public class GongmoApplication {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    public static void main(String[] args) {
        SpringApplication.run(GongmoApplication.class, args);
    }

}

  이때 NoOpPasswordEncoder 는 옛날버전에서 쓰던 메소드로, 위의 {noop} 와 같은 기능을 가지는 인코더이다.

 

 

- AccountService

public Account createNew(Account account) {
        account.encodePassword(passwordEncoder);
        return this.accountRepository.save(account);
}

  AccountService 에서 PasswordEncoder 의존성 주입을 한 후 Account 모델에 파라미터로 넘긴다.

 

- Account

public void encodePassword(PasswordEncoder passwordEncoder) {
        this.password = passwordEncoder.encode(this.password);
}

  Account 클래스에 password 를 인코딩 할 수 있도록 코드를 수정해준다.

 

 

 

5. 그러면 이제 Security 에서 권장하는 Password Encoder 를 사용하도록 하겠다.

 

    @Bean
    public PasswordEncoder passwordEncoder() {
//        return NoOpPasswordEncoder.getInstance();
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

  기존에 사용하던 Encoder 대신 위의 Encoder를 사용한다.

 

{
  "id":1,
  "username":"jake",
  "password":"{bcrypt}$2a$10$oFTspgzWB8lk/7eT4zEDu.mFrbaUxeiOEjdZ2B5RvmcVVzHBD8GAC",
  "role":"USER"
}

  그러면 위와 같이 암호화되어 저장된다.

Comments