Spring Security - 다중 로그인(회원, 관리자) 구현

조제·2024년 7월 4일
0

스프링 시큐리티를 이용한 다중 테이블 인증 및 JWT 토큰 발급 구현

소개

이 포스트에서는 스프링 시큐리티를 사용하여 회원과 관리자 두 개의 테이블에서 인증을 수행하고, JWT 토큰을 발급하는 방법에 대해 알아보겠습니다.

사용 버전

  • 스프링 부트: 2.6.1
  • spring-security-core: 5.6.0
  • jwt: 0.11.2

구현 개요

  1. 커스텀 Authentication 토큰 생성
  2. UserDetailsService 구현
  3. AuthenticationProvider 구현
  4. SecurityConfig 설정
  5. JWT 토큰 생성 및 검증

커스텀 Authentication 토큰 생성

회원 커스텀 토큰 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;
    }
}

UserDetailsService 구현

회원 정보를 가져오는 서비스

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

AuthenticationProvider 구현

AuthenticationProvider 선택 로직

스프링 시큐리티는 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);
    }

}

SecurityConfig 설정

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

JWT 토큰 생성 및 검증

@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;
    }
}
  1. 회원과 관리자에 대한 인증 로직을 명확하게 분리할 수 있습니다.
  2. 커스텀 Authentication 토큰을 사용하여 각 사용자 유형에 맞는 인증 프로세스를 구현할 수 있습니다.
  3. JWT를 사용하여 stateless한 인증 방식을 구현할 수 있습니다.
  4. Spring Security의 기본 구조를 활용하면서도 커스터마이징이 가능합니다.
profile
조제

0개의 댓글