[Spring Security] Spring Boot를 이용한 Database Authentication

dev-log·2021년 10월 10일
2

Spring Security

목록 보기
4/6

이번엔 spring boot app에 Database Authentication을 적용하는 방법을 알아본다.
(이전에 생성한 spring boot app은 여기에서 확인할 수 있다.)

1. Database 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");

    }

이전에는 WebSecurityConfigurerAdapter를 상속한 SecurityConfiguration에 InMemory Authentication에 사용할 유저 정보를 일일이 입력했다.

이렇게 하면 유저가 많지 않은 서비스라면 일일히 입력이 가능하지만, 여러 사용자를 대상으로 하는 서비스라면 위와 같은 방법은 사용할 수 없다.

따라서 이를 개선해 유저 정보를 database에 저장하고 Database Authentication을 사용해보도록 한다.

2. 전체 흐름

    1. 유저의 정보를 저장할 유저 엔티티(User Entity)를 생성한다.
    2. 데이터베이스에 유저를 저장한다.
    3. 생성한 유저 엔티티(User Entity)와 Spring Security의 내장 클래스를 연결한다.
    	3-1) User 와 UserDetails 인터페이스를 연결한다.
        3-2) UserRepository와 UserDetailsService 인터페이스를 연결한다.
    4. Security Configuration(우리의 configuration)에 Database Auth를 정의한다.

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

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

• UserDetails 클래스는 Spring Security에서 User Entity 역할
Spring Security에서 사용자의 정보를 담는 인터페이스이다.

• UserDetailService 클래스는 Spring Security에서 UserRepository 역할
Spring Security에서 사용자의 정보를 가져오는 인터페이스이다.
메소드 : loadUserByUsername
리턴 타입 : UserDetails
설명 : 유저의 정보를 불러와서 UserDetails로 리턴한다.

3. 유저 엔티티(User Entity) 생성

필요한 클래스는 아래와 같다.

먼저 User Entity를 생성한다.

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<>();
    }
}

• @Entity는 User 클래스가 DB에 있는 User 테이블에 상응하는 클래스임을 표시하는 주석이다.

• @Id 밑에 오는 id가 User 테이블의 키임을 표시하는 주석이다.

• @Getter, @Setter는 lombok의 getter와 setter를 생성해주는 주석이다.

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

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

4. User Repository

UserRepotisory

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

username으로 User 객체를 찾아 반환하도록 선언함
JpaRepository는 테이블에 대응하는 클래스와 테이블의 키 타입을 알려주면 기본적인 CRUD 기능을 수행해준다.

5. Integrate with Spring Security

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

Spring Security에 연결하기 위해선 UserDetails(interface)와 UserDetailsService(interface)를 구현해야 한다.

5-1) UserPrincipal

UserPrincipal(custom)로 UserDetails(interface)을 구현한다.

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 정보를 전달한다.

Spring Security에서 사용자의 정보를 불러오기 위해서 구현해야 하는 인터페이스로 기본 오버라이드 메서드들은 아래와 같다.

5-2) UserPrincipalDetailService

UserPrincipalDetailService(custom)로 UserDetailsService(interface)를 구현한다.

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();
    }
}

localhost8080/api/public/users로 접속하면 전제 user 정보를 반환한다.

7. SecurityConfiguration

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("/management/**").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. 실행화면


localhost8080/api/public/users로 접속하면 로그인 화면이 뜬다.

admin으로 접속해주면 user정보가 반환된다.

Reference

https://minholee93.tistory.com/entry/Spring-Security-Database-Authentication-Spring-Boot-6?category=924032
https://employee.tistory.com/entry/Spring-Boot-Security-DB%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B8%EC%A6%9D

profile
배운 걸 기록하는 곳입니다.

0개의 댓글