[Spring Security] Database Authentication - Spring Boot (6)

반응형

이번 글에서는 Spring Boot App에 Database Authentication을 적용하는 방법에 대해 알아보도록 하겠습니다.

1. Database Authentication

이번에는 이전글에서 생성한 spring boot app에 Database Authentication을 적용해보겠습니다.

이전글에서는 아래와 같이 WebSecurityConfigurerAdapter를 상속한 SecurityConfiguration에 InMemory Authentication에 사용할 유저 정보를 일일이 입력해두었습니다.

SecurityConfiguriaton

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder().encode("admin123"))
                .roles("ADMIN").authorities("ACCESS_TEST1", "ACCESS_TEST2")
                .and()
                .withUser("user")
                .password(passwordEncoder().encode("user123"))
                .roles("USER")
                .and()
                .withUser("manager")
                .password(passwordEncoder().encode("manager123"))
                .roles("MANAGER").authorities("ACCESS_TEST1");

    }

만약 특정 사용자만 사용하는 서비스라면 위와 같이 사용해도 괜찮지만, 여러 사용자들을 대상으로하는 서비스라면 위와 같은 방법은 사용할 수 없습니다.

따라서 이를 개선해 유저 정보를 Database에 저장하도록 하겠습니다. Data를 DB에 저장하기 위해 JPA를 사용하도록 하겠습니다.

JPA 관련 내용은 여기에서 확인할 수 있습니다. 😎

2. Overview

전체적인 Database Authentication의 흐름은 다음과 같습니다.

• User 데이터를 저장할 User Entity를 생성합니다.
• User를 Database에 저장합니다.
• 생성한 User Entity를 Spring Security의 내장 class와 연결합니다. 이때 UserDetails와 UserDetailsService Interface를 사용합니다.
• Security Configuartion에 Database Auth를 정의합니다.

Spring Security에는 defualt User가 정의되어있으므로 사용자가 직접 정의한 User를 사용하기위해선 Interface를 사용해 연결해야 합니다.

• AppUserPrinficpal / User Entity / AppUserRepository는 사용자가 정의한 클래스입니다.

• UserDetails 클래스는 Spring Security에서 User Entity 역할을 수행합니다.

• UserDetailService 클래스는 Spring Security에서 UserRepository 역할을 수행합니다.

3. User Entity

먼저 User Entity를 아래와 같이 생성하겠습니다.

저는 Lombok을 사용해 getter/setter를 간단히 정의했습니다.

User

@Entity
@Getter
@Setter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    private int active;

    private String roles = "";

    private String permissions = "";

    public User(String username, String password, String roles, String permissions){
        this.username = username;
        this.password = password;
        this.roles = roles;
        this.permissions = permissions;

        this.active = 1;
    }

    protected User(){
    }

    public List<String> getRoleList(){
        if(this.roles.length()>0){
            return Arrays.asList(this.roles.split(","));
        }

        return new ArrayList<>();
    }

    public List<String> getPermissionList(){
        if(this.permissions.length()>0){
            return Arrays.asList(this.permissions.split(","));
        }

        return new ArrayList<>();
    }
}

• Authentication을 사용하기위해 User 필드값에 permission과 role을 반드시 정의합니다.

• 또한, 각 사용자는 여러개의 permission과 role을 가질 수 있도록 String의 seperator로 구분하도록 하겠습니다.

예를 들어 "ACCESS_TEST1"과 "ACCESS_TEST2" permission을 가진경우 permissions 필드에 "ACCESS_TEST1,ACCESS_TEST2"로 저장합니다.

4. User Repository

다음으로 User Repository를 다음과 같이 생성합니다.

UserRepotisory

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

• username으로 User 객체를 찾아 return하도록 선언했습니다.

5. Integrate with Spring Security

이제 위에서 생성한 User Entity를 Spring Security와 연결하겠습니다.

Spring Security에 연결하기위해선 UserDetails와 UserDetailsService를 Implements 해야합니다.

5-1) UserPrincipal

먼저 UserPrincipal로 UserDetails을 implements 하겠습니다.

UserPrincipal

public class UserPrincipal implements UserDetails {

    private User user;

    public UserPrincipal(User user){
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();

        // Extract list of permissions (name)
        this.user.getPermissionList().forEach(p -> {
            GrantedAuthority authority = new SimpleGrantedAuthority(p);
            authorities.add(authority);
        });

        // Extract list of roles (ROLE_name)
        this.user.getRoleList().forEach(p -> {
            GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + p);
            authorities.add(authority);
        });

        return authorities;
    }

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return this.user.getActive() == 1;
    }
}

• UserPrincipal 클래스는 User를 생성자로 전달받아 Spring Security에 User 정보를 전달합니다.

5-2) UserPrincipalDetailService

다음으로 UserPrincipalDetailService로 UserDetailsService를 imeplements 합니다.

UserPrincipalDetailsService

@Service
public class UserPrincipalDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    public UserPrincipalDetailsService(UserRepository userRepository){
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = this.userRepository.findByUsername(username);
        UserPrincipal userPrincipal = new UserPrincipal(user);

        return userPrincipal;
    }
}

• UserPrincipalDetailsService는 UserRepository를 생성자로 주입받아, User 정보를 DB에서 가져옵니다.

• DB에서 가져온 User 정보는 UserPrincipal 클래스로 변경해 Spring Security로 전달합니다.

• UserPrincipal은 Spring Security의 UserDetails를 implements 하였으므로, 이제 Spring Security는 User Entity를 사용해 Authentication을 사용할 수 있게되었습니다.

6. API Controller

이제 user 정보를 가져올 수 있도록 API Controller를 아래와 같이 수정하겠습니다.

@RestController
@RequestMapping("api/public")
@RequiredArgsConstructor
public class PublicRestApiController {

    private final UserRepository userRepository;

    @GetMapping("test1")
    public String test1(){
        return "API Test 1";
    }

    @GetMapping("test2")
    public String test2(){
        return "API Test 2";
    }

    @GetMapping("users")
    public List<User> allUsers(){
        return this.userRepository.findAll();
    }
}

• /api/public/users url를 호출하면 전체 user 정보를 return합니다.

7. Configure Database Provider

이제 SecurityConfiguration을 수정해 database Authentication을 사용할 수 있도록 변경하겠습니다.

SecurityConfiguration

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final UserPrincipalDetailsService userPrincipalDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

    @Bean
    DaoAuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(this.userPrincipalDetailsService);

        return daoAuthenticationProvider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                //.anyRequest().authenticated()
                .antMatchers("/index.html").permitAll()
                .antMatchers("/profile/**").authenticated()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/manager/**").hasAnyRole("ADMIN","MANAGER")
                .antMatchers("/api/public/test1").hasAuthority("ACCESS_TEST1")
                .antMatchers("/api/public/test2").hasAuthority("ACCESS_TEST2")
                .antMatchers("/api/public/users").hasRole("ADMIN")
                .and()
                .httpBasic();
    }

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

• database authentication을 사용하기위해 DaoAuthenticationProvider를 정의합니다.

• 정의한 DaoAuthenticationProvider를 configure 메서드의 authenticationProvider에 전달합니다.

• antMatchers에 "/api/public/users/" 호출은 ADMIN 권한을 가진 user만 호출할 수 있도록 Authentication을 추가합니다.

8. DB Init Data

마지막으로 이번 실습에 사용할 User 정보를 임의로 Database에 저장하는 DbInit 클래스를 생성하겠습니다.

DbInit

@RequiredArgsConstructor
@Service
public class DbInit implements CommandLineRunner {

    private final UserRepository userRepository;

    private final PasswordEncoder passwordEncoder;

    @Override
    public void run(String... args) throws Exception {
        // Delete all
        this.userRepository.deleteAll();

        // create users
        User minho = new User("minho", passwordEncoder.encode("minho123"),"USER","");
        User admin = new User("admin", passwordEncoder.encode("admin123"),"ADMIN","ACCESS_TEST1,ACCESS_TEST2");
        User manager = new User("manager", passwordEncoder.encode("manager123"),"MANAGER","ACCESS_TEST1");

        List<User> users = Arrays.asList(minho, admin, manager);

        // save to db
        this.userRepository.saveAll(users);
    }
}

• @Service를 선언한 클래스에서 CommandLineRunner를 implements하면, 서비스 시작시 정의된 run() 메서드를 자동으로 수행합니다.

• Configuration에 정의한 DaoAuthentciatonProvider에서 사용한 PasswordEcoder를 사용해 user의 passsword를 encode해 저장합니다.

9. Test

이제 테스트해보겠습니다. 😎

어플리케이션을 실행하고, "/api/public/users"로 접근하겠습니다.

위에서 admin/admin123을 입력하면 아래와 같이 정상적으로 전체 user 정보를 return 하는 것을 확인할 수 있습니다.

만약 권한이 없는 사용자로 접근할 경우 아래와 같이 에러가 발생하게 됩니다.

이로써 Spring Boot App에 Database 기반 Authentication을 사용할 수 있게되었습니다. 👏👏👏


참고 자료 : https://www.youtube.com/playlist?list=PLVApX3evDwJ1d0lKKHssPQvzv2Ao3e__Q


추천서적

 

스프링5 레시피:스프링 애플리케이션 개발에 유용한 161가지 문제 해결 기법

COUPANG

www.coupang.com

파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음


반응형

댓글

Designed by JB FACTORY