728x90

 

개요

이번에는 인증/인가 총 정리다. 

OAuth2 로그인과 JWT를 활용한 보안 방식을 정리할 예정이다.

다른 포스터에 이미 올렸지만 정리를 위해 처음부터 정리할 예정

총 몇편으로 나올지 궁금하다.

 


일단 jwt 방식을 위한 기본 설정을 한다. OAuth2 설정도 같이 하면 복잡해져서 뒤에서 정리할 예정이다.

 

 

1. 엔티티 설정

  • 유저 엔티티
@Entity
@Table(name = "users")
@Getter @Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

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

    @Column(nullable = false, unique = true)
    private String username; // 이메일

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name; // 본명

    @Column(nullable = false)
    private String usernick; // 닉네임

    @Column(nullable = false)
    private LocalDateTime registrationDate = LocalDateTime.now(); // 가입일

    @Column(nullable = false)
    private LocalDate birthdate; // 생일

    @Column(nullable = false)
    private String gender; // 성별

    @Column(nullable = false, length = 50)
    private String phoneNumber; // 연락처

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "user_roles",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles = new HashSet<>();

    private String provider; // oauth2 플랫폼
    private String providerId; // 플랫폼 아이디
}
  • 롤 엔티티
@Entity
@Table(name = "roles")
@Getter@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Role {

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

    @Column(nullable = false)
    private String name;

    public Role(String name) {
        this.name = name;
    }
}
  • 리프레시 토큰 엔티티
@Entity
@Table(name = "refresh_token")
@Getter@Setter
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "value", length = 512)
    private String value;

    @Column(name = "user_id")
    private Long userId;

    @Column(name = "expiration_time")
    private String expiration;
}
  • 로그아웃 토큰 엔티티
@Entity
@Table(name = "jwt_blacklist")
@Getter@Setter
@NoArgsConstructor
public class JwtBlacklist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "token", length = 512)
    private String token;

    @Column(name = "expiration_time")
    private Date expiration;

    public JwtBlacklist(String token, Date expiration) {
        this.token = token;
        this.expiration = expiration;
    }
}

 

 

2. 레포지토리 설정

  • 유저 레포지토리
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByUsername(String username); // 아이디 찾기

    boolean existsByUsernick(String userName); // 닉네임 중복 확인
    
    boolean existsByUsername(String email); // 이메일(아이디) 중복 확인
}
  • 롤 레포지토리
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {

    Optional<Role> findByName(String name);
}
  • 리프레시 토큰 레포지토리
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

    Optional<RefreshToken> findByValue(String value);

    boolean existsByValue(String token);

    Optional<RefreshToken> findByUserId(Long userId);

    @Transactional
    void deleteByValue(String refresh);
}
  • 로그아웃 토큰 레포지토리
public interface JwtBlacklistRepository extends JpaRepository<JwtBlacklist, Long> {
    boolean existsByToken(String token);
}

 

 

 

 

 

3. DTO 설정

  • 회원가입 DTO
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserSingUpDto {

    private Long id;

    //@NotBlank(message = "아이디는 필수 입력 값입니다.")
    private String username; //이메일 ==> id
    private String name; // 사용자 이름

    //@NotBlank(message = "비밀번호는 필수 입력 값입니다.")
    //@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}", message = "비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
    private String password;
    private String passwordCheck;

    //@Pattern(regexp = "^[ㄱ-ㅎ가-힣a-z0-9-_]{2,10}$", message = "닉네임은 특수문자를 제외한 2~10자리여야 합니다.")
    private String usernick;
    private LocalDate birthdate; // 생년월일
    private String gender;
    private String phoneNumber;
}
  • 유저 정보 수정 DTO
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserEditDto {

    private String usernick; // 닉네임
    private LocalDate birthdate; // 생년월일
    private String gender; // 성별
    private String phoneNumber; // 연락처
}
  • 유저 로그인 DTO
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginDto {
    @NotEmpty
    private String username;

    @NotEmpty
    private String password;
}
  • 로그인 응답 DTO
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginResponseDto {
    private String accessToken;
    private String refreshToken;
    private Long userId; // private Long userId;
    private String username;
}

 

 

4. 서비스 설정

Service Layer Pattern 을 사용했다.
Service 계층의 인터페이스와 비즈니스 로직을 분리했다.

  • 유저서비스
@Service
public interface UserService {

    Optional<User> findById(Long id); // 유저 아이디 찾기

    void signUp(UserSingUpDto userSingUpDto); // 회원가입

    Optional<User> findByUsername(String email); // 아이디 찾기

    void deleteUser(String username); // 유저 삭제

    boolean existsByUsername(String username); // 아이디 중복 확인

    boolean existsByUsernick(String usernick); // 닉네임 중복 확인

    Optional<User> updateUser(String username, UserEditDto userEditDto); // 유저 정보 업데이트
}
  • 유저서비스 impl
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final RoleRepository roleRepository;

    @Override
    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }

    @Override
    public Optional<User> findByUsername(String username) {
        return userRepository.findByUsername(username);
    }

    @Override
    public void signUp(UserSingUpDto userSingUpDto) {
        if (!userSingUpDto.getPassword().equals(userSingUpDto.getPasswordCheck())) {
            throw new RuntimeException("비밀번호가 다릅니다.");
        }
        if (userRepository.existsByUsername(userSingUpDto.getUsername())) {
            throw new RuntimeException("이메일이 존재합니다.");
        }
        if (userRepository.existsByUsernick(userSingUpDto.getUsernick())) {
            throw new RuntimeException("닉네임이 존재합니다.");
        }

        Role role = roleRepository.findByName("ROLE_USER")
                    .orElseThrow(() -> new RuntimeException("User 역할이 없습니다."));

        User user = new User();
        user.setRoles(Collections.singleton(role)); // 단일 역할
        user.setUsernick(userSingUpDto.getUsernick()); // 닉네임
        user.setUsername(userSingUpDto.getUsername()); // email
        user.setName(userSingUpDto.getName()); // 이름
        user.setPassword(passwordEncoder.encode(userSingUpDto.getPassword()));
        user.setRegistrationDate(LocalDateTime.now());
        user.setBirthdate(userSingUpDto.getBirthdate());
        user.setGender(userSingUpDto.getGender());
        user.setPhoneNumber(userSingUpDto.getPhoneNumber());

        userRepository.save(user);
    }

    @Override
    public void deleteUser(String username) {
        Optional<User> usernameOptional = userRepository.findByUsername(username);
        if (usernameOptional.isPresent()) {
            userRepository.delete(usernameOptional.get());
        } else {
            throw new RuntimeException("삭제할 사용자가 존재하지 않습니다.");
        }
    }
    
    /**
     * 사용자 페이지 수정
     */
    @Override
    public Optional<User> updateUser(String username, UserEditDto userEditDto) {

        Optional<User> userOptional = userRepository.findByUsername(username);
        if (userOptional.isEmpty()) {
            log.error("사용자 없습니다. :: {}", username);
            return Optional.empty();
        }

        User user = userOptional.get();

        user.setUsernick(userEditDto.getUsernick());
        user.setBirthdate(userEditDto.getBirthdate());
        user.setGender(userEditDto.getGender());
        user.setPhoneNumber(userEditDto.getPhoneNumber());

        User updatedUser = userRepository.save(user);
        return Optional.of(updatedUser);
    }


    @Override
    public boolean existsByUsername(String username) {
        return userRepository.existsByUsername(username);
    }

    @Override
    public boolean existsByUsernick(String usernick) {
        return userRepository.existsByUsernick(usernick);
    }
}
  • 리프레시토큰 서비스
public interface RefreshTokenService {

    RefreshToken addRefreshToken(RefreshToken refreshToken);
    
    void deleteRefreshToken(String refreshToken);

    boolean isRefreshTokenValid(String refreshToken);
}
  • 리프레시토큰 impl
@Service
@RequiredArgsConstructor
public class RefreshTokenServiceImpl implements RefreshTokenService {
    
    private final RefreshTokenRepository refreshTokenRepository;

    @Transactional
    @Override
    public RefreshToken addRefreshToken(RefreshToken refreshToken) {
        return refreshTokenRepository.save(refreshToken);
    }

    @Override
    public void deleteRefreshToken(String refreshToken) {
        refreshTokenRepository.findByValue(refreshToken).ifPresent(refreshTokenRepository::delete);
    }

    @Override
    public boolean isRefreshTokenValid(String refreshToken) {
        return refreshTokenRepository.existsByValue(refreshToken);
    }
}
  • 로그아웃토큰 서비스
public interface JwtBlacklistService {

    void save(JwtBlacklist blacklist);

    boolean isTokenBlacklisted(String token);
}
  • 로그아웃토큰 impl
@Service
@RequiredArgsConstructor
public class JwtBlacklistServiceImpl implements JwtBlacklistService {
    
    private final JwtBlacklistRepository jwtBlacklistRepository;

    @Override
    public void save(JwtBlacklist blacklist) {
        jwtBlacklistRepository.save(blacklist);
    }

    @Override
    public boolean isTokenBlacklisted(String token) {
        return jwtBlacklistRepository.existsByToken(token);
    }
}

 

 

기본적인 틀은 이렇게 잡고 간다.

728x90

+ Recent posts