티스토리 뷰

JPA와 Security 연동

이전의 문제점

  1. 매번 유저를 추가하는 일이 생길 경우 코드를 수정해야 한다.
  2. 수정, 삭제도 마찬가지로 코드를 수정해야 한다.

-> 위와 같은 문제를 DB를 연동하여 유저 정보를 관리할 수 있도록 수정해보겠습니다. (JPA를 사용하겠습니다.)

개선

JPA 설정

dependency 추가

  • build.gradle
dependencies {
...
    // JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    // H2
    runtimeOnly 'com.h2database:h2'
...
}

class 생성

  • Account
@Entity
@Getter
@Setter
public class Account {

    @Id @GeneratedValue
    private Long id;
    @Column(unique = true)
    private String username;
    private String password;
    private String role;

}
  • AccountRepository
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    Account findByUsername(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.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException(username);  // 1
        }

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

    public Account createAccount(Account account) {
        return accountRepository.save(account);
    }

}
  1. 해당 Username 으로 찾는 유저 정보가 없는 경우 UsernameNotFoundException을 터트리도록 합니다.
  2. loadUserByUsername()은 UserDeatils를 리턴하는데 우리가 가진 유저객체는 Account이므로 UserDetails를 구현해둔 User를 빌드하여 리턴한다

테스트를 위한 회원등록 Controller

  • AccountController
@RestController
@RequiredArgsConstructor
public class AccountController {
    private final AccountService accountService;

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

실제로 회원가입기능을 이런식으로 만들면 안됩니다 간단한 테스트용입니다.

프로젝트 재시동 후

  • http://localhost:8080/account/USER/alxndr/1234
  • http://localhost:8080/account/ADMIN/admin/1234 호출하여 유저 정보를 등록해줍니다.

그 후 /dashboard 에 접근하여 방금 등록한 정보로 로그인해봅니다.

그러면 에러가 발생하게 되는데 

 스프링 시큐리티는 특정한 패스워드 패턴을 요구하는데 ({noop}1234) 현재 저장된 password는 123으로 저장되어 있기 때문에 발생하는 에러입니다.

에러 개선

우선은 패스워드를 스프링 시큐리티가 원하는 규격으로 만들어주겠습니다.

  • AccountService
public class AccountService implements UserDetailsService {
    ... 생략    
    public Account createAccount(Account account) {
        account.setPassword("{noop}" + account.getPassword());  // 1
        return accountRepository.save(account);
    }
}
  1. Inmemory user를 저장할 떄 처럼 패스워드 앞에 {noop}을 붙여서 저장을 해주도록 하겠습니다.

프로젝트를 재시동 후 다시 한번

  • http://localhost:8080/account/USER/alxndr/1234
  • http://localhost:8080/account/ADMIN/admin/1234
    회원을 등록해주면 아래와 같이 {noop}YOUR-PASSWORD로 저장되고
    /dashboard에 접근하여 로그인해보면 정상적으로 동작하는 것을 확인할 수 있습니다.

명시적으로 UserDetailsService 지정하기

  • SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    private final AccountService accountService;
    ....생략
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(accountService);
    }
}

위 처럼 AuthenticationManagerBuilder로 명시적으로 지정해줄 수 있지만, 하지 않아도 UserDeatilsService를 구현한 구현체가 Bean으로 등록만 되어있다면 사용에 문제는 없습니다.

마무리

Inmemory User의 불편한 점들을 JPA와의 연동으로 해결해봤습니다. 다음으로는 임시로 적어준 {noop}을 스프링 시큐리티가 제공해주는 PasswordEncoder 를 활용하여 개선해보겠습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함