👀 스프링 시큐리티로 회원가입, 로그인, 로그아웃 구현하기
1. 의존성 추가
pom.xml 혹은 build.gradle에 의존성을 추가한다.
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
2. 회원 도메인 생성
Member.java
@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "nickname", nullable = false)
private String nickname;
@Column(name = "password", nullable = false)
private String password;
@CreatedDate
@Column(name="created_at")
private LocalDateTime createdAt;
@Enumerated(EnumType.STRING)
private Authorities authorities;
@Builder
public Member(String email, String nickname, String password, Authorities authorities) {
this.email = email;
this.nickname = nickname;
this.password = password;
this.authorities = authorities;
}
// 사용자 닉네임 변경
public Member update(String nickname) {
this.nickname = nickname;
return this;
}
// 권한 반환
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(authorities.toString()));
}
// 사용자명(이메일) 반환
@Override
public String getUsername() {
return email;
}
// 사용자 비밀번호 반환
@Override
public String getPassword() {
return password;
}
// 계정 만료여부 반환
@Override
public boolean isAccountNonExpired() {
// 만료 여부 확인 로직
return true; // true : 만료되지 않음
}
// 계정 잠금여부 반환
@Override
public boolean isAccountNonLocked() {
return true; // true : 잠금되지 않음
}
// 비밀번호 만료 여부 반환
@Override
public boolean isCredentialsNonExpired() {
return true; // true : 만료되지 않음
}
// 계정 사용가능 여부 반환
@Override
public boolean isEnabled() {
return true; // true : 사용가능
}
}
회원 정보를 담을 Member 도메인이다.
사용자의 아이디, 이메일, 비밀번호, 닉네임, 가입일시, 권한을 가진다.
UserDetails를 상속받아 관련 메서드들을 구현했다.
MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
boolean existsByEmail(String email);
}
JpaRepository를 상속받았으며, 이메일로 사용자를 반환하는 메서드와 이메일 중복확인 메서드를 추가했다.
MemberService.java
@RequiredArgsConstructor
@Service
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return memberRepository.findByEmail(username).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 email : " + username));
}
public Member findById(Long id) {
return memberRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id : " + id));
}
public Member findByEmail(String email) {
return memberRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 email : " + email));
}
public Long saveMember(MemberAddRequest request) {
if(memberRepository.existsByEmail(request.getEmail())) { // 이메일 중복확인
return null;
}
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return memberRepository.save(
Member.builder()
.email(request.getEmail())
.nickname(request.getNickname())
.password(bCryptPasswordEncoder.encode(request.getPassword()))
.authorities(Authorities.ROLE_USER)
.build()
)
.getId();
}
}
UserDetailsService를 상속받아 loadUserByUsername 메서드를 구현했고, 사용자 정보 조회, 저장 메서드를 작성했다.
❓ UserDetails와 UserDetailsService?
UserDetails는 스프링 시큐리티에서 사용자 정보를 다루는 방식을 정의한 인터페이스이고, UserDetailsService는 그 정보를 조회하는 방법을 정의한 인터페이스이다.
이를 상속받아 구현하면 스프링 시큐리티의 보안 메커니즘에 알맞는 객체로 만들기 쉽다.
반드시 상속받아 구현해야 하는 것은 아니며, 상속받지 않아도 인증 로직을 직접 작성하는 식으로 커스터마이징할 수 있다.
MemberAddRequest.java
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MemberAddRequest {
private String email;
private String password;
private String nickname;
public Member toEntity() {
return Member.builder()
.email(email)
.password(password)
.nickname(nickname)
.authorities(Authorities.ROLE_USER)
.build();
}
}
회원 추가 요청 dto로, 이메일, 비밀번호, 닉네임 정보를 받아 전달한다.
MemberApiController.java
@RequiredArgsConstructor
@RestController
public class MemberApiController {
private final MemberService memberService;
@PostMapping("/signup")
public ResponseEntity<String> signUp(@RequestBody MemberAddRequest request) {
Long memberId = memberService.saveMember(request);
if(memberId == null) { // 이메일 중복확인으로 가입되지 않음
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("중복된 이메일");
} else { // 정상 가입 완료
return ResponseEntity.status(HttpStatus.CREATED)
.body("회원가입 완료");
}
}
@GetMapping("/logout")
public ResponseEntity<String> logout(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return ResponseEntity.ok()
.body("로그아웃 완료");
}
}
회원가입과 로그아웃 메서드이다.
- 회원가입
- 서비스 메서드에서 반환된 memberId 값이 null이면, 이메일 중복확인으로 인해 가입되지 않은 것으로, 400 Bad Request 응답을 보낸다.
- 정상적으로 가입되어 memberId가 제대로 반환되면, 201 Created 응답을 보낸다.
- 로그아웃
- new SecurityContextLogoutHandler 인스턴스를 생성하고 logout 메서드를 호출하여 로그아웃 처리를 한다.
- SecurityContextLogoutHandler는 스프링 시큐리티에서 제공하는 로그아웃 처리 담당 클래스이다.
- new SecurityContextLogoutHandler 인스턴스를 생성하고 logout 메서드를 호출하여 로그아웃 처리를 한다.
3. 시큐리티 설정 파일 생성
WebSecurityConfig.java
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers("/static/**");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/signup").permitAll()
.anyRequest().authenticated()
)
.formLogin(form ->
form.loginPage("/login")
.defaultSuccessUrl("/search")
)
.logout(logout ->
logout.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
)
.csrf().disable();
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, MemberService memberService) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(memberService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}
}
WebSecurityConfig.java 파일의 전체 코드이다.
코드를 차근차근 살펴보도록 하자
(1) 특정 경로에 대해 스프링 시큐리티 기능 비활성화
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers("/static/**");
// .requestMatchers("이렇게")
// .requestMatchers("여러 경로도")
// .reqeustMatchers("적용 가능")
}
/static/ 하위의 정적 파일들(이미지 등)에 스프링 시큐리티를 비활성화하는 코드이다.
스프링 시큐리티는 기본적으로 모든 요청에 적용되므로, 일부 경로에는 인증/인가 과정 없이도 접근할 수 있도록 스프링 시큐리티를 비활성화했다.
(2) 웹 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/signup").permitAll() //"/signup" 경로의 요청은 모든 접근을 허용.
.anyRequest().authenticated() //그 외의 모든 요청에는 접근시 인증이 필요.
)
.formLogin(form ->
form.loginPage("/login") // "/login" 경로를 로그인 페이지로 설정.
.loginProcessingUrl("/loginProcessing") // "/loginProcessing" 경로를 로그인 처리 경로로 설정
.defaultSuccessUrl("/search") //로그인 성공시, "/search" 경로로 이동.
)
.logout(logout ->
logout.logoutSuccessUrl("/login") //로그아웃 성공시 "/login" 경로로 이동.
.invalidateHttpSession(true) //로그아웃 이후 세션을 전체 삭제.
)
.csrf().disable(); // csrf 비활성화(테스트용)
return http.build();
}
인증/인가, 로그인, 로그아웃 관련 설정을 한다(주석 참고)
(3) 인증 관리자 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, MemberService memberService) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(memberService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}
인증 관리자를 커스텀하는 메서드이다.
사용자 상세 서비스를 직접 만든 클래스로 적용할 수 있고, 비밀번호 암호화 인코더를 지정할 수 있다.
필요하지 않다면 생략 가능하다.
- HttpSecurity 객체에서 AuthenticationManagerBuilder의 인스턴스를 가져온다.
- getSharedObject 메서드는 HttpSecurity에 등록된 공유 객체 중에서 특정 타입의 인스턴스를 가져온다.
- AuthenticationManagerBuilder는 인증 관리자를 구성하기 위한 빌더 클래스이다.
- 사용자 상세 서비스를 설정한다.
- 여기서 설정하는 클래스는 UserDetailsService를 상속받은 클래스여야 한다.
- 사용자 이름을 기반으로 사용자의 이름, 비밀번호, 권한 등의 정보를 불러오는 역할을 한다.
- 사용자 비밀번호를 암호화할 인코더를 설정한다.
여기까지 작성하면 회원가입, 로그인, 로그아웃 로직을 이용할 수 있다.
뷰와 뷰 컨트롤러를 추가로 작성하여 MVC 아키텍처로 구성해도 되지만, 나는 리액트 서버와 같이 굴릴 것이기 때문에 뷰 관련 작업은 하지 않았다.
대신 리액트와의 연동을 위해 추가적으로 작성한 코드가 있어, 별도의 글로 작성했다.
([Spring Boot + React] 리액트를 위한 스프링 시큐리티 추가 설정)
[Spring Boot + React] 리액트를 위한 스프링 시큐리티 추가 설정
([Spring Boot] 스프링 시큐리티로 회원가입, 로그인, 로그아웃 구현하기) [Spring Boot] 스프링 시큐리티로 회원가입, 로그인, 로그아웃 구현하기 (+React 연동) 👀 스프링 시큐리티로 회원가입, 로그인,
cr0c0.tistory.com
'Java > Spring Framework' 카테고리의 다른 글
[Spring Boot] 테스트 코드 작성하기 (JUnit, AssertJ, Mockito) (0) | 2024.04.19 |
---|---|
[Spring Boot] 스프링 시큐리티 + JWT (0) | 2024.04.01 |
[Spring Boot + React] 리액트를 위한 스프링 시큐리티 추가 설정 (CORS 오류 해결+인증 성공시 응답) (0) | 2024.03.20 |
[Spring] Spring Security(스프링 시큐리티) (0) | 2024.03.17 |
Spring Framework와 Spring Boot의 차이점 (0) | 2024.03.02 |