이 포스트에서는 스프링 시큐리티를 사용하여 회원과 관리자 두 개의 테이블에서 인증을 수행하고, JWT 토큰을 발급하는 방법에 대해 알아보겠습니다.
회원 커스텀 토큰 CustomUserAuthenticationToken 생성
public class CustomUserAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public CustomUserAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public CustomUserAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
}
관리자 커스텀 토큰 CustomManagerAuthenticationToken 생성
public class CustomManagerAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public CustomManagerAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public CustomManagerAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
}
회원 정보를 가져오는 서비스
@Service
@RequiredArgsConstructor
public class CustomUserService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUserIdAndWithdrawYn(username, "N")
.map(this::createUserDetails)
.orElseThrow(() -> new UsernameNotFoundException(username + " -> Not Found."));
}
private UserDetails createUserDetails(UserInfoEntity userInfoEntity) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority(RoleType.ROLE_USER.toString()));
return new User(
String.valueOf(userEntity.getUserId()),
"password",
grantedAuthorities
);
}
}
관리자 정보를 가져오는 서비스
@Service
@RequiredArgsConstructor
public class CustomManagerService implements UserDetailsService {
private final HospitalManagerRepository hospitalManagerRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return hospitalManagerRepository.findByManagerIdAndStatusNot(username, ManagerStatus.WITHDRAW)
.map(this::createUserDetails)
.orElseThrow(() -> new BadCredentialsException(username + " -> Not Found."));
}
private UserCustom createUserDetails(HospitalManagerEntity manager) {
ManagerStatus status = manager.getStatus();
if (ManagerStatus.WAIT.equals(status) || ManagerStatus.DENIED.equals(status)) {
throw new BadCredentialsException(manager.getManagerId() + " -> Not Found.");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority(RoleType.ROLE_MANAGER.toString()));
return new UserCustom(
manager,
grantedAuthorities
);
}
}
스프링 시큐리티는 AuthenticationManager가 인증 요청을 받을 때 각 AuthenticationProvider를 순차적으로 조회하여
supports 메소드를 이용해 해당 AuthenticationProvider가 해당 요청을 처리할 수 있는지 확인합니다.
이 때 supports 메소드가 true를 반환하면 해당 AuthenticationProvider가 요청을 처리하게 됩니다.
이렇게 함으로써 각 로그인 요청이 적절한 AuthenticationProvider에 의해 처리되도록 보장할 수 있습니다.
회원 인증을 위한 Provider:
@Component
public class CustomUserAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CustomUserService customUserService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
UserDetails userDetails = customUserService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("Bad credentials");
}
return new CustomUserAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> aClass) {
return CustomUserAuthenticationToken.class.isAssignableFrom(aClass);
}
}
관리자 인증을 위한 Provider:
@Component
@RequiredArgsConstructor
public class CustomManagerAuthenticationProvider implements AuthenticationProvider {
private final CustomManagerService customManagerService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails userDetails = customManagerService.loadUserByUsername(username);
if (!bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("BadCredentialsException");
}
return new CustomManagerAuthenticationToken(userDetails,null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return CustomManagerAuthenticationToken.class.isAssignableFrom(authentication);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserAuthenticationProvider userAuthenticationProvider;
@Autowired
private CustomManagerAuthenticationProvider managerAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(userAuthenticationProvider)
.authenticationProvider(managerAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
@Service
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
// 토큰 검증 메소드
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty");
}
return false;
}
}