📚 관련 독서/스프링 부트 3 백엔드 개발자 되기 - 자바 편

[SpringBoot] 08장 스프링 시큐리티 ver. 로그인/로그아웃, 회원 가입 구현

imname1am 2024. 2. 13. 01:31
반응형

 

스프링 시큐리티 ver.
로그인/로그아웃, 회원 가입

 

 

📗 스프링 시큐리티⭐

⭐ 인증과 인가

- 인증 (Authentication) : 등록된 사용자의 신원을 입증하는 과정 (예 : 로그인 시 누구인지 확인하는 과정)

- 인가 (Authorization)   : 특정 부분에 접근할 수 있는지 권한을 확인하는 작업 (예 : 관리자 페이지)

 

 

스프링 시큐리티 (P.201,202)

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

 

- 역할 : 보안 관련 옵션 제공 (CSRF 공격, 세션 고정 공격 방어)

 

* CSRF 공격       : 사용자 권한을 갖고 특정 동작을 수행하도록 유도하는 공격

* 세션 고정 공격 : 사용자의 인증 정보를 탈취하거나 변조하는 공격

 

- 필터 기반 동작

  • UsernamePasswordAuthentication : ID, PW 넘어오면 인증 요청을 위임하는 인증 관리자
  • FilterSecurityInterceptor : 권한 부여 처리를 위임해 접근 제어 결정을 쉽게하는 접근 결정 관리자

 

- 세션&쿠키 방식으로 인증 처리 (세션 기반 인증)

  • UserDetails : 스프링 시큐리티에서 인증,인가 정보를 담은 인터페이스. 이 클래스를 상속받은 뒤 메서드를 오버라이드해 사용하면 된다.
  • UserDetailService : 스프링 시큐리티에서 사용자 정보 가져올 때 사용하는 클래스. 이 클래스를 상속받은 뒤 loadUserByUsername()을 오버라이드해 사용

 

 

 

 

 

📗 회원 도메인 만들기

1. 의존성 추가하기 (build.gradle)

dependencies에 스프링 시큐리티용 의존성 추가하기

implementation 'org.springframework.boot:spring-boot-starter-security'  // 1) 스프링 시큐리티 사용하기 위한 스타터 추가
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'  // 2) 타임리프에서 스프링 시큐리티 사용하기 위한 의존성 추가
testImplementation 'org.springframework.security:spring-security-test'  // 3) 스프링 시큐리티 테스트 위한 의존성 추가

 

2. 엔티티 추가하기 (domain 패키지 - User.java)

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User implements UserDetails {  // UserDetails 상속받아 인증 객체로 사용

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password")
    private String password;

    @Builder
    public User(String email, String password, String auth) {
        this.email = email;
        this.password = password;
    }

    @Override   // 권한 반환
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("user"));
    }

    // 사용자 id 반환 (고유값)
    @Override
    public String getUsername() {
        return email;
    }

    // 사용자 pwd 반환
    @Override
    public String getPassword() {
        return password;
    }

    // 계정 만료 여부 반환
    @Override
    public boolean isAccountNonExpired() {
        return true;    // true -> 아직 만료 X
    }

    // 계정 잠금 여부 반환
    @Override
    public boolean isAccountNonLocked() {
        return true;    // true -> 아직 잠금 X
    }

    // 패스워드 만료 여부 반환
    @Override
    public boolean isCredentialsNonExpired() {
        return true;    // true -> 만료 X
    }

    // 계정 사용 가능 여부 반환
    @Override
    public boolean isEnabled() {
        return true;    // true -> 사용 가능
    }
}

 

➜ UserDetails 클래스 : 스프링 시큐리티에서 사용자의 인증 정보 담아두는 인터페이스

 

 

3. 리포지토리 만들기 (UserRepository.java)

public interface UserRepository extends JpaRepository<User, Long>{
    Optional<User> findByEmail(String email);   // 1) email로 사용자 정보 가져옴
}

 

▶ 자주 사용하는 쿼리 메서드

- findByName() : "name" 컬럼 값 中 파라미터로 들어오는 값과 같은 데이터 반환

- findByNameAndAge()

- findByNameOrAge()

- findByAgeLessThan()

- findByAgeGreaterThan()

- findByName(Is)Null()

 

 

4. 서비스 메서드 코드 만들기 (service - UserDetailService.java)

@RequiredArgsConstructor
@Service
// 스프링 시큐리티에서 사용자 정보 가져오는 인터페이스
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    // 사용자 이름(email)으로 사용자 정보 가져오는 메소드
    @Override
    public User loadUserByUsername(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException((email)));
    }
}

 

 

📗 시큐리티 설정하기

config - WebSecurityConfig.java : 실제 인증 처리하는 시큐리티 설정 파일

 

@RequiredArgsConstructor
@Configuration
public class WebSecurityConfig {

    private final UserDetailService userService;

    // 1) 스프링 시큐리티의 모든 기능 비활성화
    @Bean
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers("/static/**");
    }

    // 2) 특정 HTTP 요청에 대한 웹 기반 보안 구성
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests()    // 3) 인증, 인가 설정
                .requestMatchers("/login", "/signup", "/user").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()    // 4) 폼 기반 로그인 설정
                .loginPage("/login")
                .defaultSuccessUrl("/articles")
                .and()
                .logout()   // 5) 로그아웃 설정
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true)
                .and()
                .csrf().disable()   // 6) CSRF 비활성화
                .build();
    }

    // 7) 인증 관리자 관련 설정
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http,
                                                       BCryptPasswordEncoder bCryptPasswordEncoder,
                                                       UserDetailService userDetailService) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userService)    // 8) 사용자 정보 서비스 설정
                .passwordEncoder(bCryptPasswordEncoder)
                .and()
                .build();
    }

    // 9) 패스워드 인코더로 사용할 Bean 등록
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

3) 특정 경로에 대한 액세스 설정

➜ requestMatchers() : 특정 요청과 일치하는 url에 대한 액세스 설정

➜ permitAll()             : 인증/인가 없이 누구나 접근 가능하게 함

➜ anyRequest()        : 위에서 설정한 url 이외의 요청에 대해 설정

➜ authenticated()      : 인가는 필요 없지만 인증이 성공된 상태여야 접근 가능

 

4) 폼 기반 로그인 설정

➜ loginPage()              : 로그인 페이지 경로 설정

➜ defaultSuccessUrl() : 로그인 완료 시 이동할 경로 설정

 

5) 로그아웃 설정

➜ logoutSuccessUrl()          : 로그아웃 완료 시 이동할 경로 설정

➜ invalidateHttpSession()   : 로그아웃 이후 세션을 전체 삭제할지 여부 설정

 

 

📗 회원 가입 구현하기

1. 서비스 메서드 코드 만들기

1) 사용자 정보 담는 객체 작성하기 (dto - AddUserRequest.java)

@Getter
@Setter
public class AddUserRequest {
    private String email;
    private String password;
}

 

2) AddUserRequest 객체를 인수로 받는 회원 정보 추가 메서드 작성 (service - UserService.java)

- 패스워드 저장 시 시큐리티 설정하며 패스워드 인코딩용으로 등록한 빈을 사용해 암호화한 후 저장

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public Long save(AddUserRequest dto) {
        return userRepository.save(User.builder()
                .email(dto.getEmail())
                // 1) 패스워드 암호화
                .password(bCryptPasswordEncoder.encode(dto.getPassword()))
                .build()).getId();
    }
}

 

 

2. 컨트롤러 작성하기 (UserApiController.java)

- signup() : 회원가입 폼에서 회원가입 요청 받으면 서비스 메서드 사용해 사용자 저장 후, 로그인 페이지로 이동

@RequiredArgsConstructor
@Controller
public class UserApiController {

    private final UserService userService;

    @PostMapping("/user")
    public String signup(AddUserRequest request) {
        userService.save(request);  // 회원 가입 메서드 호출
        return "redirect:/login";   // 회원 가입이 완료된 이후 로그인 페이지로 이동
    }
}

 

 

📗 회원 가입, 로그인 뷰 작성하기

1. 뷰 컨트롤러 구현하기 (UserViewController.java)

: 로그인, 회원가입 경로로 접근하면 뷰 파일(화면)을 연결하는 컨트롤러

@Controller
public class UserViewController {
    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/signup")
    public String signup() {
        return "signup";
    }
}

 

2. 뷰 작성하기

- login.html 코드

- signup.html 코드

 

 

📗 로그아웃 구현하기

1. 로그아웃 메서드 작성하기 (UserApiController.java)

- SecurityContextHolder :  스프링 시큐리티에서 인증이 완료된 후 Authentication 객체 저장하는 곳

// 로그아웃
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
    new SecurityContextLogoutHandler().logout(request, response,
            SecurityContextHolder.getContext().getAuthentication());

    return "redirect:/login";
}

 

 

2. 로그아웃 뷰 추가하기 (articleList.html)

                <a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러 가기</a>
            </div>
        </div>
        <br>
    </div>

    <button type="button" class="btn btn-secondary" onclick="location.href='/logout'">로그아웃</button>
</div>
    <script src="/js/article.js"></script>
</body>
</html>

 

 

📗 실행 테스트하기

1. 테스트 위한 환경변수 추가하기 (application.yml)

spring:
  jpa:
    # 전송 쿼리 확인
    show-sql: true
    properties:
      hibernate:
        format_sql: true

    # 테이블 생성 후, data.sql 실행
    defer-datasource-initialization: true

  datasource: # DB 정보 추가
    url: jdbc:h2:mem:testdb
    username: sa

  h2: # H2 서버 활성화
    console:
      enabled: true

 

2. 로그인, 회원 가입 실행 테스트하기

1) http://localhost:8080/articles

: 인증된 사용자만 접근 가능하므로 로그인 페이지인 /login으로 리다이렉트 된다.

 

 

2) 회원가입하기 (localhost:8080/signup) → 로그인하기

: ID, PWD 입력하고 [Submit] 눌러 회원가입하고 로그인 페이지에서 만든 계정으로 로그인 성공 시 글 목록 페이지로 이동

 

 

3) 회원가입한 데이터가 DB에 있는지 확인하기 (http://localhost:8080/h2-console)

비밀번호가 암호화된 것도 확인 가능

 

 

3. 로그아웃 실행 테스트하기

/logout으로 이동하면 이렇게 변한다.

 

 

 

(해당 글 내용은 📗 스프링 부트 3 백엔드 개발자 되기 - 자바 편을 읽고 정리한 내용입니다.)

반응형