인증, 인가 / 쿠키, 세션 / Spring Security / (항해일지 18일차)

김형준·2022년 5월 26일
0

TIL&WIL

목록 보기
18/45
post-thumbnail
  • 오늘은 주특기 입문 주가 마무리 되는 날이다.
  • Spring Boot의 전체적인 구동 방식을 공부하며 전체적인 그림을 그릴 수 있게 되었다.
  • 전체적인 틀을 학습했으니, 이제는 부분 별로 어떻게 작동되는 지 뜯어볼 차례다.. 그래서인지 심화 강의가 시작되고는 받아들이는 정보의 양이 급격하게 늘어났다.
  • 하루 하루 공부한 부분을 나의 생각으로 재편하는 과정은 쉽지만은 않지만 나중에 봤을 때 훨씬 수월하게 복기할 수 있을 것 같다. 미래를 위한 투자라고 생각하고 꾸준히 작성해보자 😂

1. 학습일지

🔗 학습한 코드

1) 인증(Authentication) vs 인가(Authorization)

  • 인증과 인가는 얼핏 보면 같은 개념으로 혼동하기 쉬운 것 같다.
  • 하지만 명확한 차이를 지닌다.
    • 인증: 사용자 신원을 확인하는 행위
    • 인가: 사용자 권한을 확인하는 행위
  • 보안구역에 출입한다고 생각해보자, 출입하기 위해 출입증을 제출하여 신원을 확인하는 것은 인증에 해당하며, 신원에 따라 출입 장소에 접근할 수 있는 권한을 확인하는 것이 인가이다.
  • 웹에 접근하는 상황에서는, 인증은 로그인을 하는 것이고, 인가는 역할에 따른 사용 권한을 관리하는 것.

2) 쿠키와 세션

  • 쿠키와 세션의 가장 큰 차이점은 정보가 저장되는 공간이다.
    • 쿠키는 클라이언트에 저장된다
    • 세션은 서버에 저장된다

1) 쿠키 (클라이언트에 저장)

  • 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
  • 구성요소
    • Name (이름): 쿠키를 구별하는 데 사용되는 키 (중복될 수 없음)
    • Value (값): 쿠키의 값
    • Domain (도메인): 쿠키가 저장된 도메인
    • Path (경로): 쿠키가 사용되는 경로
    • Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제됨)

2) 세션 (서버에 저장)

  • 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
  • 서버에서 클라이언트 별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장
  • 서버에서 생성한 '세션 ID' 는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용됨
  • 즉, 세션도 쿠키가 필요하다
  • 세션 동작 방식은 아래와 같다.

3) Spring Security

  • 스프링 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공해 줌으로써 개발의 수고를 덜어주는 프레임워크


스프링 시큐리티 구현 및 로그인 기능 구현 과정

// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
  1. build.gradle에 위 코드 추가 ( 붙여 넣으면 오른쪽 상단부쯤에 아이콘이 뜬다. load Gradle Changes 클릭 필수)

  • 저 아이콘 안눌러주면 인텔리제이를 다시 켜야한다!!
  1. WebSecurityConfig 클래스 정의
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration // 서버가 기동될 때 설정해주겠다.
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                // 어떤 요청이든 '인증'이 필요하다.
                .anyRequest().authenticated()
                .and()
                    // 단, 로그인 기능은 인증없이 허용
                    .formLogin()
                    .defaultSuccessUrl("/")
                    .permitAll()
                .and()
                    // 단, 로그아웃 기능은 인증없이 허용
                    .logout()
                    .permitAll();
    }
}
  1. 서버 실행

    • 아주 친절하게도 루트 주소만 입력해도 로그인 페이지가 나타난다.
    • 아직 회원가입 기능은 구현하지 않았기 때문에 스프링 시큐리티가 디폴트로 제공하는 계정을 활용한다.
    • 계정 ID는 user / PW는 인텔리제이 콘솔에 나와있다.
    • 로그아웃은 주소창에 루트/logout 입력하면 된다.
  2. 프론트 엔드에서 작업이 완료되어 css, html 파일을 받은 상황)

    • WebSecurityConfig 클래스에서 로그인 페이지와 로그인 실패시 페이지를 지정해준다.
    • 문제가 발생했다. 모든 요청은 인증을 수반하기 때문에 로그인 페이지에 담긴 css가 안넘어온다.
    • 따라서 html에 포함된 css파일 요청이 막혀있는 것도 풀어줘야한다. 따라서 아래와 같은 코드 붙이기
// image 폴더를 login 없이 허용
.antMatchers("/images/**").permitAll()
// css 폴더를 login 없이 허용
.antMatchers("/css/**").permitAll()

Spring Security 회원 가입 기능 구현 과정

  1. 회원 테이블과 필요한 API를 설계해준다.
    • 위 예시는 현재 나만의 셀렉샵 프로젝트에 해당
  1. 설계한 테이블 바탕으로 @Entity 클래스를 구현한다.
    • Enum 값을 컬럼으로 설정할 때 Enum타입이 아닌 Enum이 가진 String 값을 저장해야한다.
    • @Enumerated(value = EnumType.STRING) -> JPA가 자동으로 해준다.
  1. 관리자 가입 토큰을 랜덤 스트링으로 만들어서 관리자 인가 방법을 구현한다
    • 현업에서는 관리자 권한을 부여할 수 있는 관리자 페이지를 구현하고, 결재과정을 거친다.
  1. 회원가입 관련 Controller, Service, Repository를 구현한다.
    • 그런데 시큐리티에 의해 회원가입 페이지, h2-console 페이지 등으로도 넘어가질 않는다.
  1. 스프링 시큐리티 허용 정책 변경 필요 (WebSecurityConfig 클래스)
  • 회원 관리 처리 API (POST /user/**) 에 대해 CSRF 무시
http.csrf()
	.ignoringAntMatchers("/user/**");
- 회원 관리 처리 API 전부를 login 없이 허용
http.authorizeRequests()
	.antMatchers("/user/**").permitAll()
  • h2 콘솔 사용에 대한 허용
@Override
public void configure(WebSecurity web) {
	// h2-console 사용에 대한 허용 (CSRF, FrameOptions 무시)
	web
		.ignoring()
		.antMatchers("/h2-console/**");
}
  • WebSecurityConfig는 공식처럼 사용하고, 차차 공부할 예정이다.
  1. 비밀번호 암호화
    • BCryptPasswordEncoder 를 활용할 것이다.
    • Bean으로 등록해야하기 때문에, @Configuration이 붙은 클래스에서 메서드를 만들자
    • 만든 메서드는 new연산자로 만들어진 BCryptPasswordEncoder를 리턴한다.
@Bean
public BCryptPasswordEncoder encodePassword() {
    return new BCryptPasswordEncoder();
}
  • 실제로 사용하는 부분인 Service에서는 PasswordEncoder를 final 변수로 선언하고
  • 생성자 부분에서 @AutoWired를 통해 DI한다.
  • BCryptPasswordEncoder는 PasswordEncoder를 implements 하기에 알아서 찾아온다.

로그인 로그아웃 구현


0. 전체적인 과정:

  • 1) 클라이언트가 로그인을 시도하여 username/password 가 전달된다.
  • 2) Authentication Manager(대장)은 UserDetailsService에게 username을 전달한다.
  • 3) UserDetailsService는 받은 username과 일치하는 회원정보를 회원정보DB에서 찾아온다
  • 이 때 성공 시 회원 상세정보(UserDetails) 생성 / 실패 시 Error 발생
  • 4) Authentication Manager(대장)에게 User Details를 넘긴다.
  • 5) 클라이언트가 넘긴 값과 DB에 저장된 값을 비교한다
  • 이 때 일치 시 로그인 성공 & 세션 생성 / 불일치 시 Error 발생
  1. WebSecurityConfig 설정 추가
// 로그인 View 제공 (GET /user/login)
.loginPage("/user/login")
// 로그인 처리 (POST /user/login)
.loginProcessingUrl("/user/login")
// 로그아웃 처리 URL
.logoutUrl("/user/logout")
  1. DB 의 회원 정보 조회 → 스프링 시큐리티의 "Authentication Manager" 에게 전달
  • UserDetailsService 구현: UserDetailsServiceImpl 구현하고 implements UserDetailsService
  • UserDetails 구현: UserDetailsImpl 클래스 구현하고 implements UserDetails
  • 여기에서 UserDetail을 구현한 UserDetailsImpl는 실제 DB에 저장된 회원의 값을 담고있는데,
    이러한 값은 Controller에서 @AuthenticationPrincipal UserDetailsImpl userDetails로 사용가능하다.
  • 각각 필요한 메서드들을 구현해줘야 한다!
  • 이렇게 구현만 하면 알아서 스프링 시큐리티가 갖다 쓴다고 한다..
  1. 로그아웃 버튼 클릭 시
  • "GET /user/logout" 로 API 설계 했는데, "POST /user/logout" 으로 처리 필요
  • CSRF protection 이 기본적으로 enable 되어 있기 때문
  • CSRF protection 을 disable 하면 GET /user/logout 으로도 사용 가능
  • Controller에서 로그인된 회원 정보를 사용할 수 있다.
  • 컨트롤러의 파라미터 자리에서 @AuthenticationPrincipal을 붙여준 UserDetailsImpl userDetailsImpl 로 받을 수 있다.
  • UserDetailsImpl은 UserDetails를 implements한, 우리가 만들어준 클래스이다.
  • csrf 토큰을 비활성화 해주며 post 요청들이 정상적으로 보내지도록 한다.

스프링 시큐리티 권한 설정 방법

  1. 회원 상세정보 (UserDetailsImpl)에 "권한 (Authority)" 정보를 담아줄 수 있다.
  • 권한을 1개 이상 설정 가능
  • "권한 이름" 규칙
  • "ROLE_" 로 시작해야 함 (ROLE_ADMIN, ROLE_USER)
public class UserDetailsImpl implements UserDetails {
		// ...

		@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(adminAuthority);

        return authorities;
    }
}
  1. 스프링 시큐리티를 이용한 API 별 권한 제어 방법
  • Controller 에 "@Secured" 어노테이션으로 권한 설정 가능
  • @Secured("권한 이름") 선언
// (관리자용) 등록된 모든 상품 목록 조회
    @Secured("ROLE_ADMIN")
    @GetMapping("/api/admin/products")
    public List<Product> getAllProducts() {
        return productService.getAllProducts();
    }
  • @Secured 어노테이션 활성화 방법
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  1. 실제 구현 과정
    1) WebSecurityConfig에 @EnableGlobalMethodSecurity(securedEnabled = true)를 붙여 @Secured 어노테이션 활성화
    2) 권한이 필요한 API에 @Secured 어노테이션 추가하여 권한 설정
    3) 스프링 시큐리티가 로그인한 회원의 권한을 인식하도록 수정
  • UserDetailsImpl 클래스가 회원정보가 담기는 곳이므로 이곳에서 권한을 담아줘야함 (아래 코드는 UserDetailsImpl의 오버라이드 메소드)
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority(); //"ROLE_USER" / "ROLE_ADMIN"

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;

    }

2. 오늘의 이슈

💥💥💥H2 DB User 예약어 충돌 이슈💥💥💥

❗❗❗ 이 과정에서 실행 시 DDL 관련 오류가 나타났다❗❗❗

  • JPA가 자동으로 만들어준 쿼리문에 오류가 발생한 것이다.
  • 오류 원인을 찾아 내는데 1시간 정도 걸린 것 같다..
  • 원인은 강의에서 사용한 h2와 내가 사용한 h2 DB의 버전 차이에 기인한다.
  • 찾아보니 h2 1.4.199 -> 2.0.206 버전으로 업그레이드되며 USER가 예약어로 지정되었다고 한다.
  • 내가 사용한 @Entity 클래스인 User가 정확히 충돌된 것이다
    -> 해결 방법은 간단하다.
      1. @Entity 클래스의 이름을 바꿔주거나
      1. application.properties에 아래 코드를 작성한다 ( SQL 문이 실행될 때, 백틱으로 테이블과 컬럼을 자동으로 감싸줘서 예약어 충돌을 방지할 수 있다.)
      spring.jpa.properties.hibernate.globally_quoted_identifiers=true
profile
BackEnd Developer

0개의 댓글