├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml ├── readme.md └── src ├── main ├── java │ └── com │ │ └── gnu │ │ └── AuthServer │ │ ├── AuthServerApplication.java │ │ ├── config │ │ ├── AuthServerConfig.java │ │ ├── AuthServerMethodSecurityConfig.java │ │ └── AuthServerWebSecurityConfig.java │ │ ├── controller │ │ └── TestController.java │ │ ├── entity │ │ ├── AuthoritiesEntity.java │ │ └── UserEntity.java │ │ ├── filter │ │ └── AuthInnerFilter.java │ │ ├── method │ │ ├── AuthServerMethodSecurityExpression.java │ │ ├── AuthServerMethodSecurityExpressionHandler.java │ │ └── AuthServerSecurityExpressionRoot.java │ │ ├── repository │ │ └── UserRepository.java │ │ ├── service │ │ ├── AuthClientDetailsService.java │ │ ├── AuthTokenService.java │ │ ├── AuthUserDetails.java │ │ ├── AuthUserDetailsService.java │ │ ├── TestService.java │ │ └── TestServiceImpl.java │ │ └── utils │ │ ├── CustomChecker.java │ │ └── GrantTypes.java └── resources │ ├── application.properties │ └── security.properties └── test └── java └── com └── gnu └── AuthServer ├── AuthServerApplicationTests.java └── dto └── AccessToken.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnu-gnu/spring-boot-oauth-authserver/1203c58896a4692448ae3dd066db92294662519c/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip 2 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.gnu 8 | AuthServer 9 | 0.0.1-SNAPSHOT 10 | jar 11 | 12 | AuthServer 13 | Demo project for Spring Boot 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-parent 18 | 1.5.10.RELEASE 19 | 20 | 21 | 22 | 23 | UTF-8 24 | UTF-8 25 | 1.8 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-actuator 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-aop 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-security 40 | 41 | 42 | org.springframework.security.oauth 43 | spring-security-oauth2 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-thymeleaf 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-web 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-devtools 56 | runtime 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-data-jpa 61 | 62 | 63 | org.hsqldb 64 | hsqldb 65 | runtime 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-data-redis 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-starter-test 74 | test 75 | 76 | 77 | org.springframework.security 78 | spring-security-test 79 | test 80 | 81 | 82 | 83 | 84 | 85 | 86 | org.springframework.boot 87 | spring-boot-maven-plugin 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-surefire-plugin 92 | 93 | ${argLine} 94 | 95 | 96 | 97 | org.jacoco 98 | jacoco-maven-plugin 99 | 0.8.1 100 | 101 | ${project.build.directory}/jacoco.exec 102 | ${project.build.directory}/jacoco-output 103 | **/*entity.* 104 | 105 | 106 | 107 | 108 | prepare-agent 109 | 110 | 111 | 112 | report 113 | prepare-package 114 | 115 | report 116 | 117 | 118 | 119 | post-unit-test 120 | test 121 | 122 | report 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Spring boot OAuth Authorization server example 2 | 3 | 1. authorization code, refresh token, password grant에 대해서 token 발급 기능이 구현되어 있습니다. 4 | 1. request token 요청 주소는 http://localhost:9099/apps/oauth/authorize 이며 access token 요청 주소는 http://localhost:9099/apps/oauth/token 입니다. 5 | 1. redirect_uri는 AuthClientDetailsService.java 를 참조하여 수정 가능합니다. POSTMAN을 통한 OAUTH 테스트를 권장합니다. 6 | 1. 각 사용자별로 추가 정보가 다르게 설정되어 있습니다. 엔드포인트의 인증을 위해 사용할 username과 password는 gnu/pass, noh/pass, jee/pass 입니다. 7 | 1. 전체 flow에 대한 단위테스트는 /test/java/AuthServerApplicationTests.java 에 구현되어 있습니다. 8 | 1. 기본적으로 in-memory store를 사용하도록 되어 있으나, VM args에 -Dspring.profiles.active=redis 를 넣어서 redis store를 사용할 수 있습니다. 접속 정보는 application.properties에 작성되어 있습니다. -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/AuthServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer; 2 | 3 | import javax.annotation.Resource; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.CommandLineRunner; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.boot.autoconfigure.SpringBootApplication; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Profile; 13 | import org.springframework.data.redis.connection.RedisConnectionFactory; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | import org.springframework.security.oauth2.provider.token.TokenStore; 16 | import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; 17 | import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; 18 | import org.springframework.security.web.FilterChainProxy; 19 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 20 | 21 | import com.gnu.AuthServer.entity.UserEntity; 22 | import com.gnu.AuthServer.repository.UserRepository; 23 | 24 | @SpringBootApplication 25 | public class AuthServerApplication extends WebMvcConfigurerAdapter implements CommandLineRunner{ 26 | Logger logger = LoggerFactory.getLogger(AuthServerApplication.class); 27 | @Autowired 28 | UserRepository userRepository; 29 | @Resource(name="springSecurityFilterChain") 30 | FilterChainProxy springSecurityFilterChain; 31 | 32 | public static void main(String[] args) { 33 | SpringApplication.run(AuthServerApplication.class, args); 34 | } 35 | 36 | @Bean 37 | @Profile("local") // 기본 active profile 은 application.properties에 local로 설정되어 있다. 38 | public TokenStore InMemoryTokenStore(){ 39 | logger.info("in-memory token store"); 40 | return new InMemoryTokenStore(); 41 | } 42 | 43 | @Bean 44 | @Profile("redis") // redis token store 설정을 위해서는 -Dspring.profiles.active=redis 가 필요 45 | public TokenStore tokenStore(RedisConnectionFactory factory){ 46 | logger.info("redis token store"); 47 | return new RedisTokenStore(factory); 48 | } 49 | 50 | 51 | /** 52 | * 초기 ID/PW 데이터 설정J 53 | */ 54 | @Override 55 | public void run(String... arg0) throws Exception { 56 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 57 | UserEntity userShim = new UserEntity("gnu", encoder.encode("pass")); 58 | userShim.createAuthority("read"); 59 | userShim.createAuthority("write"); 60 | userShim.createAuthority("delete"); 61 | userShim.createAuthority("update"); 62 | userShim.setLocale("ko"); 63 | userRepository.save(userShim); 64 | UserEntity userNoh = new UserEntity("noh", encoder.encode("pass")); 65 | userNoh.createAuthority("read"); 66 | userShim.setLocale("jp"); 67 | userRepository.save(userNoh); 68 | UserEntity userJee = new UserEntity("jee", encoder.encode("pass")); 69 | userNoh.createAuthority("read"); 70 | userNoh.createAuthority("write"); 71 | userNoh.createAuthority("update"); 72 | userShim.setLocale("en"); 73 | userRepository.save(userJee); 74 | springSecurityFilterChain.getFilterChains().forEach(value->{ 75 | value.getFilters().forEach(filter->logger.info(filter.toString())); 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/config/AuthServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.security.authentication.AuthenticationManager; 9 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 10 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 11 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 12 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 13 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 14 | import org.springframework.security.oauth2.provider.token.TokenStore; 15 | 16 | import com.gnu.AuthServer.filter.AuthInnerFilter; 17 | import com.gnu.AuthServer.service.AuthClientDetailsService; 18 | import com.gnu.AuthServer.service.AuthTokenService; 19 | 20 | /** 21 | * 22 | * OAuth 인가 서버 23 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 24 | * 25 | */ 26 | @Configuration 27 | @EnableAuthorizationServer 28 | public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { 29 | Logger logger = LoggerFactory.getLogger(AuthServerConfig.class); 30 | @Autowired 31 | private AuthenticationManager authenticationManager; 32 | @Autowired 33 | AuthClientDetailsService clientDetailsService; 34 | @Autowired 35 | AuthTokenService tokenService; 36 | @Autowired 37 | TokenStore tokenStore; 38 | 39 | 40 | /** 41 | * endpoint에 대한 설정을 담당하는 메소드 42 | * 기본 endpoint 43 | * 1) ~~/authorize -> request token을 받는다. 나중에 access token 발급에 쓰일 수 있다. 이 단계에서는 httpBasic의 인증에 설정 해 놓은 id, pw를 basic auth로 사용한다 44 | * 2) ~~/token_access -> protected resources에 엑세스하기 위한 access token을 발급한다. 이 단계에서는 client id, secret을 basic auth에 사용한다 (secret 생략 가능) 45 | */ 46 | @Override 47 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 48 | endpoints.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.OPTIONS); 49 | endpoints.authenticationManager(authenticationManager); 50 | endpoints.tokenServices(tokenService); 51 | /* 52 | * Approvalstore 객체를 선언하여 설정하거나, 직접 구현할 경우 허용에 관한 사항을 관리할 수 있다. 53 | * 아래의 주석처리된 내용이 없으며, auto approval 에 관한 사항이 없을 경우 로그인 후 매번 scope에 대한 approval을 요청한다 54 | * 기본 TokenApprovalStore를 사용하여도 Form 로그인 이후 매번 scope approval해야 하는 수고를 줄일 수 있다. 55 | * 현재 프로젝트는 application.properties에 auto approval 할 scopes를 관리하고 있음 56 | * 57 | TokenApprovalStore approvalStore = new TokenApprovalStore(); 58 | approvalStore.setTokenStore(tokenStore); 59 | endpoints.approvalStore(approvalStore); 60 | */ 61 | } 62 | /** 63 | * 보안에 관련된 설정 64 | * 권한, 접근제어등은 여기서 설정한다. 65 | * 66 | * 보안이 요구되는 endpoints (기본은 denyAll() 이므로 적절히 고쳐서 사용한다) 67 | * 1) ~~/check_token (resource server가 rest로 token의 검증을 요청할 때 사용하는 endpoint, checkTokenAcess 로 조절) 68 | * 2) ~~/token_key (JWT 사용시, 토큰 검증을 위한 공개키를 노출하는 endpoint, tokenKeyAccess로 조절) 69 | */ 70 | @Override 71 | public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 72 | security.addTokenEndpointAuthenticationFilter(new AuthInnerFilter()); // ~~/authorize 에 대한 필터 73 | security.allowFormAuthenticationForClients(); // 기본적으로 HTTP HEADER AUTH가 적용되므로, FORM 전송으로 AUTH하기 위해서 적용 74 | security.checkTokenAccess("permitAll()"); // ~~/check_token으로 remoteTokenService가 토큰의 해석을 의뢰할 경우, 해당 endpoint의 권한 설정(기본 denyAll()) 75 | security.accessDeniedHandler((request, response, exception) -> logger.error(exception.getMessage())); 76 | } 77 | /** 78 | * OAuth서버에 접근을 요청하는 Client에 관한 설정을 관리하기 위한 Configure 79 | * inMemory 나 jdbc 기반 Builder를 지원하므로 그것을 활용해도 되지만 별도의 Service를 구현 80 | */ 81 | @Override 82 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 83 | clients.withClientDetails(clientDetailsService); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/config/AuthServerMethodSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.config; 2 | 3 | import org.springframework.context.annotation.AdviceMode; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; 6 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 7 | import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; 8 | 9 | import com.gnu.AuthServer.method.AuthServerMethodSecurityExpressionHandler; 10 | 11 | @Configuration 12 | @EnableGlobalMethodSecurity(prePostEnabled=true,mode=AdviceMode.PROXY,proxyTargetClass=false) 13 | public class AuthServerMethodSecurityConfig extends GlobalMethodSecurityConfiguration { 14 | /** 15 | * 아래 부분을 주석처리하면 기본 핸들러를 사용하고, Bean 클래스의 메소드 표현식을 사용할 수 있다 16 | */ 17 | @Override 18 | protected MethodSecurityExpressionHandler createExpressionHandler() { 19 | return new AuthServerMethodSecurityExpressionHandler(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/config/AuthServerWebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.config; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.slf4j.Marker; 13 | import org.slf4j.MarkerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.context.annotation.Bean; 17 | import org.springframework.context.annotation.Configuration; 18 | import org.springframework.context.annotation.PropertySource; 19 | import org.springframework.security.authentication.AuthenticationManager; 20 | import org.springframework.security.authentication.AuthenticationProvider; 21 | import org.springframework.security.authentication.BadCredentialsException; 22 | import org.springframework.security.authentication.ProviderManager; 23 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 24 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 25 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 26 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 27 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 28 | import org.springframework.security.config.http.SessionCreationPolicy; 29 | import org.springframework.security.core.Authentication; 30 | import org.springframework.security.core.AuthenticationException; 31 | import org.springframework.security.core.GrantedAuthority; 32 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 33 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 34 | import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager; 35 | import org.springframework.util.StringUtils; 36 | import org.springframework.web.cors.CorsConfiguration; 37 | import org.springframework.web.cors.CorsConfigurationSource; 38 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 39 | 40 | import com.gnu.AuthServer.repository.UserRepository; 41 | import com.gnu.AuthServer.service.AuthUserDetails; 42 | import com.gnu.AuthServer.service.AuthUserDetailsService; 43 | /** 44 | * 45 | * 기본적인 Web Security를 설정하는 Bean 46 | * 웹 프로젝트의 Spring security 설정과 크게 다르지 않다. 47 | * Password Grant 기반의 인증을 수행할 경우 ClientDetailsService를 거치지 않고 이 서비스의 AuthenticationManager만으로 인증 과정이 수행되고 Token이 발급된다 48 | * 49 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 50 | * 51 | */ 52 | @Configuration 53 | @EnableWebSecurity 54 | @PropertySource("security.properties") 55 | public class AuthServerWebSecurityConfig extends WebSecurityConfigurerAdapter { 56 | Logger logger = LoggerFactory.getLogger(AuthServerWebSecurityConfig.class); 57 | final Marker REQUEST_MARKER = MarkerFactory.getMarker("HTTP_REQUEST"); 58 | @Autowired 59 | UserRepository userRepository; 60 | @Autowired 61 | AuthUserDetailsService userDetailsService; 62 | @Value("${securities.permitall}") 63 | private String permitAll; 64 | 65 | 66 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 67 | 68 | @Override 69 | @Bean 70 | protected AuthenticationManager authenticationManager() throws Exception { 71 | 72 | AuthenticationProvider provider = new DaoAuthenticationProvider() { 73 | @Override 74 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 75 | logger.info(authentication.getName() + "... auth start"); 76 | String username = authentication.getName(); 77 | CharSequence password = (CharSequence) authentication.getCredentials(); 78 | Map details = new HashMap<>(); 79 | AuthUserDetails user = (AuthUserDetails) userDetailsService.loadUserByUsername(username); 80 | @SuppressWarnings("unchecked") 81 | Set autho = (Set) user.getAuthorities(); 82 | autho.add(new SimpleGrantedAuthority("ACTUATOR")); 83 | if (!encoder.matches(password, user.getPassword())) { 84 | logger.info(authentication.getName() + "... bad credential"); 85 | throw new BadCredentialsException("bad credential"); 86 | } else { 87 | UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), password.toString(),autho); 88 | details.put("uname", user.getEntity().getUsername()); 89 | details.put("uid", user.getEntity().getId()); 90 | details.put("locale", user.getEntity().getLocale()); 91 | token.setDetails(details); 92 | return token; 93 | } 94 | } 95 | 96 | @Override 97 | public boolean supports(Class authentication) { 98 | return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); 99 | } 100 | }; 101 | List providers = new ArrayList(); 102 | providers.add(provider); 103 | return new ProviderManager(providers, new OAuth2AuthenticationManager()); 104 | } 105 | 106 | @Override 107 | protected void configure(HttpSecurity http) throws Exception { 108 | http.authorizeRequests().antMatchers(StringUtils.commaDelimitedListToStringArray(permitAll)).permitAll().anyRequest().authenticated().and().formLogin().and() 109 | .csrf().disable(); 110 | http.headers().frameOptions().disable(); // UI redressing attack을 방지하기 위해 X-Frame-Options를 검증하는 부분, 편의상 disable 111 | http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); 112 | http.cors(); 113 | } 114 | 115 | @Bean 116 | public CorsConfigurationSource corsConfigurationSource() { 117 | CorsConfiguration configuration = new CorsConfiguration(); 118 | configuration.setAllowedOrigins(Arrays.asList("*")); 119 | configuration.setAllowedMethods(Arrays.asList("GET","POST")); 120 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 121 | source.registerCorsConfiguration("/**", configuration); 122 | return source; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.controller; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.security.access.method.P; 8 | import org.springframework.security.access.prepost.PreAuthorize; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import com.gnu.AuthServer.service.TestService; 16 | 17 | @RestController 18 | public class TestController { 19 | @Autowired 20 | TestService service; 21 | Logger logger = LoggerFactory.getLogger(TestController.class); 22 | /** 23 | * AuthServerMethodSecurityConfig의 createExpressionHandler 메소드 override를 주석처리하고 기본 Handler를 타게 한 후 24 | * /open에 접속 해 보면 bean의 메소드 기반으로 타는 것을 확인 가능 25 | * @return 26 | */ 27 | @RequestMapping("/open") 28 | @PreAuthorize("@customChecker.isChecked(#auth)") 29 | public @ResponseBody String open(Authentication auth) { 30 | logger.info("/open is PermitAll"); 31 | return "open"; 32 | } 33 | /** 34 | * 이 endpoint는 websecurity에서 permitAll로 오픈되어 있지만, method security가 적용된 메소드 35 | * 현재 isOk(boolean bool)는 bool= 값으로 들어온 true/false 에 따라 인증 성공 / 실패를 보여준다. 36 | * @return 인증이 성공할 경우 hello? 라는 문자열 출력 37 | */ 38 | @RequestMapping("/isok") 39 | public @ResponseBody String isok(boolean bool) { 40 | return service.hello(bool); 41 | } 42 | /** 43 | * web security에서 인증을 요구하는 메소드 44 | * @param auth 45 | * @return 46 | */ 47 | @RequestMapping("/test") 48 | public @ResponseBody String test(Authentication auth) { 49 | return "hello?"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/entity/AuthoritiesEntity.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.entity; 2 | 3 | import javax.persistence.CascadeType; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.FetchType; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.ManyToOne; 12 | 13 | @Entity 14 | public class AuthoritiesEntity { 15 | @Id 16 | @GeneratedValue(strategy=GenerationType.AUTO) 17 | @Column(name="id") 18 | private Long id; 19 | @Column(name="role_name", nullable=false) 20 | private String roleName; 21 | /** 22 | * 회원(1) -> 권한(N) 및 권한(N) -> 회원(1)의 양방향 관계가 성립함을 보임 23 | * FetchType -> Lazy로딩 여부 결정 24 | * Cascade는 자세한 정보는 인터넷 참고 25 | * 26 | */ 27 | @ManyToOne(fetch=FetchType.EAGER,cascade=CascadeType.ALL) 28 | @JoinColumn(name="user_seq") 29 | private UserEntity userEntity; 30 | 31 | public AuthoritiesEntity() { 32 | } 33 | 34 | public AuthoritiesEntity(String roleName, UserEntity userEntity) { 35 | setRoleName(roleName); 36 | setUserEntity(userEntity); 37 | } 38 | 39 | public Long getId() { 40 | return id; 41 | } 42 | 43 | public void setId(Long id) { 44 | this.id = id; 45 | } 46 | 47 | public String getRoleName() { 48 | return roleName; 49 | } 50 | 51 | public void setRoleName(String roleName) { 52 | this.roleName = roleName; 53 | } 54 | 55 | public UserEntity getUserEntity() { 56 | return userEntity; 57 | } 58 | 59 | public void setUserEntity(UserEntity userEntity) { 60 | this.userEntity = userEntity; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return "AuthoritiesEntity [id=" + id + ", roleName=" + roleName+"]"; 66 | } 67 | 68 | 69 | 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/entity/UserEntity.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.entity; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import javax.persistence.CascadeType; 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.FetchType; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.GenerationType; 12 | import javax.persistence.Id; 13 | import javax.persistence.OneToMany; 14 | 15 | /** 16 | * 17 | * 회원정보 엔티티
18 | * 하이버네이트는 일반적으로 복합키를 허용하지 않으므로 Join은 tuple의 pk를 sequence로 하고, 외래키로 삼는다
19 | * (개선을 위해 @IdClass 나 @EmbeddedId 등을 참고 중) 20 | * 21 | * @author Geunwoo Shim(gflhsin@gmail.com) 22 | * 23 | */ 24 | @Entity 25 | public class UserEntity { 26 | @Id 27 | @GeneratedValue(strategy=GenerationType.AUTO) 28 | private Long id; 29 | 30 | @Column(name="user_name",unique=true) 31 | private String username; 32 | @Column(name="password") 33 | private String password; 34 | @Column(name="locale") 35 | private String locale; 36 | @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER, mappedBy="userEntity") // userEntity를 참고한다는 것을 나타냄. mappedBy는 Owner를 결정 37 | private List authorities = new ArrayList(); 38 | 39 | /** 40 | * 41 | * Join에 필요한 객체는 외부의 Helper 함수를 작성하는 것이 좋음 42 | * 43 | * @param roles 44 | * @return 45 | */ 46 | public boolean createAuthority(String... roles){ 47 | boolean bool = true; 48 | if(null == authorities){ 49 | authorities = new ArrayList(); 50 | } 51 | for(String role : roles){ 52 | bool &= authorities.add(new AuthoritiesEntity(role, this)); 53 | } 54 | return bool; 55 | } 56 | 57 | public UserEntity() { 58 | } 59 | 60 | 61 | 62 | public UserEntity(String username, String password) { 63 | this.username = username; 64 | this.password = password; 65 | } 66 | 67 | public Long getId() { 68 | return id; 69 | } 70 | 71 | public void setId(Long id) { 72 | this.id = id; 73 | } 74 | 75 | public String getUsername() { 76 | return username; 77 | } 78 | 79 | public void setUsername(String username) { 80 | this.username = username; 81 | } 82 | 83 | public String getPassword() { 84 | return password; 85 | } 86 | 87 | public void setPassword(String password) { 88 | this.password = password; 89 | } 90 | 91 | public List getAuthorities() { 92 | return authorities; 93 | } 94 | 95 | public void setAuthorities(List authorities) { 96 | this.authorities = authorities; 97 | } 98 | 99 | public String getLocale() { 100 | return locale; 101 | } 102 | 103 | public void setLocale(String locale) { 104 | this.locale = locale; 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return "UserEntity [id=" + id + ", username=" + username + ", password=" + password + ", locale=" + locale 110 | + ", authorities=" + authorities + "]"; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/filter/AuthInnerFilter.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.filter; 2 | 3 | import java.io.IOException; 4 | import java.util.Base64; 5 | import java.util.Enumeration; 6 | 7 | import javax.servlet.Filter; 8 | import javax.servlet.FilterChain; 9 | import javax.servlet.FilterConfig; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.ServletRequest; 12 | import javax.servlet.ServletResponse; 13 | import javax.servlet.http.HttpServletRequest; 14 | 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.slf4j.Marker; 18 | import org.slf4j.MarkerFactory; 19 | import org.springframework.core.Ordered; 20 | import org.springframework.core.annotation.Order; 21 | import org.springframework.stereotype.Component; 22 | /** 23 | * 24 | * Request를 기록하기 위한 Filter class 25 | * 26 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 27 | * 28 | */ 29 | @Component 30 | @Order(value=Ordered.HIGHEST_PRECEDENCE) 31 | public class AuthInnerFilter implements Filter { 32 | Logger logger = LoggerFactory.getLogger(AuthInnerFilter.class); 33 | final Marker REQUEST_MARKER = MarkerFactory.getMarker("HTTP_REQUEST"); 34 | @Override 35 | public void destroy() { 36 | // TODO Auto-generated method stub 37 | 38 | } 39 | 40 | @Override 41 | public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) 42 | throws IOException, ServletException { 43 | HttpServletRequest req = (HttpServletRequest)arg0; 44 | logger.info("START OF REQUEST -------------------------------"); 45 | logger.info("call for {}", req.getRequestURI()); 46 | logger.info("userprincipal : {}", req.getUserPrincipal()); 47 | Enumeration names = req.getHeaderNames(); 48 | logger.info("- headers"); 49 | String key = ""; 50 | while(names.hasMoreElements()){ 51 | key = names.nextElement(); 52 | if (key.startsWith("authorization")) { 53 | logger.info(REQUEST_MARKER, "{} : {}", key, new String(Base64.getDecoder().decode(req.getHeader(key).split(" ")[1]))); 54 | } else { 55 | logger.info(REQUEST_MARKER, "{} : {}", key, req.getHeader(key)); 56 | } 57 | } 58 | String paramKey = ""; 59 | logger.info("- params"); 60 | Enumeration params = req.getParameterNames(); 61 | while(params.hasMoreElements()){ 62 | paramKey = params.nextElement(); 63 | logger.info(REQUEST_MARKER, "{} : {}", paramKey, req.getParameter(paramKey)); 64 | } 65 | logger.info("------------------------------- END OF REQUEST"); 66 | arg2.doFilter(arg0, arg1); 67 | 68 | } 69 | 70 | @Override 71 | public void init(FilterConfig arg0) throws ServletException { 72 | // TODO Auto-generated method stub 73 | 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/method/AuthServerMethodSecurityExpression.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.method; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.core.Authentication; 6 | 7 | import com.gnu.AuthServer.config.AuthServerWebSecurityConfig; 8 | 9 | /** 10 | * 11 | * 이 클래스의 public boolean 메소드들이 MethodSecurityExpression 으로 쓰임(eg : PreAuthorize, PostAuthorize) 12 | * 13 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 14 | * 15 | */ 16 | public class AuthServerMethodSecurityExpression { 17 | 18 | private Authentication auth; 19 | private static final Logger logger = LoggerFactory.getLogger(AuthServerWebSecurityConfig.class); 20 | 21 | public AuthServerMethodSecurityExpression(Authentication auth) { 22 | this.auth = auth; 23 | } 24 | /** 25 | * #auth.isOk() expression을 호출할 경우 이 메소드를 call하게 된다. 이 메소드의 결과가 true, false 냐에 따라 인가 여부가 결정됨 26 | * @return 27 | */ 28 | public boolean isOk(boolean bool) { 29 | logger.info(auth.toString()); 30 | return bool; 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/method/AuthServerMethodSecurityExpressionHandler.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.method; 2 | 3 | import org.aopalliance.intercept.MethodInvocation; 4 | import org.springframework.expression.spel.support.StandardEvaluationContext; 5 | import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; 6 | import org.springframework.security.core.Authentication; 7 | /** 8 | * 9 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 10 | * 11 | */ 12 | public class AuthServerMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler{ 13 | 14 | 15 | 16 | @Override 17 | public StandardEvaluationContext createEvaluationContextInternal(Authentication auth, MethodInvocation mi) { 18 | System.out.println(mi.getMethod().getName()); 19 | AuthServerSecurityExpressionRoot root = new AuthServerSecurityExpressionRoot(auth); 20 | root.setTrustResolver(getTrustResolver()); 21 | root.setPermissionEvaluator(getPermissionEvaluator()); 22 | root.setRoleHierarchy(getRoleHierarchy()); 23 | StandardEvaluationContext sec = super.createEvaluationContextInternal(auth, mi); 24 | sec.setRootObject(root); 25 | sec.setVariable("auth", new AuthServerMethodSecurityExpression(auth)); 26 | return sec; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/method/AuthServerSecurityExpressionRoot.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.method; 2 | 3 | import org.springframework.security.access.expression.SecurityExpressionRoot; 4 | import org.springframework.security.core.Authentication; 5 | 6 | /** 7 | * 8 | * MethodSecurityExpressionRoot는 modifier가 왜 public이 아닌지 알 수가 없음. 9 | * 그냥 바로 써도 될 것 같은데 인스턴스 생성이 불가능하므로 SecurityExpressionRoot를 상속하여 구현 10 | * 아래에 위 클래스의 modifier 관련 이슈가 있음 11 | * @see https://github.com/spring-projects/spring-security/pull/4266 12 | * 13 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 14 | * 15 | */ 16 | public class AuthServerSecurityExpressionRoot extends SecurityExpressionRoot { 17 | public AuthServerSecurityExpressionRoot(Authentication authentication) { 18 | super(authentication); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.support.SimpleJpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import com.gnu.AuthServer.entity.UserEntity; 8 | 9 | /** 10 | * 11 | * User정보를 읽기 위한 Repository 12 | * Repository란 CRUD 작업 및 기타 필요한 작업에 대한 추상화를 제공해주는 인터페이스이다 13 | * 14 | * @see JpaRepository 15 | * @see SimpleJpaRepository 16 | * 17 | * @author Geunwoo Shim(gflhsin@gmail.com) 18 | * 19 | */ 20 | @Repository 21 | public interface UserRepository extends JpaRepository{ 22 | UserEntity findByUsername(String userName); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/service/AuthClientDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.service; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.security.core.authority.AuthorityUtils; 12 | import org.springframework.security.oauth2.provider.ClientDetails; 13 | import org.springframework.security.oauth2.provider.ClientDetailsService; 14 | import org.springframework.security.oauth2.provider.ClientRegistrationException; 15 | import org.springframework.security.oauth2.provider.client.BaseClientDetails; 16 | import org.springframework.security.oauth2.provider.token.TokenStore; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.util.StringUtils; 19 | 20 | import com.gnu.AuthServer.utils.GrantTypes; 21 | /** 22 | * 23 | * client에 관련된 기능을 처리하는 서비스 24 | * client_id 및 secret을 기반으로 토큰 발급에 권한 인증 처리 및 25 | * 콜백URL 체크, 허용할 scope 및 인층 처리 프로세스간 client에 부여할 authority등이 이 클래스에서 처리 된다. 26 | * 27 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 28 | * 29 | */ 30 | @Service 31 | public class AuthClientDetailsService implements ClientDetailsService { 32 | Logger logger = LoggerFactory.getLogger(AuthClientDetailsService.class); 33 | 34 | @Value("${authserver.auto.approval.scopes}") 35 | private String autoScopes; 36 | 37 | @Autowired 38 | TokenStore tokenStore; 39 | 40 | @Override 41 | public ClientDetails loadClientByClientId(String arg0) throws ClientRegistrationException { 42 | /* 43 | * token 요청시 입력하는 Callback url, client id 및 secret, scope 등은 이 메소드에서 작성하는 것과 일치하여야 한다. 44 | * ClientDetailsService 가 JdbcClientDetailsService 구현한다면 해당 클래스에는 Client 를 추가, 수정, 삭제할 수 있는 메소드가 구현되어 있다. 45 | * 해당 클래스를 구현할 경우 Client CRUD 에 에러가 발생하면 ClientRegistrationException을 발생시키도록 한다. 46 | */ 47 | logger.info("load client from \"{}\"", arg0); 48 | BaseClientDetails details = new BaseClientDetails(); 49 | Set set = new HashSet<>(); 50 | set.add("http://localhost:7077/resources/"); 51 | details.setClientId("gnu-gnu"); 52 | details.setAuthorizedGrantTypes(Arrays.asList(GrantTypes.AUTHORIZATION_CODE, GrantTypes.PASSWORD, GrantTypes.REFRESH_TOKEN)); 53 | details.setRegisteredRedirectUri(set); 54 | details.setScope(StringUtils.commaDelimitedListToSet(autoScopes)); 55 | details.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(autoScopes)); 56 | return details; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/service/AuthTokenService.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.service; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.stream.Collectors; 6 | 7 | import javax.annotation.PostConstruct; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; 13 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 14 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 15 | import org.springframework.security.oauth2.provider.token.DefaultTokenServices; 16 | import org.springframework.security.oauth2.provider.token.TokenEnhancer; 17 | import org.springframework.security.oauth2.provider.token.TokenStore; 18 | import org.springframework.stereotype.Service; 19 | /** 20 | * 21 | * Access token 발급에 관한 서비스 22 | * Access token에 global로 기록될 사항들을 이 서비스에서 관리할 수 있다. 23 | * 24 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 25 | * 26 | */ 27 | @Service 28 | public class AuthTokenService extends DefaultTokenServices { 29 | Logger logger = LoggerFactory.getLogger(AuthTokenService.class); 30 | 31 | @Autowired 32 | TokenStore tokenStore; 33 | 34 | @PostConstruct 35 | public void init(){ 36 | this.setRefreshTokenValiditySeconds(86400); 37 | this.setAccessTokenValiditySeconds(1800); 38 | this.setSupportRefreshToken(true); 39 | this.setTokenStore(tokenStore); 40 | /* 41 | * Token 발급시 부가 정보등을 기록하기 위한 메소드 42 | */ 43 | this.setTokenEnhancer(new TokenEnhancer() { 44 | @SuppressWarnings("unchecked") 45 | @Override 46 | public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 47 | logger.info("token enhance"); 48 | Map details = new HashMap<>(); 49 | logger.info(authentication.getUserAuthentication().toString()); 50 | details.putAll((Map) authentication.getUserAuthentication().getDetails()); 51 | details.put("authorities", authentication.getAuthorities().stream().map(value->value.getAuthority()).distinct().collect(Collectors.toList())); 52 | ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(details); 53 | return accessToken; 54 | } 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/service/AuthUserDetails.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.service; 2 | 3 | import java.util.Collection; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | 11 | import com.gnu.AuthServer.entity.UserEntity; 12 | /** 13 | * 14 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 15 | * 16 | */ 17 | public class AuthUserDetails implements UserDetails { 18 | private static final long serialVersionUID = 106806877220707136L; 19 | private UserEntity entity; 20 | 21 | public AuthUserDetails(UserEntity entity) { 22 | super(); 23 | this.entity = entity; 24 | } 25 | 26 | @Override 27 | public Collection getAuthorities() { 28 | Set authorities = new HashSet<>(); 29 | entity.getAuthorities().forEach(value -> { 30 | authorities.add(new SimpleGrantedAuthority(value.getRoleName())); 31 | }); 32 | return authorities; 33 | } 34 | 35 | public UserEntity getEntity() { 36 | return entity; 37 | } 38 | 39 | public void setEntity(UserEntity entity) { 40 | this.entity = entity; 41 | } 42 | 43 | @Override 44 | public String getPassword() { 45 | return entity.getPassword(); 46 | } 47 | 48 | @Override 49 | public String getUsername() { 50 | return entity.getUsername(); 51 | } 52 | 53 | @Override 54 | public boolean isAccountNonExpired() { 55 | return true; 56 | } 57 | 58 | @Override 59 | public boolean isAccountNonLocked() { 60 | // TODO Auto-generated method stub 61 | return true; 62 | } 63 | 64 | @Override 65 | public boolean isCredentialsNonExpired() { 66 | // TODO Auto-generated method stub 67 | return true; 68 | } 69 | 70 | @Override 71 | public boolean isEnabled() { 72 | // TODO Auto-generated method stub 73 | return true; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/service/AuthUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | 11 | import com.gnu.AuthServer.entity.UserEntity; 12 | import com.gnu.AuthServer.repository.UserRepository; 13 | /** 14 | * 15 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 16 | * 17 | */ 18 | @Configuration 19 | public class AuthUserDetailsService implements UserDetailsService{ 20 | Logger logger = LoggerFactory.getLogger(AuthUserDetailsService.class); 21 | 22 | @Autowired 23 | UserRepository userRepository; 24 | 25 | @Override 26 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 27 | 28 | logger.info(username+"... trying to login"); 29 | UserEntity findEntity = userRepository.findByUsername(username); 30 | if(null == findEntity){ 31 | logger.info(username+"... user not found"); 32 | throw new UsernameNotFoundException(username); 33 | } 34 | logger.info(username+"... login success"); 35 | AuthUserDetails details = new AuthUserDetails(findEntity); 36 | return details; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/service/TestService.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.service; 2 | 3 | import org.springframework.security.access.prepost.PreAuthorize; 4 | 5 | public interface TestService { 6 | @PreAuthorize("#auth.isOk(#bool)") 7 | String hello(boolean bool); 8 | 9 | } -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/service/TestServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | /** 5 | * 6 | * 서비스 레이어에 메소드 표현식 기반 인가 프로세스 적용 7 | * 인터페이스에 적용 해 두었기 때문에 서비스 레이어는 구현만 하면 됨 8 | * 9 | * @author gnu-gnu(geunwoo.j.shim@gmail.com) 10 | * 11 | */ 12 | @Service 13 | public class TestServiceImpl implements TestService { 14 | 15 | @Override 16 | public String hello(boolean bool) { 17 | return "Hello?"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/utils/CustomChecker.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.utils; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class CustomChecker { 8 | 9 | public boolean isChecked(Authentication auth) { 10 | System.out.println(auth); 11 | System.out.println("checked"); 12 | return true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gnu/AuthServer/utils/GrantTypes.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.utils; 2 | /** 3 | * 4 | * access_token을 발급하는데 필요한 grant type을 정의
5 | * 각 방식의 특징은 다음과 같다
6 | * PASSWORD : 사용자에게 ID와 PASSWORD를 받고 access_token을 발급한다
7 | * AUTHORIZATION_CODE : ~~/authorize에서 발급된 request_token(authorization_code)을 access_token으로 교환한다
8 | * CLIENT_CREDENTIALS : 신뢰하는 client에 부여된 client_credentials를 받아 access_token을 발급한다.
9 | * REFRESH_TOKEN : 만료가 임박한 access_token을 새로 갱신한다
10 | * 11 | * @author Geunwoo.Shim (gflhsin@gmail.com) 12 | */ 13 | public interface GrantTypes { 14 | String PASSWORD = "password"; 15 | String AUTHORIZATION_CODE = "authorization_code"; 16 | String CLIENT_CREDENTIALS = "client_credentials"; 17 | String REFRESH_TOKEN = "refresh_token"; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=9099 2 | server.context-path=/apps 3 | 4 | spring.redis.host=localhost 5 | spring.redis.port=6379 6 | 7 | spring.profiles.active=local 8 | 9 | #OAuth 10 | authserver.auto.approval.scopes=check,read -------------------------------------------------------------------------------- /src/main/resources/security.properties: -------------------------------------------------------------------------------- 1 | securities.permitall=/open,/login,/isok -------------------------------------------------------------------------------- /src/test/java/com/gnu/AuthServer/AuthServerApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; 5 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 8 | 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.security.authentication.AuthenticationManager; 19 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 20 | import org.springframework.security.core.Authentication; 21 | import org.springframework.security.web.FilterChainProxy; 22 | import org.springframework.test.context.junit4.SpringRunner; 23 | import org.springframework.test.web.servlet.MockMvc; 24 | import org.springframework.test.web.servlet.ResultActions; 25 | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; 26 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 27 | import org.springframework.util.LinkedMultiValueMap; 28 | import org.springframework.util.MultiValueMap; 29 | import org.springframework.web.context.WebApplicationContext; 30 | 31 | import com.fasterxml.jackson.databind.ObjectMapper; 32 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 33 | import com.gnu.AuthServer.dto.AccessToken; 34 | import com.gnu.AuthServer.utils.GrantTypes; 35 | 36 | @RunWith(SpringRunner.class) 37 | @SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT, classes=AuthServerApplication.class) 38 | public class AuthServerApplicationTests { 39 | private static final Logger LOG = LoggerFactory.getLogger(AuthServerApplicationTests.class); 40 | private static final String CLIENT_SECRET = "gnu-secret"; 41 | private static final String CLIENT_ID = "gnu-gnu"; 42 | private static final String CALLBACK_URL = "http://localhost:7077/resources/open/callback"; 43 | private static final String REQUEST_TOKEN_ENDPOINT = "/oauth/authorize"; 44 | private static final String ACCESS_TOKEN_ENDPOINT = "/oauth/token"; 45 | private static final String LOGIN_ENDPOINT = "/login"; 46 | 47 | 48 | @Autowired 49 | private WebApplicationContext ctx; 50 | 51 | @Autowired 52 | AuthenticationManager authenticationManager; 53 | @Autowired 54 | FilterChainProxy filterChainProxy; 55 | 56 | private MockMvc mvc; 57 | private String username; 58 | private String password; 59 | private Authentication auth; 60 | 61 | @Before 62 | public void init() throws Exception { 63 | LOG.info("----- unit test initialize"); 64 | username = "gnu"; 65 | password = "pass"; 66 | this.mvc = MockMvcBuilders.webAppContextSetup(ctx).addFilters(filterChainProxy).build(); 67 | auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); 68 | } 69 | 70 | @Test 71 | public void testGetToken() throws Exception { 72 | testFormLogin(); 73 | String requestToken = getRequestTokenForAuthorizationCode(); 74 | assertThat(requestToken).isNotEmpty(); 75 | AccessToken accessToken = getAccessTokenForAuthorizationCode(requestToken); 76 | assertThat(accessToken).isNotNull(); 77 | String refreshToken = accessToken.getRefreshToken(); 78 | assertThat(getAccessTokenByRefreshToken(refreshToken)).isNotNull(); 79 | assertThat(getAccessTokenForPassword()).isNotNull(); 80 | } 81 | /** 82 | * Form 기반 로그인이 성공하는지 테스트 83 | * 84 | * @throws Exception 85 | */ 86 | private void testFormLogin() throws Exception { 87 | LOG.info("----- form login test"); 88 | ResultActions result = mvc.perform(formLogin(LOGIN_ENDPOINT).user(username).password(password)); 89 | assertThat(result.andReturn().getResponse().getStatus()).isNotSameAs(500); 90 | } 91 | /** 92 | * 93 | * Access 토큰 교환을 위해 사용하는 공통 메소드 94 | * 95 | * @return Access_token 96 | * @throws Exception 97 | */ 98 | private AccessToken getAccessToken(MultiValueMap requestParam) throws Exception { 99 | /* 100 | * /oauth/token endpoint의 경우 ClientCredentialsTokenEndpointFilter가 등록되어 있음. 101 | * client_id, client_secret을 이용하여 Authentication을 수행하므로 username, password 기반 인증을 엔드포인트 인증을 위해 끼워넣을 경우 오히려 에러가 발생함 102 | * eg) request token 요청시에는 아래와 같이 .with(authentication(auth))를 삽입하여 Endpoint 인증에 Spring security의 AuthenticationManager를 사용하였으나 103 | * post(REQUEST_TOKEN_ENDPOINT).with(authentication(auth)).params(requestParam).contentType(MediaType.APPLICATION_FORM_URLENCODED).accept(MediaType.APPLICATION_JSON_UTF8).characterEncoding("UTF-8"); 104 | * access token 교환시 해당 인증을 삽입하면 client_id, secret 기반의 ClientCredentialsTokenEndpointFilter와 충돌을 일으켜 client_id를 AuthenticationManger에 등록된 id,pw로 인식하게 됨 105 | * 그러므로 Access token 교환을 위한 인증시에는 client_id, secret 기반의 인증을 사용하도록 해당 부분을 제거 106 | */ 107 | MockHttpServletRequestBuilder reqBuilder = post(ACCESS_TOKEN_ENDPOINT).params(requestParam).contentType(MediaType.APPLICATION_FORM_URLENCODED).accept(MediaType.APPLICATION_JSON_UTF8).characterEncoding("UTF-8"); 108 | ResultActions result = mvc.perform(reqBuilder).andExpect(status().isOk()); 109 | String responseBody = result.andReturn().getResponse().getContentAsString(); 110 | ObjectMapper mapper = new ObjectMapper(); 111 | mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); 112 | AccessToken accessToken = mapper.readValue(responseBody, AccessToken.class); 113 | LOG.info("raw response : {}", responseBody); 114 | LOG.info("Access token dto : {}", accessToken.toString()); 115 | return accessToken; 116 | } 117 | /** 118 | * authorization_code grant 에서 requestToken을 access token으로 교환하기 위한 메소드 119 | * 120 | * @param requestToken 121 | * @return 122 | * @throws Exception 123 | */ 124 | private AccessToken getAccessTokenForAuthorizationCode(String requestToken) throws Exception{ 125 | LOG.info("----- authorization_code access token test"); 126 | MultiValueMap requestParam = new LinkedMultiValueMap<>(); 127 | requestParam.add("grant_type", GrantTypes.AUTHORIZATION_CODE); 128 | requestParam.add("code", requestToken); 129 | requestParam.add("redirect_uri", CALLBACK_URL); 130 | requestParam.add("client_id", CLIENT_ID); 131 | // requestParam.add("client_secret", CLIENT_SECRET); 132 | return getAccessToken(requestParam); 133 | } 134 | /** 135 | * 136 | * refresh_token을 이용하여 access_token을 교환하는 메소드 137 | * access token이 유효하여야 한다. 138 | * auth server의 endpoint에 대한 security 인증은 client_id, client_password로 이루어진다. 이 부분이 없다면 401 Unauthorized 에러가 발생한다. 139 | * 140 | * @param refreshToken access_token 발급시 부여받은 refresh_token 141 | * @return 갱신된 access token 142 | * @throws Exception 143 | */ 144 | private AccessToken getAccessTokenByRefreshToken(String refreshToken) throws Exception { 145 | LOG.info("----- refresh token test"); 146 | MultiValueMap requestParam = new LinkedMultiValueMap<>(); 147 | requestParam.add("refresh_token", refreshToken); 148 | requestParam.add("grant_type", GrantTypes.REFRESH_TOKEN); 149 | requestParam.add("client_id", CLIENT_ID); 150 | // requestParam.add("client_secret", CLIENT_SECRET); 151 | return getAccessToken(requestParam); 152 | } 153 | /** 154 | * AuthenticationManager에 등록된 id와 password를 통해 access token을 부여하는 password grant 방식으로 access token을 받는다. 155 | * 이 방식은 third-party에게 제공하기는 대단히 부적합하기 때문에 유의해야 한다. 156 | * 157 | * @return access token 158 | * @throws Exception 159 | */ 160 | private AccessToken getAccessTokenForPassword() throws Exception { 161 | LOG.info("----- password grant access token test"); 162 | MultiValueMap requestParam = new LinkedMultiValueMap<>(); 163 | requestParam.add("grant_type", "password"); 164 | requestParam.add("client_id", CLIENT_ID); 165 | // requestParam.add("client_secret", CLIENT_SECRET); 166 | requestParam.add("scope", "read+check"); 167 | requestParam.add("username", username); 168 | requestParam.add("password", password); 169 | return getAccessToken(requestParam); 170 | } 171 | /** 172 | * 173 | * authorization_code grant_type에서 access_token 교환에 사용할 request_token을 받는다. 174 | * 이 flow는 일반적으로 client가 redirect_uri로 redirect하면서 해당 주소에 parameter로 code를 전달하는 방식이다. 175 | * 그러므로 단위테스트에서는 redirect 되는 주소의 queryString을 파싱한다. 176 | * 177 | * @return request_token 문자열, 이 request_token을 access_token 교환시 사용한다. 178 | * @throws Exception 179 | */ 180 | private String getRequestTokenForAuthorizationCode() throws Exception { 181 | LOG.info("----- request token test"); 182 | MultiValueMap requestParam = new LinkedMultiValueMap<>(); 183 | requestParam.add("response_type", "code"); 184 | requestParam.add("redirect_uri", CALLBACK_URL); 185 | requestParam.add("client_id", CLIENT_ID); 186 | // requestParam.add("client_secret", CLIENT_SECRET); 187 | requestParam.add("scope", "read+check"); 188 | MockHttpServletRequestBuilder reqBuilder = post(REQUEST_TOKEN_ENDPOINT).with(authentication(auth)).params(requestParam).contentType(MediaType.APPLICATION_FORM_URLENCODED).accept(MediaType.APPLICATION_JSON_UTF8).characterEncoding("UTF-8"); 189 | ResultActions result = mvc.perform(reqBuilder).andExpect(status().is3xxRedirection()); 190 | return result.andReturn().getResponse().getRedirectedUrl().split("\\?code=")[1]; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/test/java/com/gnu/AuthServer/dto/AccessToken.java: -------------------------------------------------------------------------------- 1 | package com.gnu.AuthServer.dto; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | public class AccessToken { 8 | // {"access_token":"c5352e5e-e451-4047-89dc-bb607827af23","token_type":"bearer","refresh_token":"f19c036b-43aa-4d2f-b774-8a55369a0575","expires_in":1799,"scope":"check read","uid":1,"uname":"gnu","locale":"ko","authorities":["read","ACTUATOR","update","write","delete"]} 9 | private String accessToken; 10 | private String tokenType; 11 | private String refreshToken; 12 | private int expiresIn; 13 | private List scope; 14 | private int uid; 15 | private String uname; 16 | private String locale; 17 | private Set authorities; 18 | public String getAccessToken() { 19 | return accessToken; 20 | } 21 | 22 | public void setAccessToken(String accessToken) { 23 | this.accessToken = accessToken; 24 | } 25 | public String getTokenType() { 26 | return tokenType; 27 | } 28 | public void setTokenType(String tokenType) { 29 | this.tokenType = tokenType; 30 | } 31 | public String getRefreshToken() { 32 | return refreshToken; 33 | } 34 | public void setRefreshToken(String refreshToken) { 35 | this.refreshToken = refreshToken; 36 | } 37 | public int getExpiresIn() { 38 | return expiresIn; 39 | } 40 | public void setExpiresIn(int expiresIn) { 41 | this.expiresIn = expiresIn; 42 | } 43 | public List getScope() { 44 | return scope; 45 | } 46 | public void setScope(String scope) { 47 | this.scope = Arrays.asList(scope.split(" ")); 48 | } 49 | public int getUid() { 50 | return uid; 51 | } 52 | public void setUid(int uid) { 53 | this.uid = uid; 54 | } 55 | public String getUname() { 56 | return uname; 57 | } 58 | public void setUname(String uname) { 59 | this.uname = uname; 60 | } 61 | public String getLocale() { 62 | return locale; 63 | } 64 | public void setLocale(String locale) { 65 | this.locale = locale; 66 | } 67 | public Set getAuthorities() { 68 | return authorities; 69 | } 70 | public void setAuthorities(Set authorities) { 71 | this.authorities = authorities; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "AccessToken [accessToken=" + accessToken + ", tokenType=" + tokenType + ", refreshToken=" + refreshToken 77 | + ", expiresIn=" + expiresIn + ", scope=" + scope + ", uid=" + uid + ", uname=" + uname + ", locale=" 78 | + locale + ", authorities=" + authorities + "]"; 79 | } 80 | 81 | 82 | } 83 | --------------------------------------------------------------------------------