이번 글에서는 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
추천서적
파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음
'Spring > Security' 카테고리의 다른 글
[Spring Security] Customize Form Control Names - Spring Boot (8) (0) | 2020.09.08 |
---|---|
[Spring Security] Form Authentication - Spring Boot (7) (0) | 2020.09.08 |
[Spring Security] Enable SSL/HTTPS - Spring Boot (5) (0) | 2020.09.08 |
[Spring Security] Authority Based Authorization - Spring Boot (4) (0) | 2020.09.08 |
[Spring Security] Role Based Authorization - Spring Boot (3) (0) | 2020.09.08 |