스프링 시큐리티 

스프링 기반 애플리케이션 보안(인증, 인가,권한)을 담당하는 스프링 하위 프레임워크

 

보안 관련 옵션 제공 (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으로 회원가입을 시도하게 되면

중복된 아이디 값을 확인 할 수 있다.