[
  {
    "path": ".gitignore",
    "content": "HELP.md\n.gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\nbin/\n!**/src/main/**/bin/\n!**/src/test/**/bin/\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\nout/\n!**/src/main/**/out/\n!**/src/test/**/out/\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\n\n### VS Code ###\n.vscode/\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 Eungoo Jung\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Spring Boot JWT Tutorial\n\n## Author\n\n**SilverNine**\n\n* https://silvernine.me\n* https://portfolio.silvernine.me\n* https://github.com/silvernine\n\n## Copyright and license\n\nThe code is released under the [MIT license](LICENSE?raw=true)."
  },
  {
    "path": "build.gradle",
    "content": "plugins {\n    id 'java'\n    id 'org.springframework.boot' version '3.4.0-SNAPSHOT'\n    id 'io.spring.dependency-management' version '1.1.6'\n}\n\ngroup = 'me.silvernine'\nversion = '0.0.1-SNAPSHOT'\n\njava {\n\ttoolchain {\n\t\tlanguageVersion = JavaLanguageVersion.of(17)\n\t}\n}\n\nconfigurations {\n    compileOnly {\n        extendsFrom annotationProcessor\n    }\n}\n\nrepositories {\n    mavenCentral()\n    maven { url 'https://repo.spring.io/milestone' }\n    maven { url 'https://repo.spring.io/snapshot' }\n}\n\ndependencies {\n    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'\n    implementation 'org.springframework.boot:spring-boot-starter-security'\n    implementation 'org.springframework.boot:spring-boot-starter-validation'\n    implementation 'org.springframework.boot:spring-boot-starter-web'\n    compileOnly 'org.projectlombok:lombok'\n    annotationProcessor 'org.projectlombok:lombok'\n\n    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'\n    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'\n    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'\n    runtimeOnly 'com.h2database:h2'\n\n    testImplementation 'org.springframework.boot:spring-boot-starter-test'\n    testImplementation 'org.springframework.security:spring-security-test'\n    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'\n}\n\ntasks.named('test') {\n\tuseJUnitPlatform()\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.10.2-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "settings.gradle",
    "content": "pluginManagement {\n\trepositories {\n\t\tmaven { url 'https://repo.spring.io/milestone' }\n\t\tmaven { url 'https://repo.spring.io/snapshot' }\n\t\tgradlePluginPortal()\n\t}\n}\nrootProject.name = 'jwt-tutorial'"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/JwtTutorialApplication.java",
    "content": "package me.silvernine.tutorial;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class JwtTutorialApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(JwtTutorialApplication.class, args);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/config/CorsConfig.java",
    "content": "package me.silvernine.tutorial.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.UrlBasedCorsConfigurationSource;\nimport org.springframework.web.filter.CorsFilter;\n\n@Configuration\npublic class CorsConfig {\n   @Bean\n   public CorsFilter corsFilter() {\n      UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\n      CorsConfiguration config = new CorsConfiguration();\n      config.setAllowCredentials(true);\n      config.addAllowedOriginPattern(\"*\");\n      config.addAllowedHeader(\"*\");\n      config.addAllowedMethod(\"*\");\n\n      source.registerCorsConfiguration(\"/api/**\", config);\n      return new CorsFilter(source);\n   }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/config/SecurityConfig.java",
    "content": "package me.silvernine.tutorial.config;\n\nimport me.silvernine.tutorial.jwt.JwtSecurityConfig;\nimport me.silvernine.tutorial.jwt.JwtAccessDeniedHandler;\nimport me.silvernine.tutorial.jwt.JwtAuthenticationEntryPoint;\nimport me.silvernine.tutorial.jwt.TokenProvider;\n\nimport org.springframework.boot.autoconfigure.security.servlet.PathRequest;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;\nimport org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;\nimport org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;\nimport org.springframework.security.config.http.SessionCreationPolicy;\nimport org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.security.web.SecurityFilterChain;\nimport org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;\nimport org.springframework.web.filter.CorsFilter;\n\n@EnableWebSecurity\n@EnableMethodSecurity\n@Configuration\npublic class SecurityConfig {\n    private final TokenProvider tokenProvider;\n    private final CorsFilter corsFilter;\n    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;\n    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;\n\n    public SecurityConfig(\n        TokenProvider tokenProvider,\n        CorsFilter corsFilter,\n        JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,\n        JwtAccessDeniedHandler jwtAccessDeniedHandler\n    ) {\n        this.tokenProvider = tokenProvider;\n        this.corsFilter = corsFilter;\n        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;\n        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;\n    }\n\n    @Bean\n    public PasswordEncoder passwordEncoder() {\n        return new BCryptPasswordEncoder();\n    }\n\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\n        http\n            // token을 사용하는 방식이기 때문에 csrf를 disable합니다.\n            .csrf(AbstractHttpConfigurer::disable)\n\n            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)\n            .exceptionHandling(exceptionHandling -> exceptionHandling\n                .accessDeniedHandler(jwtAccessDeniedHandler)\n                .authenticationEntryPoint(jwtAuthenticationEntryPoint)\n            )\n\n            .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests\n                .requestMatchers(\"/api/hello\", \"/api/authenticate\", \"/api/signup\").permitAll()\n                .requestMatchers(PathRequest.toH2Console()).permitAll()\n                .anyRequest().authenticated()\n            )\n\n            // 세션을 사용하지 않기 때문에 STATELESS로 설정\n            .sessionManagement(sessionManagement ->\n                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)\n            )\n\n            // enable h2-console\n            .headers(headers ->\n                headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)\n            )\n\n            .with(new JwtSecurityConfig(tokenProvider), customizer -> {});\n        return http.build();\n    }\n}"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/controller/AuthController.java",
    "content": "package me.silvernine.tutorial.controller;\n\nimport me.silvernine.tutorial.dto.LoginDto;\nimport me.silvernine.tutorial.dto.TokenDto;\nimport me.silvernine.tutorial.jwt.JwtFilter;\nimport me.silvernine.tutorial.jwt.TokenProvider;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport jakarta.validation.Valid;\n\n@RestController\n@RequestMapping(\"/api\")\npublic class AuthController {\n    private final TokenProvider tokenProvider;\n    private final AuthenticationManagerBuilder authenticationManagerBuilder;\n\n    public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {\n        this.tokenProvider = tokenProvider;\n        this.authenticationManagerBuilder = authenticationManagerBuilder;\n    }\n\n    @PostMapping(\"/authenticate\")\n    public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {\n\n        UsernamePasswordAuthenticationToken authenticationToken =\n                new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());\n\n        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);\n        SecurityContextHolder.getContext().setAuthentication(authentication);\n\n        String jwt = tokenProvider.createToken(authentication);\n\n        HttpHeaders httpHeaders = new HttpHeaders();\n        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, \"Bearer \" + jwt);\n\n        return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/controller/UserController.java",
    "content": "package me.silvernine.tutorial.controller;\n\nimport me.silvernine.tutorial.dto.UserDto;\nimport me.silvernine.tutorial.service.UserService;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.security.access.prepost.PreAuthorize;\nimport org.springframework.web.bind.annotation.*;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport jakarta.validation.Valid;\nimport java.io.IOException;\n\n@RestController\n@RequestMapping(\"/api\")\npublic class UserController {\n    private final UserService userService;\n\n    public UserController(UserService userService) {\n        this.userService = userService;\n    }\n\n    @GetMapping(\"/hello\")\n    public ResponseEntity<String> hello() {\n        return ResponseEntity.ok(\"hello\");\n    }\n\n    @PostMapping(\"/test-redirect\")\n    public void testRedirect(HttpServletResponse response) throws IOException {\n        response.sendRedirect(\"/api/user\");\n    }\n\n    @PostMapping(\"/signup\")\n    public ResponseEntity<UserDto> signup(\n            @Valid @RequestBody UserDto userDto\n    ) {\n        return ResponseEntity.ok(userService.signup(userDto));\n    }\n\n    @GetMapping(\"/user\")\n    @PreAuthorize(\"hasAnyRole('USER','ADMIN')\")\n    public ResponseEntity<UserDto> getMyUserInfo(HttpServletRequest request) {\n        return ResponseEntity.ok(userService.getMyUserWithAuthorities());\n    }\n\n    @GetMapping(\"/user/{username}\")\n    @PreAuthorize(\"hasAnyRole('ADMIN')\")\n    public ResponseEntity<UserDto> getUserInfo(@PathVariable String username) {\n        return ResponseEntity.ok(userService.getUserWithAuthorities(username));\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/dto/AuthorityDto.java",
    "content": "package me.silvernine.tutorial.dto;\n\nimport lombok.*;\n\n@Getter\n@Setter\n@Builder\n@AllArgsConstructor\n@NoArgsConstructor\npublic class AuthorityDto {\n   private String authorityName;\n}"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/dto/ErrorDto.java",
    "content": "package me.silvernine.tutorial.dto;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.springframework.validation.FieldError;\n\npublic class ErrorDto {\n    private final int status;\n    private final String message;\n    private List<FieldError> fieldErrors = new ArrayList<>();\n\n    public ErrorDto(int status, String message) {\n        this.status = status;\n        this.message = message;\n    }\n\n    public int getStatus() {\n        return status;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void addFieldError(String objectName, String path, String message) {\n        FieldError error = new FieldError(objectName, path, message);\n        fieldErrors.add(error);\n    }\n\n    public List<FieldError> getFieldErrors() {\n        return fieldErrors;\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/dto/LoginDto.java",
    "content": "package me.silvernine.tutorial.dto;\n\nimport lombok.*;\n\nimport jakarta.validation.constraints.NotNull;\nimport jakarta.validation.constraints.Size;\n\n@Getter\n@Setter\n@Builder\n@AllArgsConstructor\n@NoArgsConstructor\npublic class LoginDto {\n\n   @NotNull\n   @Size(min = 3, max = 50)\n   private String username;\n\n   @NotNull\n   @Size(min = 3, max = 100)\n   private String password;\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/dto/TokenDto.java",
    "content": "package me.silvernine.tutorial.dto;\n\nimport lombok.*;\n\n@Getter\n@Setter\n@Builder\n@AllArgsConstructor\n@NoArgsConstructor\npublic class TokenDto {\n\n    private String token;\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/dto/UserDto.java",
    "content": "package me.silvernine.tutorial.dto;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.*;\nimport me.silvernine.tutorial.entity.User;\n\nimport jakarta.validation.constraints.NotNull;\nimport jakarta.validation.constraints.Size;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n@Getter\n@Setter\n@Builder\n@AllArgsConstructor\n@NoArgsConstructor\npublic class UserDto {\n\n   @NotNull\n   @Size(min = 3, max = 50)\n   private String username;\n\n   @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)\n   @NotNull\n   @Size(min = 3, max = 100)\n   private String password;\n\n   @NotNull\n   @Size(min = 3, max = 50)\n   private String nickname;\n\n   private Set<AuthorityDto> authorityDtoSet;\n\n   public static UserDto from(User user) {\n      if(user == null) return null;\n\n      return UserDto.builder()\n              .username(user.getUsername())\n              .nickname(user.getNickname())\n              .authorityDtoSet(user.getAuthorities().stream()\n                      .map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build())\n                      .collect(Collectors.toSet()))\n              .build();\n   }\n}"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/entity/Authority.java",
    "content": "package me.silvernine.tutorial.entity;\n\nimport lombok.*;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport jakarta.persistence.Table;\n\n@Entity\n@Table(name = \"authority\")\n@Getter\n@Setter\n@Builder\n@AllArgsConstructor\n@NoArgsConstructor\npublic class Authority {\n\n   @Id\n   @Column(name = \"authority_name\", length = 50)\n   private String authorityName;\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/entity/User.java",
    "content": "package me.silvernine.tutorial.entity;\n\nimport lombok.*;\nimport jakarta.persistence.*;\nimport java.util.Set;\n\n@Entity\n@Table(name = \"`user`\")\n@Getter\n@Setter\n@Builder\n@AllArgsConstructor\n@NoArgsConstructor\npublic class User {\n\n   @Id\n   @Column(name = \"user_id\")\n   @GeneratedValue(strategy = GenerationType.IDENTITY)\n   private Long userId;\n\n   @Column(name = \"username\", length = 50, unique = true)\n   private String username;\n\n   @Column(name = \"password\", length = 100)\n   private String password;\n\n   @Column(name = \"nickname\", length = 50)\n   private String nickname;\n\n   @Column(name = \"activated\")\n   private boolean activated;\n\n   @ManyToMany\n   @JoinTable(\n      name = \"user_authority\",\n      joinColumns = {@JoinColumn(name = \"user_id\", referencedColumnName = \"user_id\")},\n      inverseJoinColumns = {@JoinColumn(name = \"authority_name\", referencedColumnName = \"authority_name\")})\n   private Set<Authority> authorities;\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/exception/DuplicateMemberException.java",
    "content": "package me.silvernine.tutorial.exception;\n\npublic class DuplicateMemberException extends RuntimeException {\n    public DuplicateMemberException() {\n        super();\n    }\n    public DuplicateMemberException(String message, Throwable cause) {\n        super(message, cause);\n    }\n    public DuplicateMemberException(String message) {\n        super(message);\n    }\n    public DuplicateMemberException(Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/exception/NotFoundMemberException.java",
    "content": "package me.silvernine.tutorial.exception;\n\npublic class NotFoundMemberException extends RuntimeException {\n    public NotFoundMemberException() {\n        super();\n    }\n    public NotFoundMemberException(String message, Throwable cause) {\n        super(message, cause);\n    }\n    public NotFoundMemberException(String message) {\n        super(message);\n    }\n    public NotFoundMemberException(Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/handler/MethodArgumentNotValidExceptionHandler.java",
    "content": "package me.silvernine.tutorial.handler;\n\nimport static org.springframework.http.HttpStatus.BAD_REQUEST;\n\nimport java.util.List;\nimport me.silvernine.tutorial.dto.ErrorDto;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.validation.BindingResult;\nimport org.springframework.web.bind.MethodArgumentNotValidException;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseBody;\nimport org.springframework.web.bind.annotation.ResponseStatus;\n\n@Order(Ordered.HIGHEST_PRECEDENCE)\n@ControllerAdvice\npublic class MethodArgumentNotValidExceptionHandler {\n\n    @ResponseStatus(BAD_REQUEST)\n    @ResponseBody\n    @ExceptionHandler(MethodArgumentNotValidException.class)\n    public ErrorDto methodArgumentNotValidException(MethodArgumentNotValidException ex) {\n        BindingResult result = ex.getBindingResult();\n        List<org.springframework.validation.FieldError> fieldErrors = result.getFieldErrors();\n        return processFieldErrors(fieldErrors);\n    }\n\n    private ErrorDto processFieldErrors(List<org.springframework.validation.FieldError> fieldErrors) {\n        ErrorDto errorDTO = new ErrorDto(BAD_REQUEST.value(), \"@Valid Error\");\n        for (org.springframework.validation.FieldError fieldError: fieldErrors) {\n            errorDTO.addFieldError(fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());\n        }\n        return errorDTO;\n    }\n}"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/handler/RestResponseExceptionHandler.java",
    "content": "package me.silvernine.tutorial.handler;\n\nimport static org.springframework.http.HttpStatus.CONFLICT;\nimport static org.springframework.http.HttpStatus.FORBIDDEN;\n\nimport me.silvernine.tutorial.dto.ErrorDto;\nimport me.silvernine.tutorial.exception.DuplicateMemberException;\nimport me.silvernine.tutorial.exception.NotFoundMemberException;\nimport org.springframework.security.access.AccessDeniedException;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseBody;\nimport org.springframework.web.bind.annotation.ResponseStatus;\nimport org.springframework.web.context.request.WebRequest;\nimport org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;\n\n@ControllerAdvice\npublic class RestResponseExceptionHandler extends ResponseEntityExceptionHandler {\n\n    @ResponseStatus(CONFLICT)\n    @ExceptionHandler(value = { DuplicateMemberException.class })\n    @ResponseBody\n    protected ErrorDto conflict(RuntimeException ex, WebRequest request) {\n        return new ErrorDto(CONFLICT.value(), ex.getMessage());\n    }\n\n    @ResponseStatus(FORBIDDEN)\n    @ExceptionHandler(value = { NotFoundMemberException.class, AccessDeniedException.class })\n    @ResponseBody\n    protected ErrorDto forbidden(RuntimeException ex, WebRequest request) {\n        return new ErrorDto(FORBIDDEN.value(), ex.getMessage());\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/jwt/JwtAccessDeniedHandler.java",
    "content": "package me.silvernine.tutorial.jwt;\n\nimport org.springframework.security.access.AccessDeniedException;\nimport org.springframework.security.web.access.AccessDeniedHandler;\nimport org.springframework.stereotype.Component;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n@Component\npublic class JwtAccessDeniedHandler implements AccessDeniedHandler {\n   @Override\n   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {\n      //필요한 권한이 없이 접근하려 할때 403\n      response.sendError(HttpServletResponse.SC_FORBIDDEN);\n   }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/jwt/JwtAuthenticationEntryPoint.java",
    "content": "package me.silvernine.tutorial.jwt;\n\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.web.AuthenticationEntryPoint;\nimport org.springframework.stereotype.Component;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n@Component\npublic class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {\n   @Override\n   public void commence(HttpServletRequest request,\n                        HttpServletResponse response,\n                        AuthenticationException authException) throws IOException {\n      // 유효한 자격증명을 제공하지 않고 접근하려 할때 401\n      response.sendError(HttpServletResponse.SC_UNAUTHORIZED);\n   }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/jwt/JwtFilter.java",
    "content": "package me.silvernine.tutorial.jwt;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.filter.GenericFilterBean;\n\nimport jakarta.servlet.FilterChain;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.ServletRequest;\nimport jakarta.servlet.ServletResponse;\nimport jakarta.servlet.http.HttpServletRequest;\nimport java.io.IOException;\n\npublic class JwtFilter extends GenericFilterBean {\n\n   private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);\n   public static final String AUTHORIZATION_HEADER = \"Authorization\";\n   private TokenProvider tokenProvider;\n   public JwtFilter(TokenProvider tokenProvider) {\n      this.tokenProvider = tokenProvider;\n   }\n\n   @Override\n   public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n      HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;\n      String jwt = resolveToken(httpServletRequest);\n      String requestURI = httpServletRequest.getRequestURI();\n\n      if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {\n         Authentication authentication = tokenProvider.getAuthentication(jwt);\n         SecurityContextHolder.getContext().setAuthentication(authentication);\n         logger.debug(\"Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}\", authentication.getName(), requestURI);\n      } else {\n         logger.debug(\"유효한 JWT 토큰이 없습니다, uri: {}\", requestURI);\n      }\n\n      filterChain.doFilter(servletRequest, servletResponse);\n   }\n\n   private String resolveToken(HttpServletRequest request) {\n      String bearerToken = request.getHeader(AUTHORIZATION_HEADER);\n\n      if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(\"Bearer \")) {\n         return bearerToken.substring(7);\n      }\n\n      return null;\n   }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/jwt/JwtSecurityConfig.java",
    "content": "package me.silvernine.tutorial.jwt;\n\nimport org.springframework.security.config.annotation.SecurityConfigurerAdapter;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.web.DefaultSecurityFilterChain;\nimport org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;\n\npublic class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {\n    private final TokenProvider tokenProvider;\n    public JwtSecurityConfig(TokenProvider tokenProvider) {\n        this.tokenProvider = tokenProvider;\n    }\n\n    @Override\n    public void configure(HttpSecurity http) {\n        http.addFilterBefore(\n            new JwtFilter(tokenProvider),\n            UsernamePasswordAuthenticationFilter.class\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/jwt/TokenProvider.java",
    "content": "package me.silvernine.tutorial.jwt;\n\nimport io.jsonwebtoken.*;\nimport io.jsonwebtoken.io.Decoders;\nimport io.jsonwebtoken.security.Keys;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.stereotype.Component;\n\nimport java.security.Key;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Date;\nimport java.util.stream.Collectors;\n\n@Component\npublic class TokenProvider implements InitializingBean {\n\n   private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);\n   private static final String AUTHORITIES_KEY = \"auth\";\n   private final String secret;\n   private final long tokenValidityInMilliseconds;\n   private Key key;\n\n   public TokenProvider(\n      @Value(\"${jwt.secret}\") String secret,\n      @Value(\"${jwt.token-validity-in-seconds}\") long tokenValidityInSeconds) {\n      this.secret = secret;\n      this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;\n   }\n\n   @Override\n   public void afterPropertiesSet() {\n      byte[] keyBytes = Decoders.BASE64.decode(secret);\n      this.key = Keys.hmacShaKeyFor(keyBytes);\n   }\n\n   public String createToken(Authentication authentication) {\n      String authorities = authentication.getAuthorities().stream()\n         .map(GrantedAuthority::getAuthority)\n         .collect(Collectors.joining(\",\"));\n\n      long now = (new Date()).getTime();\n      Date validity = new Date(now + this.tokenValidityInMilliseconds);\n\n      return Jwts.builder()\n         .setSubject(authentication.getName())\n         .claim(AUTHORITIES_KEY, authorities)\n         .signWith(key, SignatureAlgorithm.HS512)\n         .setExpiration(validity)\n         .compact();\n   }\n\n   public Authentication getAuthentication(String token) {\n      Claims claims = Jwts\n              .parserBuilder()\n              .setSigningKey(key)\n              .build()\n              .parseClaimsJws(token)\n              .getBody();\n\n      Collection<? extends GrantedAuthority> authorities =\n         Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(\",\"))\n            .map(SimpleGrantedAuthority::new)\n            .collect(Collectors.toList());\n\n      User principal = new User(claims.getSubject(), \"\", authorities);\n\n      return new UsernamePasswordAuthenticationToken(principal, token, authorities);\n   }\n\n   public boolean validateToken(String token) {\n      try {\n         Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);\n         return true;\n      } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {\n         logger.info(\"잘못된 JWT 서명입니다.\");\n      } catch (ExpiredJwtException e) {\n         logger.info(\"만료된 JWT 토큰입니다.\");\n      } catch (UnsupportedJwtException e) {\n         logger.info(\"지원되지 않는 JWT 토큰입니다.\");\n      } catch (IllegalArgumentException e) {\n         logger.info(\"JWT 토큰이 잘못되었습니다.\");\n      }\n      return false;\n   }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/repository/AuthorityRepository.java",
    "content": "package me.silvernine.tutorial.repository;\n\nimport me.silvernine.tutorial.entity.Authority;\nimport org.springframework.data.jpa.repository.JpaRepository;\n\npublic interface AuthorityRepository extends JpaRepository<Authority, String> {\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/repository/UserRepository.java",
    "content": "package me.silvernine.tutorial.repository;\n\nimport me.silvernine.tutorial.entity.User;\nimport org.springframework.data.jpa.repository.EntityGraph;\nimport org.springframework.data.jpa.repository.JpaRepository;\n\nimport java.util.Optional;\n\npublic interface UserRepository extends JpaRepository<User, Long> {\n   @EntityGraph(attributePaths = \"authorities\")\n   Optional<User> findOneWithAuthoritiesByUsername(String username);\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/service/CustomUserDetailsService.java",
    "content": "package me.silvernine.tutorial.service;\n\nimport me.silvernine.tutorial.entity.User;\nimport me.silvernine.tutorial.repository.UserRepository;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.core.userdetails.UserDetailsService;\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\nimport org.springframework.stereotype.Component;\nimport org.springframework.transaction.annotation.Transactional;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n@Component(\"userDetailsService\")\npublic class CustomUserDetailsService implements UserDetailsService {\n   private final UserRepository userRepository;\n\n   public CustomUserDetailsService(UserRepository userRepository) {\n      this.userRepository = userRepository;\n   }\n\n   @Override\n   @Transactional\n   public UserDetails loadUserByUsername(final String username) {\n      return userRepository.findOneWithAuthoritiesByUsername(username)\n         .map(user -> createUser(username, user))\n         .orElseThrow(() -> new UsernameNotFoundException(username + \" -> 데이터베이스에서 찾을 수 없습니다.\"));\n   }\n\n   private org.springframework.security.core.userdetails.User createUser(String username, User user) {\n      if (!user.isActivated()) {\n         throw new RuntimeException(username + \" -> 활성화되어 있지 않습니다.\");\n      }\n\n      List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()\n              .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))\n              .collect(Collectors.toList());\n\n      return new org.springframework.security.core.userdetails.User(user.getUsername(),\n              user.getPassword(),\n              grantedAuthorities);\n   }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/service/UserService.java",
    "content": "package me.silvernine.tutorial.service;\n\nimport java.util.Collections;\nimport java.util.Optional;\nimport me.silvernine.tutorial.dto.UserDto;\nimport me.silvernine.tutorial.entity.Authority;\nimport me.silvernine.tutorial.entity.User;\nimport me.silvernine.tutorial.exception.DuplicateMemberException;\nimport me.silvernine.tutorial.exception.NotFoundMemberException;\nimport me.silvernine.tutorial.repository.UserRepository;\nimport me.silvernine.tutorial.util.SecurityUtil;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.annotation.Transactional;\n\n@Service\npublic class UserService {\n    private final UserRepository userRepository;\n    private final PasswordEncoder passwordEncoder;\n\n    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {\n        this.userRepository = userRepository;\n        this.passwordEncoder = passwordEncoder;\n    }\n\n    @Transactional\n    public UserDto signup(UserDto userDto) {\n        if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {\n            throw new DuplicateMemberException(\"이미 가입되어 있는 유저입니다.\");\n        }\n\n        Authority authority = Authority.builder()\n                .authorityName(\"ROLE_USER\")\n                .build();\n\n        User user = User.builder()\n                .username(userDto.getUsername())\n                .password(passwordEncoder.encode(userDto.getPassword()))\n                .nickname(userDto.getNickname())\n                .authorities(Collections.singleton(authority))\n                .activated(true)\n                .build();\n\n        return UserDto.from(userRepository.save(user));\n    }\n\n    @Transactional(readOnly = true)\n    public UserDto getUserWithAuthorities(String username) {\n        return UserDto.from(userRepository.findOneWithAuthoritiesByUsername(username).orElse(null));\n    }\n\n    @Transactional(readOnly = true)\n    public UserDto getMyUserWithAuthorities() {\n        return UserDto.from(\n                SecurityUtil.getCurrentUsername()\n                        .flatMap(userRepository::findOneWithAuthoritiesByUsername)\n                        .orElseThrow(() -> new NotFoundMemberException(\"Member not found\"))\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/silvernine/tutorial/util/SecurityUtil.java",
    "content": "package me.silvernine.tutorial.util;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextHolder;\nimport org.springframework.security.core.userdetails.UserDetails;\n\nimport java.util.Optional;\n\npublic class SecurityUtil {\n\n   private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);\n\n   private SecurityUtil() {}\n\n   public static Optional<String> getCurrentUsername() {\n      final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n\n      if (authentication == null) {\n         logger.debug(\"Security Context에 인증 정보가 없습니다.\");\n         return Optional.empty();\n      }\n\n      String username = null;\n      if (authentication.getPrincipal() instanceof UserDetails) {\n         UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();\n         username = springSecurityUser.getUsername();\n      } else if (authentication.getPrincipal() instanceof String) {\n         username = (String) authentication.getPrincipal();\n      }\n\n      return Optional.ofNullable(username);\n   }\n}\n"
  },
  {
    "path": "src/main/resources/application.yml",
    "content": "spring:\n\n  h2:\n    console:\n      enabled: true\n\n  datasource:\n    url: jdbc:h2:mem:testdb\n    driver-class-name: org.h2.Driver\n    username: sa\n    password:\n\n  jpa:\n    database-platform: org.hibernate.dialect.H2Dialect\n    hibernate:\n      ddl-auto: create-drop\n    properties:\n      hibernate:\n        format_sql: true\n        show_sql: true\n    defer-datasource-initialization: true\n\njwt:\n  header: Authorization\n  #HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.\n  #echo 'silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret'|base64\n  secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK\n  token-validity-in-seconds: 86400\n\nlogging:\n  level:\n    me.silvernine: DEBUG"
  },
  {
    "path": "src/main/resources/data.sql",
    "content": "insert into \"user\" (username, password, nickname, activated) values ('admin', '$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO/Vxd5DOi', 'admin', 1);\ninsert into \"user\" (username, password, nickname, activated) values ('user', '$2a$08$UkVvwpULis18S19S5pZFn.YHPZt3oaqHZnDwqbCW9pft6uFtkXKDC', 'user', 1);\n\ninsert into authority (authority_name) values ('ROLE_USER');\ninsert into authority (authority_name) values ('ROLE_ADMIN');\n\ninsert into user_authority (user_id, authority_name) values (1, 'ROLE_USER');\ninsert into user_authority (user_id, authority_name) values (1, 'ROLE_ADMIN');\ninsert into user_authority (user_id, authority_name) values (2, 'ROLE_USER');"
  },
  {
    "path": "src/test/java/me/silvernine/tutorial/JwtTutorialApplicationTests.java",
    "content": "package me.silvernine.tutorial;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass JwtTutorialApplicationTests {\n\n\t@Test\n\tvoid contextLoads() {\n\t}\n\n}\n"
  }
]