[Spring & Spriboot] Spring Security를 이용하여 회원 로그인 , 회원가입 서비스 만들기
|2024. 9. 27. 19:04
스프링 시큐리티
스프링 기반 애플리케이션 보안(인증, 인가,권한)을 담당하는 스프링 하위 프레임워크
보안 관련 옵션 제공 (CSRF 공격, 세션 고정 공격 방어)
- CSRF 공격 : 사용자 권한을 갖고 특정 동작을 수행하도록 유도하는 공격
- 세션 고정 공격 : 사용자의 인증 정보를 탈취하거나 변조하는 공격
필터 기반 동작
- UsernamePasswordAuthentication : ID, PW 넘어오면 인증 요청을 위임하는 인증 관리자
- FilterSecurityInterceptor : 권한 부여 처리를 위임해 접근 제어 결정을 쉽게하는 접근 결정 관리자
- UserDetails : 스프링 시큐리티에서 사용자의 인증 정보 담아두는 인터페이스
이제 실습을 해보자!
의존성 추가하기 (build.gradle)
build.gradle에 스프링 시큐리티용 의존성 추가하기
implementation 'org.springframework.boot:spring-boot-starter-security' // 1) 스프링 시큐리티 사용하기 위한 스타터 추가
testImplementation 'org.springframework.security:spring-security-test' // 3) 스프링 시큐리티 테스트 위한 의존성 추가
Entity
@NoArgsConstructor
@Getter @Setter
@ToString
@Entity
@Table(name = "USER")
public class Member extends BaseTimeEntity implements UserDetails {
@Id
private String userId;
@Column(name = "PASSWORD", nullable = false)
private String password;
@Column(name = "NICKNAME", nullable = false)
private String nickName;
@Column(name = "BIRTHDAY", nullable = false)
private LocalDate birthday;
@Column(name = "EMAIL", nullable = false, unique = true)
private String email;
@Enumerated(EnumType.STRING)
@Column(name = "GENDER", nullable = false)
private Gender gender;
@Enumerated(EnumType.STRING)
@Column(name = "ROLE", nullable = false)
private Role role;
@Builder
private Member(String userId, String password, String nickName, String email, LocalDate birthday, Gender gender) {
this.userId = userId;
this.password = password;
this.nickName = nickName;
this.email = email;
this.birthday = birthday;
this.gender = gender;
this.role = Role.USER; // 기본 값
}
// 사용자의 권한 목록을 반환(사용자 인지 관리자 인지)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.getKey()));
}
// 사용자 id 반환 (고유값)
@Override
public String getUsername() {
return this.userId;
}
// 사용자의 계정이 만료되었는지 여부
@Override
public boolean isAccountNonExpired() {
return true; // 계정이 만료되지 않았음을 의미
}
// 계정이 잠겼는지 여부
@Override
public boolean isAccountNonLocked() {
return true; // 잠금 X
}
//비밀번호 만료 여부
@Override
public boolean isCredentialsNonExpired() {
return true; // 만료 X
}
//계정 로그인 상태 활성화 상태인지
@Override
public boolean isEnabled() {
return true; //계정이 활성화된 상태
}
}
Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByUserId (String userId);
}
DTO
@Data
public class MemberSignUpDto {
private String userId;
private String password;
private String nickName;
private String email;
private LocalDate birthday;
private Gender gender;
}
Service
@RequiredArgsConstructor
@Slf4j
@Service
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
/**
* 회원가입
*
* @param dto
* @param
* @return
*/
public void registerMember(MemberSignUpDto dto) {
log.info("registerMember(dto={})" , dto);
Member member = Member.builder()
.userId(dto.getUserId())
.password(passwordEncoder.encode(dto.getPassword()))
.nickName(dto.getNickName())
.email(dto.getEmail())
.birthday(dto.getBirthday())
.gender(dto.getGender())
.build();
memberRepository.save(member);
return dto.getUserId(); // 회원가입 ID 반환
}
Controller
@PostMapping("/signUp")
public ResponseEntity<String> signUp(@RequestBody MemberSignUpDto dto) {
log.info("singUP() POST");
log.info("dto: {}", dto);
memberService.registerMember(dto);
return ResponseEntity.ok("회원가입 성공");
}
하지만 우리 다시 한번 회원가입을 할 때 생각을 해보자
아이디는 하나의 아이디만 존재해야 한다. 즉 같은 아이디가 있을 수 없는 고유의 값!
그러면 아이디의 중복 제거하는 기능을 만들어 보자
아이디 중복 제거
Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member>findByUserId (String userId);
boolean existsByUserId(String userId); // 추가
}
뜬금 없지만
Member findByUserId (String userId);
사용을 했지만 Optional로 바꿔주면 null 값을 직접 처리하지 않고, 값이 있는지 없는지에 대한 명확한 처리하기에 좀 더 코드가 깔끔해지는거 같아 바꿨다.
그 다음 exception패키지 -> DuplicateMemberldException 클래스를 만들어준 다음
public class DuplicateMemberIdException extends RuntimeException{
public DuplicateMemberIdException(String message) {
super(message);
}
}
해당 코드 복사하여 붙여넣기! 회원가입 시 중복된 아이디가 발생할 때 특정한 예외 상황을 처리하기 위해 작성
그 다음 MemberService에 registerMember메서드에 해당 코드를 추가해준다.
// 중복 아이디 인 경우 !
if (memberRepository.existsByUserId(dto.getUserId())) {
throw new DuplicateMemberIdException("중복된 아이디입니다.");
}
그 다음 그냥 MemberService 해당 메서드를 추가
/**
* 회원 아이디가 있는지 없는지 확인하는 메서드
* @param userId
* @return
*/
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
log.info("logUserByUsername(username={})", userId);
// DB에서 userId로 사용자 정보 검색 (select)
return memberRepository.findByUserId(userId).orElseThrow(() -> new UsernameNotFoundException("회원 정보를 찾을 수 없습니다. 아이디: " + userId));
}
전체적인 Service 코드
@RequiredArgsConstructor
@Slf4j
@Service
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final ConcurrentHashMap<String, MemberSignUpDto> pendingVerifications = new ConcurrentHashMap<>();
/**
* 회원가입
* 중복 아이디 확인
* 가입 후 이메일 인증 시도
*
* @param dto
* @param
*/
public void registerMember(MemberSignUpDto dto) {
log.info("registerMember(dto={})" , dto);
// 중복 아이디 인 경우 !
if (memberRepository.existsByUserId(dto.getUserId())) {
throw new DuplicateMemberIdException("중복된 아이디입니다. 변경해주세요");
}
Member member = Member.builder()
.userId(dto.getUserId())
.password(passwordEncoder.encode(dto.getPassword()))
.nickName(dto.getNickName())
.email(dto.getEmail())
.birthday(dto.getBirthday())
.gender(dto.getGender())
.build();
memberRepository.save(member);
}
/**
* 회원 아이디가 있는지 없는지 확인하는 메서드
* @param userId
* @return
*/
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
log.info("logUserByUsername(username={})", userId);
// DB에서 userId로 사용자 정보 검색 (select)
return memberRepository.findByUserId(userId)
.orElseThrow(() -> new UsernameNotFoundException("회원 정보를 찾을 수 없습니다. 아이디: " + userId));
}
}
Controller
@PostMapping("/signUp")
public ResponseEntity<String> signUp(@RequestBody MemberSignUpDto dto) {
log.info("singUP() POST");
log.info("dto: {}", dto);
try {
memberService.registerMember(dto);
return ResponseEntity.ok("회원가입 성공");
} catch (DuplicateMemberIdException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("중복된 아이디입니다.");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("에러");
}
}
그럼 한번 Postman을 통해 아이디 중복을 잡는지 확인해보자!
user113아이디 회원이 있고 다른 클라이언트가 user113으로 회원가입을 시도하게 되면
중복된 아이디 값을 확인 할 수 있다.
'트러블슈팅 > SpringBoot' 카테고리의 다른 글
[Springboot & JSP]JPA 환경설정 (1) | 2024.10.12 |
---|---|
[Spring & Spriboot] Spring Security를 이용하여 이메일 인증 서비스 만들기 (2) | 2024.09.27 |
[Spring & SpringBoot] 이미지 저장 방법 (0) | 2024.09.22 |