Spring Security가 자체적으로 세션 관리 기능을 지원하기는 하지만, 이는 어디까지나 자체적인 기능이지 Spring 서버와 연결된 DB의 정보와는 무관한 기능이다. 따라서 Spring Security를 제대로 이용하기 위해선 우선 Spring Security와 DB의 회원 정보를 연결하는 과정이 필요하다.
우선은 Spring Security와 연결할 DB, 그리고 DB와 연결된 Repository가 필요하다. 이 글에서는 H2 DB에서 아래와 같이 정의된 테이블과, 해당 테이블을 JDBC Template을 이용해 관리하는 Repository가 있다고 가정하겠다.
create table user
(
id bigint generated by default as identity,
username varchar(255) not null,
password varchar(255) not null,
role varchar(255) not null,
authority varchar(255) not null,
primary key(id)
);
Spring Security에서는 사용자 정보를 UserDetails
라는 Wrapper 클래스로 묶어서 관리한다. 이 UserDetails
는 Spring Security에서 interface로 구현되어 있으므로, UserDetails
를 상속받아 일부 기능을 구현한 클래스를 새로 만들어야 한다.
UserDetails
에서 구현해야 하는 기능은 크게 세 가지로 나뉘는데, 사용자의 username / password의 getter, 사용자의 권한 getter, 그리고 사용자 계정의 사용 가능 여부이다. 사용자의 username / password getter는 미리 구현한 User 도메인의 getter를 사용하면 되고, 사용자 계정의 사용 가능 여부는 단순히 true를 반환하기만 해도 간단한 구현에는 문제가 없다. 중요한 건 사용자의 권한 getter인데, 이 getter는 이후 사용자의 인가 과정에서 매우 중요하게 사용된다. 일반적으로는 사용자의 권한을 SimpleGrantedAuthority
클래스에 담아 List에 담아 반환하면 되는데, Spring Security에서는 사용자의 권한을 일반적인 권한 Authority
와 특수한 권한 Role
두 가지로 분류하므로 이 둘을 잘 구분해서 추가해야 한다.
package themion7.spring_security_jwt.security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AllArgsConstructor;
// 직접 구현한 User 클래스
import themion7.spring_security_jwt.domain.User;
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
// 일반적인 권한은 그냥 SimpleGrantedAuthority로 만들어 추가
authorities.add(new SimpleGrantedAuthority(user.getAuthority()));
// 특수한 권한인 Role은 추가할 때 권한 앞에 문자열 "ROLE_"을 더해 Role임을 명시해야 함
authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
return authorities;
}
@Override
public String getPassword() {
return this.user.getPassword();
}
@Override
public String getUsername() {
return this.user.getUsername();
}
// 사용자 계정의 사용 가능 여부는 단순히 true를 반환하기만 해도 된다
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails
의 구현을 마쳤다면, 다음은 UserDetails
를 활용하는 interface인 UserDetailsService
역시 구현해야 한다. UserDetailsService
에서 구현해야 하는 기능은 String 타입인 username을 가진 사용자를 찾아 직접 구현한 UserDetails
클래스 형태로 반환하는 기능이다.
package themion7.spring_security_jwt.security;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import lombok.AllArgsConstructor;
// 직접 구현한 User 클래스
import themion7.spring_security_jwt.domain.User;
// DB에서 사용자 정보를 가져오는 Repository 클래스
import themion7.spring_security_jwt.repository.UserRepository;
@Component
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository repo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.repo.findByUsername(username).orElseThrow(() -> {
throw new UsernameNotFoundException("Username " + username + " not found");
});
return new UserDetailsImpl(user);
}
}
UserDetails
와 UserDetailsService
를 구현했다면 다음으로는 비밀번호를 암호화하는 클래스인 PasswordEncoder
를 Component로 등록해야 한다. PasswordEncoder
는 위의 두 interface와는 달리 모든 기능을 구현한 클래스가 이미 Spring Security 안에 존재하므로, 별다른 기능을 구현할 필요 없이 기존에 구현된 PasswordEncoder
를 상속받은 클래스를 새로 만들기만 한 다음 Component로 등록하면 된다. 이 글에서는 BCryptPasswordEncoder
를 사용하겠다.
package themion7.spring_security_jwt.security;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class PasswordEncoder extends BCryptPasswordEncoder {
}
package themion7.spring_security_jwt.repository;
// ... 기존 import들 ...
import themion7.spring_security_jwt.security.PasswordEncoder;
@Transactional
public class UserRepositoryImpl implements UserRepository {
// ... 기존 코드들 ...
// 비밀번호를 암호화하기 위해 Repository의 멤버 변수로 선언
private final PasswordEncoder encoder = new PasswordEncoder();
// ... 기존 코드들 ...
@Override
public User save(User user) {
// 사용자를 저장하기 전 사용자의 비밀번호를 PasswordEncoder를 이용해 암호화
user.setPassword(encoder.encode(user.getPassword()));
// ... 기존 코드들 ...
}
// ... 기존 코드들 ...
}
PasswordEncoder
까지 구현했다면, 이제 작성한 코드를 Spring Security에 적용하기 위한 설정 클래스를 작성해야 한다. 일반적인 Spring의 설정 클래스와는 다르게, Spring Security의 설정 클래스는 WebSecurityConfigurerAdapter
를 상속받아야 하고, Annotation으로 @Configuration
외에 @EnableWebSecurity
역시 사용해야 한다.
이 글에서 설정 클래스에 작성할 코드는, 위에서 Component로 만든 UserDetailsService와 PasswordEncoder를 가진 AuthenticationProvider를 Bean으로 등록한 뒤 해당 AuthenticationProvider를 인증 절차에 삽입하는 코드이다.
package themion7.spring_security_jwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import lombok.AllArgsConstructor;
import themion7.spring_security_jwt.security.PasswordEncoder;
import themion7.spring_security_jwt.security.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// AuthenticationProvider에 등록할 UserDetailsService
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth
// 인증 절차에 Bean으로 등록한 AuthenticationProvider를 삽입
.authenticationProvider(authenticationProvider());
}
@Bean
DaoAuthenticationProvider authenticationProvider() {
// 인증 절차에 삽입할 AuthenticationProvider
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 직접 작성한 UserDetailsService와 PasswordEncoder를 등록
provider.setUserDetailsService(this.userDetailsService);
provider.setPasswordEncoder(new PasswordEncoder());
// AuthenticationProvider를 반환
return provider;
}
}
위 과정을 모두 거치게 되면 Spring Security에서 기존 DB에 등록한 User를 이용해 인증 과정을 진행할 수 있다.
'웹 > Spring' 카테고리의 다른 글
Spring Security에서 JWT를 이용해 인증 토큰 발행하기 (0) | 2022.04.13 |
---|---|
Spring Security에서 사용자 인가하기 (0) | 2022.04.12 |
Spring 프로젝트에서 Spring Security 사용하기 (0) | 2022.04.12 |
Spring 프로젝트에서 HTTPS 사용하기 (0) | 2022.04.10 |
Spring 프로젝트에 Lombok 추가하기 (0) | 2022.04.09 |