Spring 심화반 - 2주차 - 1

귀찮Lee·2022년 4월 13일
0

2022년 4월 13일(수)
[스파르타코딩클럽] Spring 심화반 - 2주차 - 1

◎ 웹의 인증 및 인가

  • 인증 (Authentication): 사용자 신원을 확인하는 행위
    ex) 로그인을 통해 본인임을 확인 (주로, 아이디와 패스워드 이용)
  • 인가 (Authorization): 사용자 권한을 확인하는 행위
    ex) 웹 카페 사이트에서 회원 랭킹 별 가능한 첨부파일 크기를 다르게 부여

◎ 쿠키와 세션

  • 기본적으로 HTTP는 상태를 저장하지 않는다.
    -> 따라서 HTTP 한 클라이언트에서 서버에게 요청을 여러번 보낸다고 해도, 서버에서는 같은 클라이언트에서 보낸 요청인지 알 수 없다.

  • 쿠키 : 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일

    • 구성요소
      • Name (이름): 쿠키를 구별하는 데 사용되는 키 (중복될 수 없음)
      • Value (값): 쿠키의 값
      • Domain (도메인): 쿠키가 저장된 도메인
      • Path (경로): 쿠키가 사용되는 경로
      • Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제됩니다.)
    • 웹 브라우저에 저장된 '쿠키' 를 확인하는 법
      • 크롬 브라우저 기준으로 '개발자도구' 를 연다.
      • Application - Storage - Cookies 에 도메인 별로 저장되어 있음.
  • 세션 : 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용

    • 서버에서 클라이언트 별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장
    • 서버에서 생성한 '세션 ID' 는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용됨
    • 세션을 유지하는 과정
      1. 클라이언트가 서버에 1번 요청
      2. 서버가 세션ID 를 생성하고, 응답 헤더에 전달 (세션 ID 형태: "SESSIONID = 12A345")
      3. 클라이언트가 쿠키를 저장 ('세션쿠키')
      4. 클라이언트가 서버에 2번 요청 - 쿠키값 (세션 ID) 포함하여 요청
      5. 서버가 세션ID 를 확인하고, 1번 요청과 같은 클라이언트임을 인지

◎ '스프링 시큐리티' 프레임워크

  • 'Spring Security': 스프링 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공해 줌으로써 개발의 수고를 덜어준다.

  • '스프링 시큐리티' 프레임워크 추가하는 법

// build.gradle dependencies 에 추가하고 실행시켜 줄것
// 스프링 시큐리티
    implementation 'org.springframework.boot:spring-boot-starter-security'
  • '스프링 시큐리티' 활성화
// com.sparta.security WebSecurityConfig.java

@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();
    }
}
  • '스프링 시큐리티' default 로그인 기능
    • localhost:8080/login 접속
    • Username : user / Password : spring 로그 확인

◎ 회원 가입 UI 반영

  • 실행을 하면 HTML에 CSS가 적용이 안됨
    -> '개발자 도구' > Network 로 에러 내용 확인
    -> 스프링 시큐어리티의 허용 정책 변경 필요
    // WebSecurityConfig.js
    
    // image 폴더를 login 없이 허용
    .antMatchers("/images/**").permitAll()
    // css 폴더를 login 없이 허용
    .antMatchers("/css/**").permitAll()

◎ 회원 가입 기능 구현

  • 회원 테이블 설계 (id, username, password, email, role)

    • 회원 ID는 username으로 설정할 것 (스프링 시큐어리티 기본값임)

    • role은 특정 값만 가지면 되므로 Enum을 선언해서 사용

      // model > User 일부
      	...
        @Column(nullable = false)
        @Enumerated(value = EnumType.STRING)
        private UserRoleEnum role;
      
        public User(String username, String password, String email, UserRoleEnum role) {
            this.username = username;
            this.password = password;
            this.email = email;
            this.role = role;
      // model > UserRoleEnum
      public enum UserRoleEnum {
        USER,  // 사용자 권한
        ADMIN  // 관리자 권한
      }
  • 회원 가입 UI 반영

    • 타임리프 모듈 추가 (멀티 페이지 기준으로 작성)
      // build.gradle dependencies 에 추가하고 실행시켜 줄것
       // Thymeleaf (뷰 템플릿 엔진)
       implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    • 회원 가입 페이지 요청 처리: WebSecurityConfig 에 formLogin() 처리 부분 변경 / 페이지를 호출하는 UserController 생성
      // WebSecurityConfig.js
      				.and()
                       // 로그인 기능 허용
                       .formLogin()
                       .loginPage("/user/login")
                       .defaultSuccessUrl("/")
                       .failureUrl("/user/login?error")
                       .permitAll()

◎ 패스워드 암호화 구현

  • 현 상태의 문제점: 패스워드가 평문(=있는 그대로)으로 저장되어 있음
    ->'정보통신망법, 개인정보보호법' 에 의해 비밀번호는 암호화(Encryption)가 의무이다. // 해커나 개발자의 악용 소지가 있기 때문이다.

    -> 암호화 후 패스워드 저장이 필요 / 평문 → (암호화 알고리즘) → 암호문

  • 암호화 알고리즘 사용용도

    • 회원 가입 시 패스워드를 암호화하여 저장
      • 직접 구현해 줘야 함
    • 로그인 인증 시 사용
      • 스프링 시큐리티 자동으로 가져다 사용
      • 로그인 처리 시
        • 사용자가 입력한 패스워드 평문을 암호화
        • 암호화된 DB 의 패스워드와 비교
  • 패스워드 암호화 적용 (스프링 시큐어리티에서 'BCrypt 해시함수' 사용하기를 권고)

    1. 암호화 알고리즘을 Bean으로 등록 (@Configuration 아래에 있어야함)
    // security > WebSecurityConfig 에 추가
    
    @Bean
    public BCryptPasswordEncoder encodePassword() {
        return new BCryptPasswordEncoder();
    }
    1. 회원 가입 시 패스워드 암호화 구현
    // service > UserService.js 일부
     public class UserService {
     private final PasswordEncoder passwordEncoder;
     private final UserRepository userRepository;
      ...
      
        public void registerUser(SignupRequestDto requestDto) {
     	  ...
           // 패스워드 암호화
          String password = passwordEncoder.encode(requestDto.getPassword());
          ...
          User user = new User(username, password, email, role);
          userRepository.save(user);
     	}
    }

◎ 로그인, 로그아웃 기능 구현

  • 스프링 시큐리티 사용시 구조

  • 로그인 처리 과정

  • 로그아웃 처리 과정

    • "GET /user/logout" 요청 시 로그아웃
    • 서버 세션에 저장되어 있는 로그인 사용자 정보 삭제
  • 로그인, 로그아웃 구현

    • 로그인, 로그아웃 처리 URL 설정
    // security > WebSecurityConfig.js
    
                    // [로그인 기능]
                        .formLogin()
                        // 로그인 View 제공 (GET /user/login)
                        .loginPage("/user/login")
                        // 로그인 처리 (POST /user/login)  *추가*
                        .loginProcessingUrl("/user/login")
                        // 로그인 처리 후 성공 시 URL  *추가*
                        .defaultSuccessUrl("/")
                        // 로그인 처리 후 실패 시 URL  *추가*
                        .failureUrl("/user/login?error")
                        .permitAll()
                    .and()
                        // [로그아웃 기능]
                        .logout()
                        // 로그아웃 처리 URL  *추가*
                        .logoutUrl("/user/logout")
                        .permitAll();
    • DB 의 회원 정보 조회 → 스프링 시큐리티의 "인증 관리자" 에게 전달

      • UserDetailsService 구현
      @Service
      public class UserDetailsServiceImpl implements UserDetailsService {
      
          private final UserRepository userRepository;
      
          @Autowired
          public UserDetailsServiceImpl(UserRepository userRepository) {
              this.userRepository = userRepository;
          }
      
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              User user = userRepository.findByUsername(username)
                      .orElseThrow(() -> new UsernameNotFoundException("Can't find " + username));
      
              return new UserDetailsImpl(user);
          }
      }
      • UserDetails 구현 (기본 세팅)
      public class UserDetailsImpl implements UserDetails {
      
          private final User user;
      
          public UserDetailsImpl(User user) {
              this.user = user;
          }
      
          public User getUser() {
              return user;
          }
      
          @Override
          public String getPassword() {
              return user.getPassword();
          }
      
          @Override
          public String getUsername() {
              return user.getUsername();
          }
      
          @Override
          public boolean isAccountNonExpired() {
              return true;
          }
      
          @Override
          public boolean isAccountNonLocked() {
              return true;
          }
      
          @Override
          public boolean isCredentialsNonExpired() {
              return true;
          }
      
          @Override
          public boolean isEnabled() {
              return true;
          }
      
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              return Collections.emptyList();
          }
      }
    • Controller에서 로그인한 유저 정보 사용하는 법

    // controller > HomeController.js
    
    @Controller
      public class HomeController {
          @GetMapping("/")
          public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
              model.addAttribute("username", userDetails.getUsername()); // 타임리프를 통해 정보 넘겨줌
              return "index"; //  html 건네줌
          }
      }
profile
배운 것은 기록하자! / 오류 지적은 언제나 환영!

0개의 댓글