ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 학원 day100. 폼 입력값 유효성 검증, CSRF
    기록 2023. 1. 26. 16:15

    폼 입력값 유효성 검증하기 

     

    1. 프런트엔드 폼 입력값 유효성 검증 

          * 폼에서 submit 이벤트 발생시 폼입력값을 스크립트 코드로 검증하는 것

     

    2. 백-엔드 폼 입력값 검증

          * 폼 입력값을 저장하는 Form 객체에 각 멤버변수별로 입력값의 유효성을 체크하는 어노테이션을 추가한다. 

          * 컨트롤러의 요청핸들러 메소드에서 @Valid어노테이션을 이용해서 Form 객체에 저장된 폼 입력값의 유효성을 검사한다. 

     

    유효성을 체크하는 어노테이션을 붙인 Form 예시

    * 백엔드 서버로만 검증하지 않고 프런트에서도 이중으로 검증을 한다. 서버검증만 하면 서버의 부하가 클테니까..

     

         - 폼 입력값 유효성 검증 절차

             1. 폼 입력값 유효성 체크를 지원하는 라이브러리 의존성 추가한다.  (Validation)

                  <dependency>
                       <groupId>org.springframework.boot</groupId>
                       <artifactId>spring-boot-starter-validation</artifactId>
                 </dependency>

    유효성체크 라이브러리 추가된 모습

    유효성 검증하는 게 jakarta.validation에서 자바 표준으로 정의되어 있고, 그것을 구현한게 hibernate-validator이다.

    유효성 검증과 관련된 제약조건에 맞는지 확인하는 걸 hibernate-validator에서 구현해 놓은 것이다. 

    javax에 유효성검증과 관련된 어노테이션들이 있다. 

    +) Future.class - 오늘을 기준으로 미래여야 한다. Past.class - 오늘을 기준으로 과거여야 한다. (예매, 예약할 때 사용)

     

    2. 입력폼에서 spring의 form태그를 사용해서 입력폼과 입력필드를 구성한다.

              <form:form>태그,  <form:input>태그,  <form:password>태그,  <form:errors>태그 등을 이용해서 입력폼과 입력필드를 구성한다. 

    스프링에서 지원하는 폼태그의 종류

    3. 폼 입력값을 저장하는 Form 클래스의 멤버변수에 유효성 체크 규칙을 어노테이션을 이용해서 지정한다.

         @NotNull, @NotEmpty, @NotBlank, @Email 등의 어노테이션을 이용해서 유효성체크규칙을 멤버변수별로 지정한다. 

     

    4. 요청핸들러 메소드에서 Form 객체에 저장된 폼 입력값에 대한 유효성검사를 수행하고, 검사결과를 전달받기

    ( * BindingResult은 UserRegisterForm 바로 뒤에 있어야 한다, 사이에 다른 매개변수가 있으면 안된다.)

          @PostMapping("/register")

           public String saveUser(@Valid UserRegisterForm userRegisterForm, BindingResult errors) {

                    if(errors.hasErrors()) {

                            // 유효성 검사를 통과하지 못한 경우, 입력화면으로 내부이동시킨다. 

                            return "register-form";              // WEB-INF/views/register-form.jsp로 내부이동시킨다.

                    }

                    // 유효성 검사를 통과한 경우, 회원가입 업무로직을 호출한다.

                    userService.registerUser(userRegisterForm);

            }

    * @Valid 어노테이션은 UserRegisterForm 객체에 저장된 폼 입력값에 대한 유효성 검사를 수행하게 하는 어노테이션이다. 

    * BindingResult 객체는 유효성 검사결과를 전달받는 객체다.

     

    부트스트랩 Forms-Validation

     

    회원가입 메뉴를 클릭했을 때에는 회원가입폼에 입력값이 없어야 하고, 회원가입버튼을 클릭했는데 유효성 검사를 통과하지 못해서 회원가입폼으로 돌아갔을 때는 입력값이 남아있게 해야 한다.

    회원가입 메뉴를 클릭하면 사용자정보를 표시하는 객체가 없기 때문에 코드를 바꿔야 한다. 

    입력화면으로 들어가기 전에 userRegisterForm 객체를 만들어서 model에 담아놓는다.

     

    userRegisterForm이라는 이름으로 모델안에 객체가 들어있는 것이다.

    <form:form modelAttribute="userRegisterForm"> 

    아이디 : <form:input path="id" /> 

    비밀번호 : <form:password path="password" />

    </form:form>

    userRegisterForm이라는 객체를 찾고 그 객체의 멤버변수 안에 들어있는 값을 찾아 입력되는 것이다. 

    form:type명을 적고, name대신 path를 적고, type은 없앤다. path는 멤버변수의 이름이다. 

    검사창

    path에 적은 값이 name의 값으로 적용되고, id의 값으로 적용됨을 검사창을 통해 확인할 수 있다.

    @ModelAttribute("userRegisterForm")의 역할 2가지

    1. 요청객체나 세션객체에서 userRegisterForm이라는 속성명으로 저장된 객체를 찾아옴 (없으면 새로 만듦)

    2. UserRegisterForm객체를 모델객체에 userRegisterForm라는 이름으로 저장시키는 작업 (위의 코드에 해당)

     

    폼입력값 검증에 활용되는 어노테이션

    @NotNull

               null 값을 허용하지 않는다. 

    @NotEmpty

               null 값을 허용하지 않으며, 최소 한 개 이상의 글자를 포함해야 한다. (공백문자 상관없음)

    @NotBlank

               null 값을 허용하지 않으며, 최소 한 개 이상의 글자(공백문자를 제외한)를 포함해야 한다. 

    @Size(min=숫자, max=숫자)

              문자열 혹은 배열의 최소길이, 최대길이를 지정한다. 

    @Length(min=숫자, max=숫자)

              문자열의 최소 길이, 최대 길이를 지정한다.

    @Min

              최소 정수값을 지정한다.

    @Max

              최대 정수값을 지정한다. 

    @Pattern(regexp=정규표현식, flag={"i", "g", "m"})

              문자열이 지정된 정규표현식과 일치해야 한다. (i: 대소문자 포함하지 않음, g: 중복 포함)

    @Email

              문자열이 이메일형식인지 체크한다. 

    @URL

              문자열이 URL 형식인지 체크한다.

    @Past

              날짜가 현재시간보다 과거인지 체크한다.

    @Future

             날짜가 현재시간보다 미래인지 체크한다.  

     

    어노테이션 옆에 (message = "아이디는 필수입력값이다.")와 같이 유효성 검증을 통과하지 못했을 때 화면에 표시할 메시지를 설정해놓을 수 있다.

     

    @Getter
    @Setter
    public class UserRegisterForm {
    
    	@Size(min = 1, message = "접속 권한은 하나 이상 체크하세요.")
    	private List<String> roleName;
    	
    	@NotBlank(message = "아이디는 필수입력값입니다.")   // 유효성 체크 규칙을 통과하지 못했을 때 화면에 표시할 메시지다.
    	@Length(min = 4, max = 20, message = "아이디는 4글자이상 20글자 이하로 입력하세요")
    	private String id;
    	
    	@NotBlank(message = "비밀번호는 필수입력값입니다.")
    	@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$", message = "비밀번호는 최소 8 자, 대문자 하나 이상, 소문자 하나 및 숫자 하나 이상 포함해야 합니다.")
    	private String password;
    	
    	@NotBlank(message = "이름은 필수입력값입니다.")
    	@Pattern(regexp = "^[가-힣]{2,}$", message = "이름을 한글 2글자 이상으로 입력하세요.")
    	private String name;
    	
    	@NotBlank(message = "이메일은 필수입력값입니다.")
    	@Email(message = "유효한 이메일 형식이 아닙니다.")
    	private String email;
    	
    	@NotBlank(message = "전화번호는 필수입력값입니다.")
    	@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
    	private String tel;
    }

    HomeController.java

    	// 회원가입 요청
    	@PostMapping("/register")
    	// BindingResult는 유효성 검사 결과가 저장되는 객체이다. 
    	public String register(@ModelAttribute("userRegisterForm") @Valid UserRegisterForm userRegisterForm, BindingResult errors) {
    		if (errors.hasErrors()) {
    			return "register-form";
    		}
    		
    		try {
    			userService.registerUser(userRegisterForm);
    		} catch (AlreadyRegisteredUserIdException ex) { 
    			errors.rejectValue("id", null, "이미 사용중인 아이디입니다.");  // id는 jsp의 path name이랑 같아야 한다. 
    			return "register-form";
    		} catch (AlreadyRegisteredEmailException ex) {
    			errors.rejectValue("email", null, "이미 사용중인 이메일입니다.");
    			return "register-form";
    		}
    		
    		return "redirect:registered";
    	}

    @Valid라는 어노테이션을 붙이면 유효성체크를 실행하게 함. UserRegisterForm에 전달된 값이 유효성에 어긋나지 않는지 체크함.

    BindingResult는 유효성검사결과가 저장되는 객체이다.

    <form:errors path="tel"  cssClass="text-danger" />라는 태그를 사용하여 유효성체크에 어긋났을 때, 에러메시지를 나타낼 수 있다.


    어노테이션으로 검증할 수 없는 사항(로직을 수행해야지 검증할 수 있는 것)의 에러메시지를 화면에 표현하고 싶을 때

    BindingResult errors에 저장한다.

     

    ApplicationException을 상속받은 Exception클래스를 만든다.

    Exception을 따로 발생시킨다.

    HomeController.java

    유효성검증을 통과하지 못했던 사유를 errors.rejectValue에 적는다. 

    id, email는 register-form.jsp의 path 이름이랑 같아야 한다.

    null에는 국제화처리로, 에러메시지의 코드명을 적는다.


    회원가입페이지 소스보기 아래쪽에 hidden으로 csrf가 활성화되어있음을 확인할 수 있음 .

    CSRF를 활성화시키고 로그인을 했더니 에러가 발생하였음

    로그인페이지의 소스보기에서는 hidden으로 csrf가 없음을 확인할 수 있었음

    hidden필드 값이 없으면 글쓰기, 로그인 등이 안됨.

    게시글 작성폼 요청, 로그인폼 요청, 회원가입폼요청이 오면 스프링시큐리티가 csrf 방지 토큰을 생성하고, 토큰을 세션객체에 저장한다. 

    controller에 넘어가기전에 csrf필터가 입력폼의 csrf의 값과 HttpSession객체의 csrf값을 비교해서 동일한지 확인한다.

     

    CSRF 공격의 절차

      1. 공격자가 악성코드가 포함된 컨텐츠를 등록한다. 

         * 악성코드는 선량한 사용자의 의도와 상관없이 서버를 상대로 공격자가 지정한 행동을 하게 한다. 

      2. 선량한 사용자는 로그인상태에서 악성코드가 포함된 컨텐츠를 열람한다.

         * 악성코드에 포함된 스크립트는 선량한 사용자의 세션아이디를 사용해서 서버를 상대로 악의적인 작업을 수행시킨다.

         * 선량한 사용자는 단지 컨텐츠를 열람했을 뿐인데, 자신의 이름으로 서버를 상대로 악의적인 작업을 수행하게 된다.

         * 위의 행동을 사이트 간 요청위조라고 한다. 

     

    사이트 간 요청위조를 방지하기

       * 서버에 값이 전달되는 행위는 반드시 사용자가 입력폼을 요청한 후에 사용자의 자발적인 등록 절차를 통해서만 수행되게 하자. 

       * 입력폼에 CSRF 토큰을 추가시킨다. 

       * 클라이언트의 등록요청을 처리하기 전에, 서버로 전달된 폼 입력값의 CSRF 토큰과 세션에 저장된 CSRF 토큰을 비교해서 토큰값이 일치하는 경우에만 사이트 간 요청위조가 아닌 정상적인 요청으로 간주한다. 

    따라서, csrf토큰이 없거나 일치하지 않으면 사용자의 자발적인 요청이 아니라고 간주하는 것이다.

     

    로그인은 충분히 유효성 검증을 안해도 프론트엔드쪽에서도 검증가능하다.

    회원정보는 자유도가 낮아서 유효성을 검증해야한다.

     

    CSRF 토큰을 입력폼에 포함시키기

    - <form:form> 태그로 입력폼 구성하기

      <form:form></form:form> 태그로 입력폼을 구성하면 자동으로 CSRF 토큰이 히든 필드로 추가된다. 

      * 입력값 유효성 검증이 필요한 입력폼을 구성할 때 사용하자.

     

    - <sec:csrfinput /> 태그를 입력폼에 추가하기

      <sec:csrfinput /> 태그를 추가하면 입력폼에 CSRF 토큰이 히든필드로 추가된다.

    * 입력값 유효성 검증이 필요없는 입력폼(예: 로그인화면)을 구성할 때 사용하자.

     

    CSRF 를 켜놓으면 로그아웃이 get방식을 지원하지 않는다. 

    따라서 코드를 다음과 같이 적어놓는다.

     

    tags.jsp를 모든 jsp에 포함시키기 위해서는 web.xml에 <jsp-config>태그를 사용해서 설정하거나, 

    스프링부트에서는 ServletInitializer.java에 아래와 같이 자바코드로 설정한다. 

    모든 jsp에 tags.jsp를 포함시키라는 의미이다.

    댓글

Designed by Tistory.