├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── me │ │ └── silvernine │ │ └── tutorial │ │ ├── JwtTutorialApplication.java │ │ ├── config │ │ ├── CorsConfig.java │ │ └── SecurityConfig.java │ │ ├── controller │ │ ├── AuthController.java │ │ └── UserController.java │ │ ├── dto │ │ ├── AuthorityDto.java │ │ ├── ErrorDto.java │ │ ├── LoginDto.java │ │ ├── TokenDto.java │ │ └── UserDto.java │ │ ├── entity │ │ ├── Authority.java │ │ └── User.java │ │ ├── exception │ │ ├── DuplicateMemberException.java │ │ └── NotFoundMemberException.java │ │ ├── handler │ │ ├── MethodArgumentNotValidExceptionHandler.java │ │ └── RestResponseExceptionHandler.java │ │ ├── jwt │ │ ├── JwtAccessDeniedHandler.java │ │ ├── JwtAuthenticationEntryPoint.java │ │ ├── JwtFilter.java │ │ ├── JwtSecurityConfig.java │ │ └── TokenProvider.java │ │ ├── repository │ │ ├── AuthorityRepository.java │ │ └── UserRepository.java │ │ ├── service │ │ ├── CustomUserDetailsService.java │ │ └── UserService.java │ │ └── util │ │ └── SecurityUtil.java └── resources │ ├── application.yml │ └── data.sql └── test └── java └── me └── silvernine └── tutorial └── JwtTutorialApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Eungoo Jung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot JWT Tutorial 2 | 3 | ## Author 4 | 5 | **SilverNine** 6 | 7 | * https://silvernine.me 8 | * https://portfolio.silvernine.me 9 | * https://github.com/silvernine 10 | 11 | ## Copyright and license 12 | 13 | The code is released under the [MIT license](LICENSE?raw=true). -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.4.0-SNAPSHOT' 4 | id 'io.spring.dependency-management' version '1.1.6' 5 | } 6 | 7 | group = 'me.silvernine' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | java { 11 | toolchain { 12 | languageVersion = JavaLanguageVersion.of(17) 13 | } 14 | } 15 | 16 | configurations { 17 | compileOnly { 18 | extendsFrom annotationProcessor 19 | } 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | maven { url 'https://repo.spring.io/milestone' } 25 | maven { url 'https://repo.spring.io/snapshot' } 26 | } 27 | 28 | dependencies { 29 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 30 | implementation 'org.springframework.boot:spring-boot-starter-security' 31 | implementation 'org.springframework.boot:spring-boot-starter-validation' 32 | implementation 'org.springframework.boot:spring-boot-starter-web' 33 | compileOnly 'org.projectlombok:lombok' 34 | annotationProcessor 'org.projectlombok:lombok' 35 | 36 | implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' 37 | runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' 38 | runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' 39 | runtimeOnly 'com.h2database:h2' 40 | 41 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 42 | testImplementation 'org.springframework.security:spring-security-test' 43 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 44 | } 45 | 46 | tasks.named('test') { 47 | useJUnitPlatform() 48 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilverNine/spring-boot-jwt-tutorial/4606db0bdb56cc090d6f27b84a76052ee075d90a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url 'https://repo.spring.io/milestone' } 4 | maven { url 'https://repo.spring.io/snapshot' } 5 | gradlePluginPortal() 6 | } 7 | } 8 | rootProject.name = 'jwt-tutorial' -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/JwtTutorialApplication.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class JwtTutorialApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(JwtTutorialApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.cors.CorsConfiguration; 6 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 7 | import org.springframework.web.filter.CorsFilter; 8 | 9 | @Configuration 10 | public class CorsConfig { 11 | @Bean 12 | public CorsFilter corsFilter() { 13 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 14 | CorsConfiguration config = new CorsConfiguration(); 15 | config.setAllowCredentials(true); 16 | config.addAllowedOriginPattern("*"); 17 | config.addAllowedHeader("*"); 18 | config.addAllowedMethod("*"); 19 | 20 | source.registerCorsConfiguration("/api/**", config); 21 | return new CorsFilter(source); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.config; 2 | 3 | import me.silvernine.tutorial.jwt.JwtSecurityConfig; 4 | import me.silvernine.tutorial.jwt.JwtAccessDeniedHandler; 5 | import me.silvernine.tutorial.jwt.JwtAuthenticationEntryPoint; 6 | import me.silvernine.tutorial.jwt.TokenProvider; 7 | 8 | import org.springframework.boot.autoconfigure.security.servlet.PathRequest; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 12 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 14 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 15 | import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; 16 | import org.springframework.security.config.http.SessionCreationPolicy; 17 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 18 | import org.springframework.security.crypto.password.PasswordEncoder; 19 | import org.springframework.security.web.SecurityFilterChain; 20 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 21 | import org.springframework.web.filter.CorsFilter; 22 | 23 | @EnableWebSecurity 24 | @EnableMethodSecurity 25 | @Configuration 26 | public class SecurityConfig { 27 | private final TokenProvider tokenProvider; 28 | private final CorsFilter corsFilter; 29 | private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; 30 | private final JwtAccessDeniedHandler jwtAccessDeniedHandler; 31 | 32 | public SecurityConfig( 33 | TokenProvider tokenProvider, 34 | CorsFilter corsFilter, 35 | JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, 36 | JwtAccessDeniedHandler jwtAccessDeniedHandler 37 | ) { 38 | this.tokenProvider = tokenProvider; 39 | this.corsFilter = corsFilter; 40 | this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; 41 | this.jwtAccessDeniedHandler = jwtAccessDeniedHandler; 42 | } 43 | 44 | @Bean 45 | public PasswordEncoder passwordEncoder() { 46 | return new BCryptPasswordEncoder(); 47 | } 48 | 49 | @Bean 50 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 51 | http 52 | // token을 사용하는 방식이기 때문에 csrf를 disable합니다. 53 | .csrf(AbstractHttpConfigurer::disable) 54 | 55 | .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) 56 | .exceptionHandling(exceptionHandling -> exceptionHandling 57 | .accessDeniedHandler(jwtAccessDeniedHandler) 58 | .authenticationEntryPoint(jwtAuthenticationEntryPoint) 59 | ) 60 | 61 | .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests 62 | .requestMatchers("/api/hello", "/api/authenticate", "/api/signup").permitAll() 63 | .requestMatchers(PathRequest.toH2Console()).permitAll() 64 | .anyRequest().authenticated() 65 | ) 66 | 67 | // 세션을 사용하지 않기 때문에 STATELESS로 설정 68 | .sessionManagement(sessionManagement -> 69 | sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) 70 | ) 71 | 72 | // enable h2-console 73 | .headers(headers -> 74 | headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) 75 | ) 76 | 77 | .with(new JwtSecurityConfig(tokenProvider), customizer -> {}); 78 | return http.build(); 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.controller; 2 | 3 | import me.silvernine.tutorial.dto.LoginDto; 4 | import me.silvernine.tutorial.dto.TokenDto; 5 | import me.silvernine.tutorial.jwt.JwtFilter; 6 | import me.silvernine.tutorial.jwt.TokenProvider; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.core.context.SecurityContextHolder; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | import jakarta.validation.Valid; 20 | 21 | @RestController 22 | @RequestMapping("/api") 23 | public class AuthController { 24 | private final TokenProvider tokenProvider; 25 | private final AuthenticationManagerBuilder authenticationManagerBuilder; 26 | 27 | public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) { 28 | this.tokenProvider = tokenProvider; 29 | this.authenticationManagerBuilder = authenticationManagerBuilder; 30 | } 31 | 32 | @PostMapping("/authenticate") 33 | public ResponseEntity authorize(@Valid @RequestBody LoginDto loginDto) { 34 | 35 | UsernamePasswordAuthenticationToken authenticationToken = 36 | new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()); 37 | 38 | Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); 39 | SecurityContextHolder.getContext().setAuthentication(authentication); 40 | 41 | String jwt = tokenProvider.createToken(authentication); 42 | 43 | HttpHeaders httpHeaders = new HttpHeaders(); 44 | httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt); 45 | 46 | return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.controller; 2 | 3 | import me.silvernine.tutorial.dto.UserDto; 4 | import me.silvernine.tutorial.service.UserService; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import jakarta.validation.Valid; 12 | import java.io.IOException; 13 | 14 | @RestController 15 | @RequestMapping("/api") 16 | public class UserController { 17 | private final UserService userService; 18 | 19 | public UserController(UserService userService) { 20 | this.userService = userService; 21 | } 22 | 23 | @GetMapping("/hello") 24 | public ResponseEntity hello() { 25 | return ResponseEntity.ok("hello"); 26 | } 27 | 28 | @PostMapping("/test-redirect") 29 | public void testRedirect(HttpServletResponse response) throws IOException { 30 | response.sendRedirect("/api/user"); 31 | } 32 | 33 | @PostMapping("/signup") 34 | public ResponseEntity signup( 35 | @Valid @RequestBody UserDto userDto 36 | ) { 37 | return ResponseEntity.ok(userService.signup(userDto)); 38 | } 39 | 40 | @GetMapping("/user") 41 | @PreAuthorize("hasAnyRole('USER','ADMIN')") 42 | public ResponseEntity getMyUserInfo(HttpServletRequest request) { 43 | return ResponseEntity.ok(userService.getMyUserWithAuthorities()); 44 | } 45 | 46 | @GetMapping("/user/{username}") 47 | @PreAuthorize("hasAnyRole('ADMIN')") 48 | public ResponseEntity getUserInfo(@PathVariable String username) { 49 | return ResponseEntity.ok(userService.getUserWithAuthorities(username)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/dto/AuthorityDto.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.dto; 2 | 3 | import lombok.*; 4 | 5 | @Getter 6 | @Setter 7 | @Builder 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class AuthorityDto { 11 | private String authorityName; 12 | } -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/dto/ErrorDto.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.dto; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.springframework.validation.FieldError; 6 | 7 | public class ErrorDto { 8 | private final int status; 9 | private final String message; 10 | private List fieldErrors = new ArrayList<>(); 11 | 12 | public ErrorDto(int status, String message) { 13 | this.status = status; 14 | this.message = message; 15 | } 16 | 17 | public int getStatus() { 18 | return status; 19 | } 20 | 21 | public String getMessage() { 22 | return message; 23 | } 24 | 25 | public void addFieldError(String objectName, String path, String message) { 26 | FieldError error = new FieldError(objectName, path, message); 27 | fieldErrors.add(error); 28 | } 29 | 30 | public List getFieldErrors() { 31 | return fieldErrors; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/dto/LoginDto.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.dto; 2 | 3 | import lombok.*; 4 | 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Size; 7 | 8 | @Getter 9 | @Setter 10 | @Builder 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class LoginDto { 14 | 15 | @NotNull 16 | @Size(min = 3, max = 50) 17 | private String username; 18 | 19 | @NotNull 20 | @Size(min = 3, max = 100) 21 | private String password; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/dto/TokenDto.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.dto; 2 | 3 | import lombok.*; 4 | 5 | @Getter 6 | @Setter 7 | @Builder 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class TokenDto { 11 | 12 | private String token; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/dto/UserDto.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.*; 5 | import me.silvernine.tutorial.entity.User; 6 | 7 | import jakarta.validation.constraints.NotNull; 8 | import jakarta.validation.constraints.Size; 9 | import java.util.Set; 10 | import java.util.stream.Collectors; 11 | 12 | @Getter 13 | @Setter 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class UserDto { 18 | 19 | @NotNull 20 | @Size(min = 3, max = 50) 21 | private String username; 22 | 23 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 24 | @NotNull 25 | @Size(min = 3, max = 100) 26 | private String password; 27 | 28 | @NotNull 29 | @Size(min = 3, max = 50) 30 | private String nickname; 31 | 32 | private Set authorityDtoSet; 33 | 34 | public static UserDto from(User user) { 35 | if(user == null) return null; 36 | 37 | return UserDto.builder() 38 | .username(user.getUsername()) 39 | .nickname(user.getNickname()) 40 | .authorityDtoSet(user.getAuthorities().stream() 41 | .map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build()) 42 | .collect(Collectors.toSet())) 43 | .build(); 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/entity/Authority.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.entity; 2 | 3 | import lombok.*; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.Table; 9 | 10 | @Entity 11 | @Table(name = "authority") 12 | @Getter 13 | @Setter 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class Authority { 18 | 19 | @Id 20 | @Column(name = "authority_name", length = 50) 21 | private String authorityName; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/entity/User.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.entity; 2 | 3 | import lombok.*; 4 | import jakarta.persistence.*; 5 | import java.util.Set; 6 | 7 | @Entity 8 | @Table(name = "`user`") 9 | @Getter 10 | @Setter 11 | @Builder 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class User { 15 | 16 | @Id 17 | @Column(name = "user_id") 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long userId; 20 | 21 | @Column(name = "username", length = 50, unique = true) 22 | private String username; 23 | 24 | @Column(name = "password", length = 100) 25 | private String password; 26 | 27 | @Column(name = "nickname", length = 50) 28 | private String nickname; 29 | 30 | @Column(name = "activated") 31 | private boolean activated; 32 | 33 | @ManyToMany 34 | @JoinTable( 35 | name = "user_authority", 36 | joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")}, 37 | inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")}) 38 | private Set authorities; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/exception/DuplicateMemberException.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.exception; 2 | 3 | public class DuplicateMemberException extends RuntimeException { 4 | public DuplicateMemberException() { 5 | super(); 6 | } 7 | public DuplicateMemberException(String message, Throwable cause) { 8 | super(message, cause); 9 | } 10 | public DuplicateMemberException(String message) { 11 | super(message); 12 | } 13 | public DuplicateMemberException(Throwable cause) { 14 | super(cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/exception/NotFoundMemberException.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.exception; 2 | 3 | public class NotFoundMemberException extends RuntimeException { 4 | public NotFoundMemberException() { 5 | super(); 6 | } 7 | public NotFoundMemberException(String message, Throwable cause) { 8 | super(message, cause); 9 | } 10 | public NotFoundMemberException(String message) { 11 | super(message); 12 | } 13 | public NotFoundMemberException(Throwable cause) { 14 | super(cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/handler/MethodArgumentNotValidExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.handler; 2 | 3 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 4 | 5 | import java.util.List; 6 | import me.silvernine.tutorial.dto.ErrorDto; 7 | import org.springframework.core.Ordered; 8 | import org.springframework.core.annotation.Order; 9 | import org.springframework.validation.BindingResult; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ControllerAdvice; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | import org.springframework.web.bind.annotation.ResponseBody; 14 | import org.springframework.web.bind.annotation.ResponseStatus; 15 | 16 | @Order(Ordered.HIGHEST_PRECEDENCE) 17 | @ControllerAdvice 18 | public class MethodArgumentNotValidExceptionHandler { 19 | 20 | @ResponseStatus(BAD_REQUEST) 21 | @ResponseBody 22 | @ExceptionHandler(MethodArgumentNotValidException.class) 23 | public ErrorDto methodArgumentNotValidException(MethodArgumentNotValidException ex) { 24 | BindingResult result = ex.getBindingResult(); 25 | List fieldErrors = result.getFieldErrors(); 26 | return processFieldErrors(fieldErrors); 27 | } 28 | 29 | private ErrorDto processFieldErrors(List fieldErrors) { 30 | ErrorDto errorDTO = new ErrorDto(BAD_REQUEST.value(), "@Valid Error"); 31 | for (org.springframework.validation.FieldError fieldError: fieldErrors) { 32 | errorDTO.addFieldError(fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage()); 33 | } 34 | return errorDTO; 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/handler/RestResponseExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.handler; 2 | 3 | import static org.springframework.http.HttpStatus.CONFLICT; 4 | import static org.springframework.http.HttpStatus.FORBIDDEN; 5 | 6 | import me.silvernine.tutorial.dto.ErrorDto; 7 | import me.silvernine.tutorial.exception.DuplicateMemberException; 8 | import me.silvernine.tutorial.exception.NotFoundMemberException; 9 | import org.springframework.security.access.AccessDeniedException; 10 | import org.springframework.web.bind.annotation.ControllerAdvice; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | import org.springframework.web.bind.annotation.ResponseStatus; 14 | import org.springframework.web.context.request.WebRequest; 15 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 16 | 17 | @ControllerAdvice 18 | public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler { 19 | 20 | @ResponseStatus(CONFLICT) 21 | @ExceptionHandler(value = { DuplicateMemberException.class }) 22 | @ResponseBody 23 | protected ErrorDto conflict(RuntimeException ex, WebRequest request) { 24 | return new ErrorDto(CONFLICT.value(), ex.getMessage()); 25 | } 26 | 27 | @ResponseStatus(FORBIDDEN) 28 | @ExceptionHandler(value = { NotFoundMemberException.class, AccessDeniedException.class }) 29 | @ResponseBody 30 | protected ErrorDto forbidden(RuntimeException ex, WebRequest request) { 31 | return new ErrorDto(FORBIDDEN.value(), ex.getMessage()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/jwt/JwtAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.jwt; 2 | 3 | import org.springframework.security.access.AccessDeniedException; 4 | import org.springframework.security.web.access.AccessDeniedHandler; 5 | import org.springframework.stereotype.Component; 6 | 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | 11 | @Component 12 | public class JwtAccessDeniedHandler implements AccessDeniedHandler { 13 | @Override 14 | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { 15 | //필요한 권한이 없이 접근하려 할때 403 16 | response.sendError(HttpServletResponse.SC_FORBIDDEN); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/jwt/JwtAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.jwt; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.AuthenticationEntryPoint; 5 | import org.springframework.stereotype.Component; 6 | 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | 11 | @Component 12 | public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { 13 | @Override 14 | public void commence(HttpServletRequest request, 15 | HttpServletResponse response, 16 | AuthenticationException authException) throws IOException { 17 | // 유효한 자격증명을 제공하지 않고 접근하려 할때 401 18 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/jwt/JwtFilter.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.jwt; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.util.StringUtils; 8 | import org.springframework.web.filter.GenericFilterBean; 9 | 10 | import jakarta.servlet.FilterChain; 11 | import jakarta.servlet.ServletException; 12 | import jakarta.servlet.ServletRequest; 13 | import jakarta.servlet.ServletResponse; 14 | import jakarta.servlet.http.HttpServletRequest; 15 | import java.io.IOException; 16 | 17 | public class JwtFilter extends GenericFilterBean { 18 | 19 | private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class); 20 | public static final String AUTHORIZATION_HEADER = "Authorization"; 21 | private TokenProvider tokenProvider; 22 | public JwtFilter(TokenProvider tokenProvider) { 23 | this.tokenProvider = tokenProvider; 24 | } 25 | 26 | @Override 27 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 28 | HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; 29 | String jwt = resolveToken(httpServletRequest); 30 | String requestURI = httpServletRequest.getRequestURI(); 31 | 32 | if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { 33 | Authentication authentication = tokenProvider.getAuthentication(jwt); 34 | SecurityContextHolder.getContext().setAuthentication(authentication); 35 | logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI); 36 | } else { 37 | logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI); 38 | } 39 | 40 | filterChain.doFilter(servletRequest, servletResponse); 41 | } 42 | 43 | private String resolveToken(HttpServletRequest request) { 44 | String bearerToken = request.getHeader(AUTHORIZATION_HEADER); 45 | 46 | if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { 47 | return bearerToken.substring(7); 48 | } 49 | 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/jwt/JwtSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.jwt; 2 | 3 | import org.springframework.security.config.annotation.SecurityConfigurerAdapter; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.web.DefaultSecurityFilterChain; 6 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 7 | 8 | public class JwtSecurityConfig extends SecurityConfigurerAdapter { 9 | private final TokenProvider tokenProvider; 10 | public JwtSecurityConfig(TokenProvider tokenProvider) { 11 | this.tokenProvider = tokenProvider; 12 | } 13 | 14 | @Override 15 | public void configure(HttpSecurity http) { 16 | http.addFilterBefore( 17 | new JwtFilter(tokenProvider), 18 | UsernamePasswordAuthenticationFilter.class 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/jwt/TokenProvider.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.jwt; 2 | 3 | import io.jsonwebtoken.*; 4 | import io.jsonwebtoken.io.Decoders; 5 | import io.jsonwebtoken.security.Keys; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.InitializingBean; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 14 | import org.springframework.security.core.userdetails.User; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.security.Key; 18 | import java.util.Arrays; 19 | import java.util.Collection; 20 | import java.util.Date; 21 | import java.util.stream.Collectors; 22 | 23 | @Component 24 | public class TokenProvider implements InitializingBean { 25 | 26 | private final Logger logger = LoggerFactory.getLogger(TokenProvider.class); 27 | private static final String AUTHORITIES_KEY = "auth"; 28 | private final String secret; 29 | private final long tokenValidityInMilliseconds; 30 | private Key key; 31 | 32 | public TokenProvider( 33 | @Value("${jwt.secret}") String secret, 34 | @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) { 35 | this.secret = secret; 36 | this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000; 37 | } 38 | 39 | @Override 40 | public void afterPropertiesSet() { 41 | byte[] keyBytes = Decoders.BASE64.decode(secret); 42 | this.key = Keys.hmacShaKeyFor(keyBytes); 43 | } 44 | 45 | public String createToken(Authentication authentication) { 46 | String authorities = authentication.getAuthorities().stream() 47 | .map(GrantedAuthority::getAuthority) 48 | .collect(Collectors.joining(",")); 49 | 50 | long now = (new Date()).getTime(); 51 | Date validity = new Date(now + this.tokenValidityInMilliseconds); 52 | 53 | return Jwts.builder() 54 | .setSubject(authentication.getName()) 55 | .claim(AUTHORITIES_KEY, authorities) 56 | .signWith(key, SignatureAlgorithm.HS512) 57 | .setExpiration(validity) 58 | .compact(); 59 | } 60 | 61 | public Authentication getAuthentication(String token) { 62 | Claims claims = Jwts 63 | .parserBuilder() 64 | .setSigningKey(key) 65 | .build() 66 | .parseClaimsJws(token) 67 | .getBody(); 68 | 69 | Collection authorities = 70 | Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) 71 | .map(SimpleGrantedAuthority::new) 72 | .collect(Collectors.toList()); 73 | 74 | User principal = new User(claims.getSubject(), "", authorities); 75 | 76 | return new UsernamePasswordAuthenticationToken(principal, token, authorities); 77 | } 78 | 79 | public boolean validateToken(String token) { 80 | try { 81 | Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); 82 | return true; 83 | } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { 84 | logger.info("잘못된 JWT 서명입니다."); 85 | } catch (ExpiredJwtException e) { 86 | logger.info("만료된 JWT 토큰입니다."); 87 | } catch (UnsupportedJwtException e) { 88 | logger.info("지원되지 않는 JWT 토큰입니다."); 89 | } catch (IllegalArgumentException e) { 90 | logger.info("JWT 토큰이 잘못되었습니다."); 91 | } 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/repository/AuthorityRepository.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.repository; 2 | 3 | import me.silvernine.tutorial.entity.Authority; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface AuthorityRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.repository; 2 | 3 | import me.silvernine.tutorial.entity.User; 4 | import org.springframework.data.jpa.repository.EntityGraph; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | 9 | public interface UserRepository extends JpaRepository { 10 | @EntityGraph(attributePaths = "authorities") 11 | Optional findOneWithAuthoritiesByUsername(String username); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/service/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.service; 2 | 3 | import me.silvernine.tutorial.entity.User; 4 | import me.silvernine.tutorial.repository.UserRepository; 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.Component; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | @Component("userDetailsService") 17 | public class CustomUserDetailsService implements UserDetailsService { 18 | private final UserRepository userRepository; 19 | 20 | public CustomUserDetailsService(UserRepository userRepository) { 21 | this.userRepository = userRepository; 22 | } 23 | 24 | @Override 25 | @Transactional 26 | public UserDetails loadUserByUsername(final String username) { 27 | return userRepository.findOneWithAuthoritiesByUsername(username) 28 | .map(user -> createUser(username, user)) 29 | .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다.")); 30 | } 31 | 32 | private org.springframework.security.core.userdetails.User createUser(String username, User user) { 33 | if (!user.isActivated()) { 34 | throw new RuntimeException(username + " -> 활성화되어 있지 않습니다."); 35 | } 36 | 37 | List grantedAuthorities = user.getAuthorities().stream() 38 | .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName())) 39 | .collect(Collectors.toList()); 40 | 41 | return new org.springframework.security.core.userdetails.User(user.getUsername(), 42 | user.getPassword(), 43 | grantedAuthorities); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.service; 2 | 3 | import java.util.Collections; 4 | import java.util.Optional; 5 | import me.silvernine.tutorial.dto.UserDto; 6 | import me.silvernine.tutorial.entity.Authority; 7 | import me.silvernine.tutorial.entity.User; 8 | import me.silvernine.tutorial.exception.DuplicateMemberException; 9 | import me.silvernine.tutorial.exception.NotFoundMemberException; 10 | import me.silvernine.tutorial.repository.UserRepository; 11 | import me.silvernine.tutorial.util.SecurityUtil; 12 | import org.springframework.security.crypto.password.PasswordEncoder; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | @Service 17 | public class UserService { 18 | private final UserRepository userRepository; 19 | private final PasswordEncoder passwordEncoder; 20 | 21 | public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { 22 | this.userRepository = userRepository; 23 | this.passwordEncoder = passwordEncoder; 24 | } 25 | 26 | @Transactional 27 | public UserDto signup(UserDto userDto) { 28 | if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) { 29 | throw new DuplicateMemberException("이미 가입되어 있는 유저입니다."); 30 | } 31 | 32 | Authority authority = Authority.builder() 33 | .authorityName("ROLE_USER") 34 | .build(); 35 | 36 | User user = User.builder() 37 | .username(userDto.getUsername()) 38 | .password(passwordEncoder.encode(userDto.getPassword())) 39 | .nickname(userDto.getNickname()) 40 | .authorities(Collections.singleton(authority)) 41 | .activated(true) 42 | .build(); 43 | 44 | return UserDto.from(userRepository.save(user)); 45 | } 46 | 47 | @Transactional(readOnly = true) 48 | public UserDto getUserWithAuthorities(String username) { 49 | return UserDto.from(userRepository.findOneWithAuthoritiesByUsername(username).orElse(null)); 50 | } 51 | 52 | @Transactional(readOnly = true) 53 | public UserDto getMyUserWithAuthorities() { 54 | return UserDto.from( 55 | SecurityUtil.getCurrentUsername() 56 | .flatMap(userRepository::findOneWithAuthoritiesByUsername) 57 | .orElseThrow(() -> new NotFoundMemberException("Member not found")) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/me/silvernine/tutorial/util/SecurityUtil.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | 9 | import java.util.Optional; 10 | 11 | public class SecurityUtil { 12 | 13 | private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class); 14 | 15 | private SecurityUtil() {} 16 | 17 | public static Optional getCurrentUsername() { 18 | final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 19 | 20 | if (authentication == null) { 21 | logger.debug("Security Context에 인증 정보가 없습니다."); 22 | return Optional.empty(); 23 | } 24 | 25 | String username = null; 26 | if (authentication.getPrincipal() instanceof UserDetails) { 27 | UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal(); 28 | username = springSecurityUser.getUsername(); 29 | } else if (authentication.getPrincipal() instanceof String) { 30 | username = (String) authentication.getPrincipal(); 31 | } 32 | 33 | return Optional.ofNullable(username); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | 3 | h2: 4 | console: 5 | enabled: true 6 | 7 | datasource: 8 | url: jdbc:h2:mem:testdb 9 | driver-class-name: org.h2.Driver 10 | username: sa 11 | password: 12 | 13 | jpa: 14 | database-platform: org.hibernate.dialect.H2Dialect 15 | hibernate: 16 | ddl-auto: create-drop 17 | properties: 18 | hibernate: 19 | format_sql: true 20 | show_sql: true 21 | defer-datasource-initialization: true 22 | 23 | jwt: 24 | header: Authorization 25 | #HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다. 26 | #echo 'silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret'|base64 27 | secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK 28 | token-validity-in-seconds: 86400 29 | 30 | logging: 31 | level: 32 | me.silvernine: DEBUG -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | insert into "user" (username, password, nickname, activated) values ('admin', '$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO/Vxd5DOi', 'admin', 1); 2 | insert into "user" (username, password, nickname, activated) values ('user', '$2a$08$UkVvwpULis18S19S5pZFn.YHPZt3oaqHZnDwqbCW9pft6uFtkXKDC', 'user', 1); 3 | 4 | insert into authority (authority_name) values ('ROLE_USER'); 5 | insert into authority (authority_name) values ('ROLE_ADMIN'); 6 | 7 | insert into user_authority (user_id, authority_name) values (1, 'ROLE_USER'); 8 | insert into user_authority (user_id, authority_name) values (1, 'ROLE_ADMIN'); 9 | insert into user_authority (user_id, authority_name) values (2, 'ROLE_USER'); -------------------------------------------------------------------------------- /src/test/java/me/silvernine/tutorial/JwtTutorialApplicationTests.java: -------------------------------------------------------------------------------- 1 | package me.silvernine.tutorial; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class JwtTutorialApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------