이전까지는 모두 계정을 인메모리에서 사용했지만, 실무에서는 DB에서 계정 정보를 가져온다. 따라서 실제로 스프링 시큐리티를 써서 서비스를 만든다고 하면, 개발자들은 UserDetails를 구현한 User 객체와 UserDeatilsService부터 만든다. 왜냐하면, UserDetailsService와 UserDeatils 구현체만 구현하면 스프링 시큐리티가 나머지는 쉽게 쓸 수 있도록 대부분 설정해주기 때문이다. 추후 Student를 UserDetails 구현체로, StudentManager를 UserDetailsService 구현체로 개선한다.
(UsernamePasswordAuthenticationToken => UsernamePasswordAuthenticationFilter 오타수정)
UsernamePasswordAuthenticationFilter에서 UsernamePasswordAuthenticationToken을 ProviderManager에게 넘겨준다. DaoAuthenticationProvider는 해당 토큰을 처리해준다. AuthenticationProvider가 UserDetailsService 구현체에 인증토큰을 넘기고 UserDetails 구현체에 principal을 리턴받게 된다. 인증이 된 사용자는 그 정보가 UsernamePasswordAuthenticaionToken에 담겨서 리턴된다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name ="sp_user_authority")
@IdClass(SpAuthority.class)
public class SpAuthority implements GrantedAuthority {
@Id
@Column(name="user_id")
private Long userId;
@Id
private String authority;
}
UserDetails 구현체를 반환할 때 권한은 GrantedAuthority를 리턴타입으로 가져야 한다. 따라서 아예 GrantedAuthority를 구현하는 엔티티를 만들어서 UserDetails 구현체의 권한 칼럼과 연관관계를 가진다. (혹은 권한 클래스가 GrantedAuthority를 구현하지 않아도 UserDetails 객체를 만들 때 , GrantedAuthority 타입으로 변환시켜주면 된다.)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name="sp_user")
public class SpUser implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "user_id", foreignKey = @ForeignKey(name="user_id"))
private Set<SpAuthority> authorities;
private String email;
private String password;
private boolean enabled;
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return enabled;
}
@Override
public boolean isAccountNonLocked() {
return enabled;
}
@Override
public boolean isCredentialsNonExpired() {
return enabled;
}
}
UserDeatils를 구현하는 SpUser 엔티티를 만들었으며, 여러개의 authorities 권한을 가질 수 있다.
public interface SpUserRepository extends JpaRepository<SpUser, Long> {
Optional<SpUser> findUserByEmail(String email);
}
JPA를 이용한 저장소를 사용해서 계정을 조회하도록 한다.
@Service
@Transactional
public class SpUserService implements UserDetailsService {
private final SpUserRepository spUserRepository;
public SpUserService(SpUserRepository spUserRepository) {
this.spUserRepository = spUserRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return spUserRepository.findUserByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
}
public Optional<SpUser> findUser(String email) {
return spUserRepository.findUserByEmail(email);
}
public SpUser save(SpUser spUser) {
return spUserRepository.save(spUser);
}
public void addAuthority(Long userId, String authority) {
spUserRepository.findById(userId).ifPresent(user -> {
SpAuthority newRole = new SpAuthority(user.getUserId(), authority);
if(user.getAuthorities() == null) {
Set<SpAuthority> authorities = new HashSet<>();
authorities.add(newRole);
user.setAuthorities(authorities);
save(user);
} else if(!user.getAuthorities().contains(newRole)) {
HashSet<SpAuthority> authorities = new HashSet<>();
authorities.addAll(user.getAuthorities());
authorities.add(newRole);
user.setAuthorities(authorities);
save(user);
}
});
}
public void removeAuthority(Long userId, String authority){
spUserRepository.findById(userId).ifPresent(user->{
if(user.getAuthorities() == null) return;
SpAuthority targetRole = new SpAuthority(user.getUserId(), authority);
if(user.getAuthorities().contains(targetRole)){
user.setAuthorities(
user.getAuthorities().stream().filter(auth->!auth.equals(targetRole))
.collect(Collectors.toSet())
);
save(user);
}
});
}
}
기존에는 AuthenticationManager에 위임해서 처리했었지만, 이제 UserDetailsService를 구현해서 계정 생성, 권한 추가 및 삭제 기능을 만든다. 이 내용을 필터 설정의 인증설정에 넣어주면 된다.
주의할 것은 loadUserByUsername 리턴 타입은 UserDetails를 구현한 SpUser를 조회하도록 해야한다. 왜냐하면 UserDetailsService는 loadUserByUsername()라는 하나의 추상메서드를 가지고 있고 리턴 타입이 UserDeatils 이기 때문이다. 시큐리티는 단순히 계정 조회뿐만 아니라 권한도 함께 고려하는 것을 잊지 않는다.
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final SpUserService spUserService;
public SecurityConfig(SpUserService spUserService) {
this.spUserService = spUserService;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(spUserService);
}
인메모리방식으로 구현했던 방식을 UserDetailsService를 구현하는 spUserService방식으로 바꾼다. 실제 DB와 연동하여 계정을 사용할 수 있다.
'Spring > Spring Security' 카테고리의 다른 글
세션관리 ( 동시접속 ) (0) | 2021.10.14 |
---|---|
RememberMe (0) | 2021.10.12 |
BaiscToken으로 웹/모바일 로그인 인증 구현하기 (0) | 2021.10.11 |
BasicAuthentication 인증 (0) | 2021.10.11 |
authentication 메커니즘 (0) | 2021.10.08 |