├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── rest │ │ └── oauth2 │ │ ├── Oauth2Application.java │ │ ├── config │ │ ├── CustomAuthenticationProvider.java │ │ ├── Oauth2AuthorizationConfig.java │ │ ├── SecurityConfig.java │ │ └── WebMvcConfig.java │ │ ├── controller │ │ └── common │ │ │ └── Oauth2Controller.java │ │ ├── entity │ │ └── User.java │ │ ├── model │ │ └── oauth2 │ │ │ └── OAuthToken.java │ │ ├── repo │ │ └── UserJpaRepo.java │ │ └── service │ │ └── security │ │ └── CustomUserDetailService.java └── resources │ ├── application.yml │ ├── oauth2jwt.jks │ └── schema.sql └── test └── java └── com └── rest └── oauth2 ├── Oauth2ApplicationTests.java ├── encoding └── EncodingTest.java └── repo └── UserJpaRepoTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | /build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | /out/ 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | 29 | ### VS Code ### 30 | .vscode/ 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Java_8](https://img.shields.io/badge/java-v1.8-red?logo=java) 2 | ![Spring_Boot](https://img.shields.io/badge/Spring_Boot-v2.1.4-green.svg?logo=spring) 3 | ![Spring_Cloud_Security](https://img.shields.io/badge/Spring_Cloud_Security-v2.1.2-green.svg?logo=spring) 4 | ![Spring_Cloud_Security](https://img.shields.io/badge/Spring_Cloud_Oauth2-v2.1.2-green.svg?logo=spring) 5 | ![GitHub stars](https://img.shields.io/github/stars/codej99/SpringOauth2AuthorizationServer?style=social) 6 | 7 | # Oauth2 AuthorizationServer(인증서버) 구축 8 | 9 | ### 0. 개요 10 | - SpringBoot에서 제공하는 Oauth2 인증서버 구축 방법에 대한 실습 11 | - daddyprogrammer.org에서 연재 및 소스 Github 등록 12 | - https://daddyprogrammer.org/post/series/spring-boot-oauth2/ 13 | 14 | ### 1. 실습 환경 15 | - Java 8~11 16 | - SpringBoot 2.x 17 | - Spring Oauth2 18 | - JPA, H2 19 | - Intellij Community 20 | 21 | ### 2. 실습 내용 22 | - Spring Boot Oauth2 – AuthorizationServer 23 | - Document 24 | - https://daddyprogrammer.org/post/1239/spring-oauth-authorizationserver/ 25 | - Git 26 | - https://github.com/codej99/SpringOauth2AuthorizationServer 27 | - Spring Boot Oauth2 – AuthorizationServer : DB처리, JWT 토큰 방식 적용 28 | - Document 29 | - https://daddyprogrammer.org/post/1287/spring-oauth2-authorizationserver-database/ 30 | - Git 31 | - https://github.com/codej99/SpringOauth2AuthorizationServer 32 | 33 | ### Oauth2 Authorize Code 실습 34 | - http://localhost:8081/oauth/authorize?client_id=testClientId&redirect_uri=http://localhost:8081/oauth2/callback&response_type=code&scope=read 35 | 36 | ### 토큰 발급 37 | curl -X POST 38 | 'http://localhost:8080/oauth/token' 39 | -H 'Authorization:Basic dGVzdENsaWVudElkOnRlc3RTZWNyZXQ=' 40 | -d 'grant_type=authorization_code' 41 | -d 'code=9THJxB' 42 | -d 'redirect_uri=http://localhost:8080/oauth2/callback' 43 | 44 | ### 클라이언트 등록 45 | insert into oauth_client_details(client_id, resource_ids,client_secret,scope,authorized_grant_types,web_server_redirect_uri,authorities,access_token_validity,refresh_token_validity,additional_information,autoapprove) values('testClientId',null,'{bcrypt}$2a$10$H2oQgFY7qCRHWqkvAV4P6ONy2v74wfr3fQv.xERw3BJYSqh/Gcgrq','read,write','authorization_code,refresh_token','http://localhost:8081/oauth2/callback','ROLE_USER',36000,50000,null,null); 46 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.1.4.RELEASE' 3 | id 'java' 4 | } 5 | 6 | apply plugin: 'io.spring.dependency-management' 7 | 8 | group = 'com.rest' 9 | version = '0.0.1-SNAPSHOT' 10 | sourceCompatibility = '1.8' 11 | 12 | configurations { 13 | compileOnly { 14 | extendsFrom annotationProcessor 15 | } 16 | } 17 | 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | dependencies { 23 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 24 | implementation 'org.springframework.boot:spring-boot-starter-freemarker' 25 | implementation 'org.springframework.boot:spring-boot-starter-web' 26 | implementation 'org.springframework.cloud:spring-cloud-starter-security:2.1.2.RELEASE' 27 | implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.1.2.RELEASE' 28 | implementation 'com.google.code.gson:gson' 29 | compileOnly 'org.projectlombok:lombok' 30 | testCompileOnly 'org.projectlombok:lombok' 31 | testAnnotationProcessor 'org.projectlombok:lombok' 32 | runtimeOnly 'com.h2database:h2' 33 | runtimeOnly 'mysql:mysql-connector-java' 34 | annotationProcessor 'org.projectlombok:lombok' 35 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 36 | } 37 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codej99/SpringOauth2AuthorizationServer/2d2777d3462406c4ee20784df8455360e08fad0c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 23 18:15:04 KST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | rootProject.name = 'oauth2' 7 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/Oauth2Application.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Oauth2Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Oauth2Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/config/CustomAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.config; 2 | 3 | import com.rest.oauth2.entity.User; 4 | import com.rest.oauth2.repo.UserJpaRepo; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.authentication.AuthenticationProvider; 7 | import org.springframework.security.authentication.BadCredentialsException; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.stereotype.Component; 13 | 14 | @RequiredArgsConstructor 15 | @Component 16 | public class CustomAuthenticationProvider implements AuthenticationProvider { 17 | 18 | private final PasswordEncoder passwordEncoder; 19 | private final UserJpaRepo userJpaRepo; 20 | 21 | @Override 22 | public Authentication authenticate(Authentication authentication) { 23 | 24 | String name = authentication.getName(); 25 | String password = authentication.getCredentials().toString(); 26 | 27 | User user = userJpaRepo.findByUid(name).orElseThrow(() -> new UsernameNotFoundException("user is not exists")); 28 | 29 | if (!passwordEncoder.matches(password, user.getPassword())) 30 | throw new BadCredentialsException("password is not valid"); 31 | 32 | return new UsernamePasswordAuthenticationToken(name, password, user.getAuthorities()); 33 | } 34 | 35 | @Override 36 | public boolean supports(Class authentication) { 37 | return authentication.equals( 38 | UsernamePasswordAuthenticationToken.class); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/config/Oauth2AuthorizationConfig.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.config; 2 | 3 | 4 | import com.rest.oauth2.service.security.CustomUserDetailService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.io.FileSystemResource; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 13 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 14 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 15 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 16 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 17 | import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; 18 | 19 | import javax.sql.DataSource; 20 | 21 | /** 22 | * id/password 기반 Oauth2 인증을 담당하는 서버 23 | * 다음 endpont가 자동 생성 된다. 24 | * /oauth/authorize 25 | * /oauth/token 26 | */ 27 | @RequiredArgsConstructor 28 | @Configuration 29 | @EnableAuthorizationServer 30 | public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter { 31 | 32 | private final PasswordEncoder passwordEncoder; 33 | private final DataSource dataSource; 34 | private final CustomUserDetailService userDetailService; 35 | 36 | @Value("${security.oauth2.jwt.signkey}") 37 | private String signKey; 38 | 39 | @Override 40 | public void configure(AuthorizationServerSecurityConfigurer security) { 41 | security.tokenKeyAccess("permitAll()") 42 | .checkTokenAccess("isAuthenticated()") //allow check token 43 | .allowFormAuthenticationForClients(); 44 | } 45 | 46 | /** 47 | * 클라이언트 정보 주입 방식을 jdbcdetail로 변경 48 | */ 49 | @Override 50 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 51 | clients.jdbc(dataSource).passwordEncoder(passwordEncoder); 52 | } 53 | 54 | /** 55 | * 토큰 정보를 DB를 통해 관리한다. 56 | */ 57 | // @Override 58 | // public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 59 | // endpoints.tokenStore(new JdbcTokenStore(dataSource)).userDetailsService(userDetailService); 60 | // } 61 | 62 | /** 63 | * 토큰 발급 방식을 JWT 토큰 방식으로 변경한다. 이렇게 하면 토큰 저장하는 DB Table은 필요가 없다. 64 | */ 65 | @Override 66 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 67 | super.configure(endpoints); 68 | endpoints.accessTokenConverter(jwtAccessTokenConverter()).userDetailsService(userDetailService); 69 | } 70 | 71 | /** 72 | * jwt converter - signKey 공유 방식 73 | */ 74 | // @Bean 75 | // public JwtAccessTokenConverter jwtAccessTokenConverter() { 76 | // JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); 77 | // converter.setSigningKey(signKey); 78 | // return converter; 79 | // } 80 | 81 | /** 82 | * jwt converter - 비대칭 키 sign 83 | */ 84 | @Bean 85 | public JwtAccessTokenConverter jwtAccessTokenConverter() { 86 | KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new FileSystemResource("src/main/resources/oauth2jwt.jks"), "oauth2jwtpass".toCharArray()); 87 | JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); 88 | converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2jwt")); 89 | return converter; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 | 9 | @RequiredArgsConstructor 10 | @Configuration 11 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 12 | 13 | private final CustomAuthenticationProvider authenticationProvider; 14 | 15 | @Override 16 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 17 | auth.authenticationProvider(authenticationProvider); 18 | } 19 | 20 | @Override 21 | protected void configure(HttpSecurity security) throws Exception { 22 | security 23 | .csrf().disable() 24 | .headers().frameOptions().disable() 25 | .and() 26 | .authorizeRequests().antMatchers("/oauth/**", "/oauth/token", "/oauth2/**", "/h2-console/*").permitAll() 27 | .and() 28 | .formLogin().and() 29 | .httpBasic(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.factory.PasswordEncoderFactories; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | import org.springframework.web.client.RestTemplate; 8 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | @Configuration 12 | public class WebMvcConfig implements WebMvcConfigurer { 13 | 14 | private static final long MAX_AGE_SECONDS = 3600; 15 | 16 | @Override 17 | public void addCorsMappings(CorsRegistry registry) { 18 | registry.addMapping("/**") 19 | .allowedOrigins("*") 20 | .allowedMethods("GET", "POST", "PUT", "DELETE") 21 | .allowedHeaders("*") 22 | .allowCredentials(true) 23 | .maxAge(MAX_AGE_SECONDS); 24 | } 25 | 26 | @Bean 27 | public RestTemplate getRestTemplate() { 28 | return new RestTemplate(); 29 | } 30 | 31 | @Bean 32 | public PasswordEncoder passwordEncoder() { 33 | return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/controller/common/Oauth2Controller.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.controller.common; 2 | 3 | import com.google.gson.Gson; 4 | import com.rest.oauth2.model.oauth2.OAuthToken; 5 | import lombok.RequiredArgsConstructor; 6 | import org.apache.commons.codec.binary.Base64; 7 | import org.springframework.http.*; 8 | import org.springframework.util.LinkedMultiValueMap; 9 | import org.springframework.util.MultiValueMap; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.client.RestTemplate; 15 | 16 | @RequiredArgsConstructor 17 | @RestController 18 | @RequestMapping("/oauth2") 19 | public class Oauth2Controller { 20 | 21 | private final Gson gson; 22 | private final RestTemplate restTemplate; 23 | 24 | @GetMapping(value = "/callback") 25 | public OAuthToken callbackSocial(@RequestParam String code) { 26 | 27 | String credentials = "testClientId:testSecret"; 28 | String encodedCredentials = new String(Base64.encodeBase64(credentials.getBytes())); 29 | 30 | HttpHeaders headers = new HttpHeaders(); 31 | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 32 | headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); 33 | headers.add("Authorization", "Basic " + encodedCredentials); 34 | 35 | MultiValueMap params = new LinkedMultiValueMap<>(); 36 | params.add("code", code); 37 | params.add("grant_type", "authorization_code"); 38 | params.add("redirect_uri", "http://localhost:8081/oauth2/callback"); 39 | HttpEntity> request = new HttpEntity<>(params, headers); 40 | ResponseEntity response = restTemplate.postForEntity("http://localhost:8081/oauth/token", request, String.class); 41 | if (response.getStatusCode() == HttpStatus.OK) { 42 | return gson.fromJson(response.getBody(), OAuthToken.class); 43 | } 44 | return null; 45 | } 46 | 47 | @GetMapping(value = "/token/refresh") 48 | public OAuthToken refreshToken(@RequestParam String refreshToken) { 49 | 50 | String credentials = "testClientId:testSecret"; 51 | String encodedCredentials = new String(Base64.encodeBase64(credentials.getBytes())); 52 | 53 | HttpHeaders headers = new HttpHeaders(); 54 | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 55 | headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); 56 | headers.add("Authorization", "Basic " + encodedCredentials); 57 | 58 | MultiValueMap params = new LinkedMultiValueMap<>(); 59 | params.add("refresh_token", refreshToken); 60 | params.add("grant_type", "refresh_token"); 61 | HttpEntity> request = new HttpEntity<>(params, headers); 62 | ResponseEntity response = restTemplate.postForEntity("http://localhost:8081/oauth/token", request, String.class); 63 | if (response.getStatusCode() == HttpStatus.OK) { 64 | return gson.fromJson(response.getBody(), OAuthToken.class); 65 | } 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.security.core.GrantedAuthority; 9 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | 12 | import javax.persistence.*; 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | @Builder // builder를 사용할수 있게 합니다. 19 | @Entity // jpa entity임을 알립니다. 20 | @Getter // user 필드값의 getter를 자동으로 생성합니다. 21 | @NoArgsConstructor // 인자없는 생성자를 자동으로 생성합니다. 22 | @AllArgsConstructor // 인자를 모두 갖춘 생성자를 자동으로 생성합니다. 23 | @Table(name = "user") // 'user' 테이블과 매핑됨을 명시 24 | public class User implements UserDetails { 25 | @Id // pk 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | private long msrl; 28 | @Column(nullable = false, unique = true, length = 50) 29 | private String uid; 30 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 31 | @Column(length = 100) 32 | private String password; 33 | @Column(nullable = false, length = 100) 34 | private String name; 35 | @Column(length = 100) 36 | private String provider; 37 | 38 | @ElementCollection(fetch = FetchType.EAGER) 39 | @Builder.Default 40 | private List roles = new ArrayList<>(); 41 | 42 | @Override 43 | public Collection getAuthorities() { 44 | return this.roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); 45 | } 46 | 47 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 48 | @Override 49 | public String getUsername() { 50 | return this.uid; 51 | } 52 | 53 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 54 | @Override 55 | public boolean isAccountNonExpired() { 56 | return true; 57 | } 58 | 59 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 60 | @Override 61 | public boolean isAccountNonLocked() { 62 | return true; 63 | } 64 | 65 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 66 | @Override 67 | public boolean isCredentialsNonExpired() { 68 | return true; 69 | } 70 | 71 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 72 | @Override 73 | public boolean isEnabled() { 74 | return true; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/model/oauth2/OAuthToken.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.model.oauth2; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class OAuthToken { 9 | private String access_token; 10 | private String token_type; 11 | private String refresh_token; 12 | private long expires_in; 13 | private String scope; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/repo/UserJpaRepo.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.repo; 2 | 3 | import com.rest.oauth2.entity.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserJpaRepo extends JpaRepository { 9 | Optional findByUid(String email); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/rest/oauth2/service/security/CustomUserDetailService.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.service.security; 2 | 3 | import com.rest.oauth2.entity.User; 4 | import com.rest.oauth2.repo.UserJpaRepo; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.authentication.AccountStatusUserDetailsChecker; 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 | import org.springframework.stereotype.Service; 11 | 12 | @RequiredArgsConstructor 13 | @Service 14 | public class CustomUserDetailService implements UserDetailsService { 15 | 16 | private final UserJpaRepo userJpaRepo; 17 | private final AccountStatusUserDetailsChecker detailsChecker = new AccountStatusUserDetailsChecker(); 18 | 19 | @Override 20 | public UserDetails loadUserByUsername(String name) { 21 | User user = userJpaRepo.findByUid(name).orElseThrow(() -> new UsernameNotFoundException("user is not exists")); 22 | detailsChecker.check(user); 23 | return user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | 4 | spring: 5 | h2: 6 | console: 7 | enabled: true 8 | settings: 9 | web-allow-others: true 10 | datasource: 11 | url: jdbc:h2:tcp://localhost/~/test 12 | driver-class-name: org.h2.Driver 13 | username: sa 14 | jpa: 15 | database-platform: org.hibernate.dialect.H2Dialect 16 | properties.hibernate.hbm2ddl.auto: update 17 | showSql: true 18 | 19 | security: 20 | oauth2: 21 | jwt: 22 | signkey: 123@#$ -------------------------------------------------------------------------------- /src/main/resources/oauth2jwt.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codej99/SpringOauth2AuthorizationServer/2d2777d3462406c4ee20784df8455360e08fad0c/src/main/resources/oauth2jwt.jks -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | -- used in tests that use HSQL 2 | create table IF NOT EXISTS oauth_client_details ( 3 | client_id VARCHAR(256) PRIMARY KEY, 4 | resource_ids VARCHAR(256), 5 | client_secret VARCHAR(256), 6 | scope VARCHAR(256), 7 | authorized_grant_types VARCHAR(256), 8 | web_server_redirect_uri VARCHAR(256), 9 | authorities VARCHAR(256), 10 | access_token_validity INTEGER, 11 | refresh_token_validity INTEGER, 12 | additional_information VARCHAR(4096), 13 | autoapprove VARCHAR(256) 14 | ); 15 | 16 | create table IF NOT EXISTS oauth_client_token ( 17 | token_id VARCHAR(256), 18 | token LONGVARBINARY, 19 | authentication_id VARCHAR(256) PRIMARY KEY, 20 | user_name VARCHAR(256), 21 | client_id VARCHAR(256) 22 | ); 23 | 24 | create table IF NOT EXISTS oauth_access_token ( 25 | token_id VARCHAR(256), 26 | token LONGVARBINARY, 27 | authentication_id VARCHAR(256) PRIMARY KEY, 28 | user_name VARCHAR(256), 29 | client_id VARCHAR(256), 30 | authentication LONGVARBINARY, 31 | refresh_token VARCHAR(256) 32 | ); 33 | 34 | create table IF NOT EXISTS oauth_refresh_token ( 35 | token_id VARCHAR(256), 36 | token LONGVARBINARY, 37 | authentication LONGVARBINARY 38 | ); 39 | 40 | create table IF NOT EXISTS oauth_code ( 41 | code VARCHAR(256), authentication LONGVARBINARY 42 | ); 43 | 44 | create table IF NOT EXISTS oauth_approvals ( 45 | userId VARCHAR(256), 46 | clientId VARCHAR(256), 47 | scope VARCHAR(256), 48 | status VARCHAR(10), 49 | expiresAt TIMESTAMP, 50 | lastModifiedAt TIMESTAMP 51 | ); 52 | -------------------------------------------------------------------------------- /src/test/java/com/rest/oauth2/Oauth2ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.junit4.SpringRunner; 8 | 9 | @Slf4j 10 | @RunWith(SpringRunner.class) 11 | @SpringBootTest 12 | public class Oauth2ApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/rest/oauth2/encoding/EncodingTest.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.encoding; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | 10 | @RunWith(SpringRunner.class) 11 | @SpringBootTest 12 | public class EncodingTest { 13 | 14 | @Autowired 15 | private PasswordEncoder passwordEncoder; 16 | 17 | @Test 18 | public void encodeTest() { 19 | System.out.printf("testSecret : %s", passwordEncoder.encode("testSecret")); 20 | System.out.println(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/rest/oauth2/repo/UserJpaRepoTest.java: -------------------------------------------------------------------------------- 1 | package com.rest.oauth2.repo; 2 | 3 | import com.rest.oauth2.entity.User; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | 12 | import java.util.Collections; 13 | 14 | @Slf4j 15 | @RunWith(SpringRunner.class) 16 | @SpringBootTest 17 | public class UserJpaRepoTest { 18 | @Autowired 19 | private UserJpaRepo userJpaRepo; 20 | 21 | @Autowired 22 | private PasswordEncoder passwordEncoder; 23 | 24 | @Test 25 | public void insert() { 26 | // given 27 | userJpaRepo.save(User.builder() 28 | .uid("happydaddy@gmail.com") 29 | .password(passwordEncoder.encode("1234")) 30 | .name("happydaddy") 31 | .roles(Collections.singletonList("ROLE_USER")) 32 | .build()); 33 | } 34 | } --------------------------------------------------------------------------------