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
'Spring > Security' 카테고리의 다른 글
Spring Security + OAuth2 + JWT 를 사용한 로그인, 로그아웃 구현 - 3 (0) | 2024.08.13 |
---|---|
Spring Security + OAuth2 + JWT 를 사용한 로그인, 로그아웃 구현 - 2 (0) | 2024.08.13 |
Spring Security + OAuth2 + Session 를 사용한 로그인, 로그아웃 구현 - google (0) | 2024.08.08 |
Spring Security + jwt 토큰 (with 쿠키) :: http-only 설정방법 (0) | 2024.08.02 |
Spring : JWT를 왜 사용해야 할까? (0) | 2024.07.27 |