Skip to content

Commit

Permalink
[프론트에서 검증코드 작성시 문제점] 완성
Browse files Browse the repository at this point in the history
  • Loading branch information
jojoldu committed Apr 24, 2017
1 parent d18b6c2 commit 7026cc4
Show file tree
Hide file tree
Showing 29 changed files with 837 additions and 0 deletions.
25 changes: 25 additions & 0 deletions front-validate-problem/.gitignore
@@ -0,0 +1,25 @@
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
nbproject/private/
build/
nbbuild/
dist/
nbdist/
.nb-gradle/
91 changes: 91 additions & 0 deletions front-validate-problem/README.md
@@ -0,0 +1,91 @@
# 프론트엔드에서 유효성 검사가 문제인 이유

안녕하세요? 이번 시간엔 프론트엔드에서 유효성 검사가 문제인 이유에 대해 간단한 예제를 통해 소개하려고 합니다.
모든 코드는 [Github](https://github.com/jojoldu/blog-code/tree/master/front-validate-problem)에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다.
(공부한 내용을 정리하는 [Github](https://github.com/jojoldu/blog-code)와 세미나+책 후기를 정리하는 [Github](https://github.com/jojoldu/review), 이 모든 내용을 담고 있는 [블로그](http://jojoldu.tistory.com/)가 있습니다. )<br/>

이미 아시는 분들에겐 너무나 당연한 이야기겠지만, 포털과 같이 일반 사용자 서비스가 아닌 내부 시스템을 주로 하시는 분들은 크게 신경쓰지 않는 부분이라 모르실 수도 있다는 생각에 진행하게 되었습니다.
길지 않은 내용이라 빠르게 보실 수 있으실것 같습니다^^;
사용된 코드는 전부 위에 링크된 Github 주소에 있으니 참고해주세요!
그럼 시작하겠습니다.

### 문제 상황

결론적으로 **클라이언트 사이드에서만 검증 코드를 작성해서는 안됩니다**.
서버 사이드에서 검증코드가 필수입니다.

![기본화면](./images/기본화면.png)

위처럼 회원 가입 페이지를 만든다고 가정하겠습니다.
일반적으로 회원 가입 서비스는 유효성 검사가 존재하며, 해당 검사를 통해 사용자에게 경고 메세지를 전달하는 기능을 기본적으로 갖고 있습니다.
이럴때 해당 값이 비어있는지, 잘못된 값이 입력되었는지, 길이 제한을 초과하지 않았는지에 대해 검사(앞으로 유효성 검사라 하겠습니다)하는 코드를 어디에 둘지 고민할 수 있습니다.
사용자에게 메세지를 전달하기 편하게 하려고 무심코 자바스크립트로 유효성 검사 코드를 작성할 수 있습니다.
아래는 스크립트로 작성된 유효성 검사 코드입니다.

![코드](./images/코드.png)

클라이언트 사이드에서 input 박스에 빈 값이 있으면 메세지를 출력시키고, 전부 값이 채워져 있으면 Ajax를 통해 save하도록 코드를 작성하였습니다.
클라이언트 사이드에서 체크하는 일반적인 코드라 보시면 됩니다.
그럼 기능을 한번 수행해보겠습니다.
화면에서 빈값으로 등록 버튼을 클릭하면!

![유효성체크](./images/유효성체크.png)

이렇게 에러 메세지를 잘 노출시킵니다.
기능상 문제가 없어보이는데 왜 이렇게 하면 안될까요?
크롬의 개발자 도구를 열어 (단축키 : ```alt+command+i```) 다음과 같이 **브레이크 포인트를 찍어보겠습니다.**

![브레이크 포인트](./images/브레이크포인트.png)

최종 유효성 검사 ```flag```체크인 ```if(isValid)``` 부분에 브레이크 포인트를 걸고, 다시 input box에 빈값을 채운채로 등록 버튼을 클릭하겠습니다.
그러면 코드가 실행 도중 47라인에서 브레이크가 걸리실텐데요,
이때 하단 console 창에 아래와 같이 ```isValid = true```를 입력해보겠습니다.

![콘솔입력](./images/콘솔입력.png)

그리고 브레이크를 넘어가겠습니다.

![브레이크 포인트 넘어가기](./images/브레이크포인트넘어가기.png)

자 이렇게 하면 어떤 결과가 발생할까요?

![오류 발생](./images/오류발생.png)

ajax의 error 실행 코드인 ```alert("오류발생")``` 코드가 실행되었습니다.
네트워크 탭을 통해 어떻게 진행되었는지 자세히 보겠습니다.

![잘못된 전송](./images/잘못된전송.png)

500라인과 함께 비어있는 값들이 Ajax로 요청된 것을 확인할 수 있습니다.
프론트의 검증 코드를 브라우저의 개발자도구에서 값 변조를 통해 회피한 것입니다.
추가로 전송 전 데이터도 얼마든지 변조할 수 있습니다.

![마음껏 변조](./images/마음껏변조.png)

똑같이 브레이크를 건 뒤, 마음껏 원하는대로 console 창에서 값을 변경합니다.
이렇게 하면

![마음껏 변조 오류](./images/마음껏변조오류.png)

변조된 값 그대로 서버에 요청하게 됩니다.

즉, **프론트의 검증코드는 언제든지 회피할 수 있습니다**.
그래서 프론트의 검증코드는 사실상 의미가 없습니다.

### 해결

크게보면 2가지가 있습니다.

* 프론트와 백엔드 양쪽에 모두 검증 코드를 작성한다.
* 백엔드에 검증 코드를 작성 후, 백엔드 결과에 따라 프론트는 메세지만 노출한다.

1번째 방식은 단순하게 양쪽에 전부 검증 코드를 작성하기 때문에 귀찮을 뿐이지, 어렵지는 않다고 생각합니다.
2번째 방식은 백엔드에서 검증 후, 오류 혹은 결과값을 통일시켜 프론트에서 처리하도록 하는 방식인데, 이전에 관련해서 포스팅을 하였으니 참고 부탁드립니다!

* [Spring Validation 공통모듈 만들기](http://jojoldu.tistory.com/129)

### 후기

굉장히 짧은 내용이고 간단한 내용이라 굳이 포스팅해야하나? 고민했었는데 예전에 제가 선임 개발자 분들이 얘기만 하셨을때 당장 이해가 안됐던 기억이나서 작성하게 되었습니다.
혹시나 도움이 되셨다면 정말 다행일것 같습니다.
부족한 내용이지만 끝까지 읽어주셔서 감사합니다!
31 changes: 31 additions & 0 deletions front-validate-problem/build.gradle
@@ -0,0 +1,31 @@
buildscript {
ext {
springBootVersion = '1.5.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
mavenCentral()
}


dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Binary file not shown.
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip
172 changes: 172 additions & 0 deletions front-validate-problem/gradlew
@@ -0,0 +1,172 @@
#!/usr/bin/env sh

##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn ( ) {
echo "$*"
}

die ( ) {
echo
echo "$*"
echo
exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option

if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi

# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi

exec "$JAVACMD" "$@"

0 comments on commit 7026cc4

Please sign in to comment.