[프로젝트] 어드민 서비스 구현하기 1탄 어드민 생성 및 권한 부여

조찬영·2023년 9월 14일
0

들어가기 앞서


프로젝트에 다양한 기능들이 추가됨에 따라 이제 서비스를 관리할 수 있는 어드민 페이지를 만들어야 할 것 같다는 생각이 있었는데요.

사실 구현해보고 서비스 기능들이 여러 있었지만 어드민 기능은 일반적인 서비스 기능과는 조금은 다른 권한(authorization)이라는 성격을 지니고 있기에 흥미를 느낀 부분들이 있었고 구현을 해보면 많은 도움이 될 것 같아 개발해보기로 결정했습니다.



어떤 기능을 만들 수 있을까?


현재 프로젝트 규모에 따라 구현하기 적합한 기능들을 떠올려봤고
다음의 기능들을 우선적으로 구현해 볼 수 있을 것 같았습니다.

Admin Service -> 어드민 계정 생성,유저 관리, 컨텐츠 관리, 신고 처리

구현을 하기 위해서는 가장 뼈대가 되는 어드민 계정을 생성해야하므로
어드민 계정을 생성하는 로직을 먼저 만들어 보겠습니다.

1. 어드민 계정 생성하기


 1-1. 어드민은 어떻게 로그인하지?


첫번째 고민이 되었던 지점이였고 적절한 방법들을 생각하고 정리를 해보았습니다.

1. 일반 User를 Admin으로 승격시키기

2. 유니크한 Key 값을 적용시켜 회원가입에서 어드민을 구분하기

3. 데이터베이스에 직접 접근하여 계정 생성하기

4. 서버측 코드에서 관리자 계정 직접 생성

저는 3,4번의 방법을 적절히 활용하는게 좋을 것 같다는 결정을 했습니다.

1번의 방법은 기존의 로그인 체제를 유지하며 비용을 절약할 수 있는등 합리적인 방법이라고 생각했지만 계정 정보를 프로젝트 내에서 관리하여 보안적인 면을 강화해보고 싶었습니다.

2번의 방법은 key 값이 노출/탈취 되었을 때 여파가 너무나 클 것 같았습니다.

3번의 방법은 직관적인 방법이며 편하다고 생각되었습니다.
하지만 생성시마다 DB에 접근하기에는 개발자의 실수에 큰 리스크가 따라 왔기 때문에 DB에 직접적인 접근 횟수를 줄여야 한다고 생각했고

그래서 3,4 번 방법을 적절히 활용하여
기본적으로 서버의 코드로서 계정을 생성하며 특수한 상황(서버의 에러 발생) 에서
DB에 접근하는게 안전한 방법
이라고 생각했습니다.



1-2. UserRole 권한 분리


먼저 유저의 권한 정보를 알 수 있는 UserRole Enum 클래스를 생성하고 UserEntity에 컬럼으로 추가 해주었습니다.

UserRole

@Getter
@AllArgsConstructor
public enum UserRole {

    USER("ROLE_USER", "일반 사용자"),
    ADMIN("ROLE_ADMIN", "관리자 계정");

    private final String role;
    private final String description;
}

UserEntity

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "\"user\"")
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long Id;

    ...(생략)

    @Enumerated(EnumType.STRING)
    private UserRole role = UserRole.USER;

    ...(생략)

1-3. 어드민 계정 생성하기

이후 어드민 계정을 생성해주는 AdminCreationService 클래스를 생성해 주었습니다.

AdminCreationService

import static com.sns.yourconnection.utils.AdminAccountUtil.*;

@Service
@RequiredArgsConstructor
public class AdminCreationService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;
    private final JavaMailSender javaMailSender;

    @Transactional
    public void createAccount(String email) {
        String adminName = createAdminName();
        String password = createAdminPassword();

        UserEntity userEntity = UserEntity.of(
            adminName, encoder.encode(password), "admin", email);
        userEntity.toAdmin();
        userRepository.save(userEntity);

        javaMailSender.send(createEmailForm(email, adminName, password));
    }
}

AdminCreationService 의 어드민 계정 생성 부분은 프로젝트와 서비스의 성격에 따라 언제든지 달라질 수 있는 부분이며 저는 Admin 계정의 보안 부분에 초점을 맞춰 코드로 구현하였습니다.

  • AdminAccountUtil 유틸클래스를 통해 계정의 정보를 생성합니다.
  • BCryptPasswordEncoder 통해 생성된 password를 암호화합니다.
  • 생성된 admin 계정의 정보를 프로젝트 이메일 또는 팀원의 이메일로 발송합니다.
  • 발송된 메일을 통하여 admin 계정의 정보를 확인하고 로그인할 수 있습니다.

Admin 계정을 생성하는 유틸 클래스인 AdminAccountUtil 를 살펴보겠습니다.


AdminAccountUtil

@RequiredArgsConstructor
public class AdminAccountUtil {

    private static final String ADMIN_PREFIX = "Admin";
    private static final String TITLE_MESSAGE = "[YourConnection] Don't expose your password";
    private static final int SECURITY_DIGIT_NUMBER = 6;
    private static final int RANDOM_NUMBER_BOUND = 10;

    public static String createAdminName() {
        return ADMIN_PREFIX + UUID.randomUUID();
    }

    public static String createAdminPassword() {
        return UUID.randomUUID() + createRandomCodeNumber();
    }

    private static String createRandomCodeNumber() {
        try {
            Random random = SecureRandom.getInstanceStrong();
            return createRandomNumber(random);
        } catch (NoSuchAlgorithmException e) {
            throw new AppException(ErrorCode.NO_SUCH_ALGORITHM);
        }
    }

    private static String createRandomNumber(Random random) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < SECURITY_DIGIT_NUMBER; i++) {
            builder.append(random.nextInt(RANDOM_NUMBER_BOUND));
        }
        return builder.toString();
    }

    public static SimpleMailMessage createEmailForm(String toEmail, String adminName,
        String password) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(toEmail);
        message.setSubject(TITLE_MESSAGE);
        message.setText(getText(adminName, password));
        return message;
    }

    private static String getText(String adminName, String password) {
        return String.format("[Admin Account : %s], [Password : %s] ", adminName, password);
    }
}

  • UUID와 상수 선언된 prefix를 조합하여 Admin name 을 생성합니다.
  • UUID 와 createRandomCodeNumber() 메서드에 의해 생성된 랜덤 난수를 조합하여 password 를 발급합니다.
  • 계정정보를 이메일로 발송하기 위한 Email form 을 생성합니다.

(이메일을 보내기 위한 properties 값들은 생략하겠습니다.)

메일을 통해 어드민 계정 확인

전달되는 form은 수정이 필요하겠지만 우선적으로 의도대로 잘 전달된 것을 확인할 수 있습니다.



2. Admin 권한 부여 하기


어드민 계정에 권한을 부여하는 방법에는 여러 방법이 있지만,
저 같은 경우에는 spring security 를 활용하여 권한을 부여했습니다.

SecurityConfig

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationEntryPoint entryPoint;
    private final JwtTokenFilter jwtTokenFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .httpBasic().disable()
            .csrf().disable()
            .cors().and()
            .authorizeRequests(request ->
                request.antMatchers("/public-api/**").permitAll()
                    .antMatchers("/api/admin/**").hasRole("ADMIN")
                    .antMatchers("/api/**").authenticated())
            .sessionManagement(sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(handler ->
                handler.authenticationEntryPoint(entryPoint))
            .build();
    }
}

.antMatchers("/api/admin/**").hasRole("ADMIN") 의 설정 값을 통해
지정된 api 호출에는 ADMIN 권한을 가진 계정만 접근할 수 있도록 설정하였습니다.

UserDatail

 @Getter
@AllArgsConstructor
public class User implements UserDetails {
  
    private Long id;
    
    private UserRole role;
    
    ...(생략)

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(this.role.getRole()));
    }
  }
  ...(생략)

그리고 권한 체크를 할 수 있도록 UserDetails 를 상속받는 클래스의 getAuthorities()에 아까 만들어 두었던 UserRole enum 을 통해 권한에 대한 정보를 나타냈습니다.


이로서 어드민에 대한 계정 생성과 권한 부여를 끝마쳤습니다.

이제 어드민 서비스 구현하기 2탄에서 본격적인 서비스를 구현해 보겠습니다!
전체의 코드는 [깃 허브 링크]에서 보실수 있습니다.

profile
보안/응용 소프트웨어 개발자

0개의 댓글