├── .gitignore ├── HELP.md ├── LINCENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── threlease │ │ └── base │ │ ├── BaseApplication.java │ │ ├── common │ │ ├── configs │ │ │ ├── CustomPhysicalNamingStrategy.java │ │ │ ├── QueryDslConfig.java │ │ │ ├── SwaggerConfig.java │ │ │ ├── WebMvcConfig.java │ │ │ └── WebSecurityConfig.java │ │ ├── convert │ │ │ └── DateConverter.java │ │ ├── crypto │ │ │ └── Hash.java │ │ ├── details │ │ │ └── CustomUserDetails.java │ │ ├── enums │ │ │ └── Roles.java │ │ ├── exception │ │ │ └── TokenValidException.java │ │ ├── filter │ │ │ └── JwtAuthenticationFilter.java │ │ ├── handler │ │ │ └── GlobalExceptionHandler.java │ │ ├── interceptors │ │ │ └── TokenInterceptor.java │ │ ├── provider │ │ │ └── JwtProvider.java │ │ ├── service │ │ │ └── CustomUserDetailsService.java │ │ └── utils │ │ │ ├── Failable.java │ │ │ ├── enumeration │ │ │ ├── EnumValueValidator.java │ │ │ └── Enumeration.java │ │ │ ├── random │ │ │ ├── GetRandom.java │ │ │ └── RandomType.java │ │ │ └── responses │ │ │ ├── BasicResponse.java │ │ │ └── Messages.java │ │ ├── entites │ │ └── AuthEntity.java │ │ ├── functions │ │ └── auth │ │ │ ├── AuthController.java │ │ │ ├── AuthService.java │ │ │ └── dto │ │ │ ├── LoginDto.java │ │ │ └── SignUpDto.java │ │ └── repositories │ │ └── auth │ │ ├── AuthRepository.java │ │ ├── UserCustomRepository.java │ │ └── UserCustomRepositoryImpl.java └── resources │ └── application.yml └── test └── java └── com └── threlease └── base └── BaseApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle 3 | /.idea 4 | /user -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Gradle documentation](https://docs.gradle.org) 7 | * [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.1.5/gradle-plugin/reference/html/) 8 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.1.5/gradle-plugin/reference/html/#build-image) 9 | * [Spring Web](https://docs.spring.io/spring-boot/docs/3.1.5/reference/htmlsingle/index.html#web) 10 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.1.5/reference/htmlsingle/index.html#data.sql.jpa-and-spring-data) 11 | 12 | ### Guides 13 | The following guides illustrate how to use some features concretely: 14 | 15 | * [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) 16 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 17 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 18 | * [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) 19 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 20 | 21 | ### Additional Links 22 | These additional references should also help you: 23 | 24 | * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) 25 | 26 | -------------------------------------------------------------------------------- /LINCENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 null-variable 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-boot-base 2 | spring boot base 3 | 4 | > 해당 프로젝트는 아래의 기능들이 셋팅되어있어요! 5 | 6 | * Security 7 | * Json Web Token 8 | * MYSQL JPA 9 | * Swagger 10 | * Validate + Enum Validate 11 | * Encrypt Utils 12 | * Random String Utils 13 | * Failable 14 | * QueryDSL 15 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.1.5' 4 | id 'io.spring.dependency-management' version '1.1.3' 5 | } 6 | 7 | group = 'com.threlease' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | java { 11 | sourceCompatibility = '17' 12 | } 13 | 14 | configurations { 15 | compileOnly { 16 | extendsFrom annotationProcessor 17 | } 18 | } 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 26 | implementation 'org.springframework.boot:spring-boot-starter-web' 27 | implementation 'org.springframework.boot:spring-boot-starter-security' 28 | implementation 'org.springframework.boot:spring-boot-starter-validation' 29 | implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' 30 | implementation 'io.jsonwebtoken:jjwt-api:0.12.5' 31 | runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' 32 | runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' 33 | runtimeOnly 'com.mysql:mysql-connector-j' 34 | compileOnly 'org.projectlombok:lombok' 35 | annotationProcessor 'org.projectlombok:lombok' 36 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 37 | testImplementation 'org.springframework.security:spring-security-test' 38 | 39 | implementation 'com.google.code.gson:gson:2.10.1' 40 | 41 | implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' 42 | annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" 43 | annotationProcessor "jakarta.annotation:jakarta.annotation-api" 44 | annotationProcessor "jakarta.persistence:jakarta.persistence-api" 45 | } 46 | 47 | tasks.named('test') { 48 | useJUnitPlatform() 49 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/th-release/spring-boot-base/49e4da5dc34ded33224247b6a6d6b7843ebeb8a1/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.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'base' 2 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/BaseApplication.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | 13 | @EnableJpaRepositories 14 | @SpringBootApplication 15 | public class BaseApplication { 16 | public static void main(String[] args) { 17 | String currentDirectory = System.getProperty("user.dir"); 18 | 19 | try { 20 | Path userDirectory = Paths.get(currentDirectory + File.separator + "user/"); 21 | if (!Files.exists(userDirectory) && !Files.isDirectory(userDirectory)) { 22 | Files.createDirectory(userDirectory); 23 | } 24 | 25 | Path profileDirectory = Paths.get(currentDirectory + File.separator + "user/profile/"); 26 | if (!Files.exists(profileDirectory) && !Files.isDirectory(profileDirectory)) { 27 | Files.createDirectory(profileDirectory); 28 | } 29 | } catch (IOException e) { 30 | System.out.println("Required Directory"); 31 | return; 32 | } 33 | 34 | SpringApplication.run(BaseApplication.class, args); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/configs/CustomPhysicalNamingStrategy.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.configs; 2 | 3 | import org.hibernate.boot.model.naming.Identifier; 4 | import org.hibernate.boot.model.naming.PhysicalNamingStrategy; 5 | import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; 6 | 7 | /** 8 | * JPA 데이터베이스 테이블과 컬럼 명을 매핑할 때 기본적으로 자바의 네이밍 규칙을 따르거나 사용자가 지정한 규칙을 따를 수 있습니다. 9 | * 이 클래스는 Hibernate의 네이밍 전략을 사용자 정의하기 위해 사용됩니다. 10 | */ 11 | public class CustomPhysicalNamingStrategy implements PhysicalNamingStrategy { 12 | 13 | @Override 14 | public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) { 15 | return name; 16 | } 17 | 18 | @Override 19 | public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) { 20 | return name; 21 | } 22 | 23 | @Override 24 | public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) { 25 | return applyQuoting(name); // 테이블 이름에 큰따옴표 적용 26 | } 27 | 28 | @Override 29 | public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) { 30 | return name; 31 | } 32 | 33 | @Override 34 | public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) { 35 | return applyQuoting(name); // 컬럼 이름에 큰따옴표 적용 36 | } 37 | 38 | private Identifier applyQuoting(Identifier identifier) { 39 | if (identifier == null) { 40 | return null; 41 | } 42 | // 이미 큰따옴표로 감싸지지 않은 경우 큰따옴표 적용 43 | String quotedText = "\"" + identifier.getText() + "\""; 44 | return Identifier.toIdentifier(quotedText, identifier.isQuoted()); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/configs/QueryDslConfig.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.configs; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.persistence.PersistenceContext; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class QueryDslConfig { 11 | @PersistenceContext 12 | private EntityManager entityManager; 13 | 14 | @Bean 15 | public JPAQueryFactory jpaQueryFactory() { 16 | return new JPAQueryFactory(entityManager); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/configs/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.configs; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import io.swagger.v3.oas.models.security.SecurityRequirement; 7 | import io.swagger.v3.oas.models.security.SecurityScheme; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class SwaggerConfig { 13 | @Bean 14 | public OpenAPI openAPI() { 15 | String jwt = "JWT"; 16 | SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); 17 | Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() 18 | .name(jwt) 19 | .type(SecurityScheme.Type.HTTP) 20 | .scheme("bearer") 21 | .bearerFormat("JWT") 22 | ); 23 | return new OpenAPI() 24 | .components(new Components()) 25 | .info(apiInfo()) 26 | .addSecurityItem(securityRequirement) 27 | .components(components); 28 | } 29 | private Info apiInfo() { 30 | return new Info() 31 | .title("API Test") // API의 제목 32 | .description("Let's practice Swagger UI") // API에 대한 설명 33 | .version("1.0.0"); // API의 버전 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/configs/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.configs; 2 | 3 | import com.threlease.base.common.interceptors.TokenInterceptor; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 7 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 8 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | /** 12 | * MVC 관련으로 세팅하는 클래스 13 | */ 14 | @Configuration 15 | @AllArgsConstructor 16 | @EnableWebMvc 17 | public class WebMvcConfig implements WebMvcConfigurer { 18 | private final TokenInterceptor tokenInterceptor; 19 | 20 | /** 21 | * Interceptors 등록 함수 22 | * @param registry 23 | */ 24 | @Override 25 | public void addInterceptors(InterceptorRegistry registry) { 26 | registry.addInterceptor(tokenInterceptor) 27 | .addPathPatterns("/**") 28 | .excludePathPatterns("/auth/login", "/auth/publicKey", "/swagger", "/swagger-ui/**", "/v3/api-docs/**"); 29 | } 30 | 31 | /** 32 | * Cors 관련 함수 33 | * @param registry 34 | */ 35 | @Override 36 | public void addCorsMappings(CorsRegistry registry) { 37 | registry.addMapping("/**") 38 | .allowedOriginPatterns("*") 39 | .allowedMethods("*") // OPTIONS 포함 40 | .allowedHeaders("*") 41 | .allowCredentials(true); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/configs/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.configs; 2 | 3 | import com.threlease.base.common.filter.JwtAuthenticationFilter; 4 | import com.threlease.base.common.provider.JwtProvider; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.config.http.SessionCreationPolicy; 12 | import org.springframework.security.web.SecurityFilterChain; 13 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @RequiredArgsConstructor 18 | public class WebSecurityConfig { 19 | private final JwtProvider jwtProvider; 20 | 21 | @Bean 22 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 23 | http 24 | .httpBasic(AbstractHttpConfigurer::disable) 25 | .csrf(AbstractHttpConfigurer::disable) 26 | .sessionManagement(management -> 27 | management.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) 28 | ) 29 | .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); 30 | return http.build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/convert/DateConverter.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.convert; 2 | 3 | import com.threlease.base.common.utils.Failable; 4 | 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | import java.time.LocalTime; 8 | import java.time.format.DateTimeFormatter; 9 | import java.time.format.DateTimeParseException; 10 | 11 | /** 12 | * yyyy-MM-dd 형식으로 문자열이 오면 해당 문자열을 LocalDateTime 데이터로 바꿔준다. 13 | */ 14 | public class DateConverter { 15 | public static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 16 | 17 | /** 18 | * 19 | * @param dateString yyyy-MM-dd 형식의 문자열 20 | * @return LocalDateTime 데이터 21 | */ 22 | public static Failable run(String dateString) { 23 | try { 24 | // 입력된 문자열이 지정된 형식에 맞는지 검증하며 LocalDate로 변환 25 | LocalDate date = LocalDate.parse(dateString, formatter); 26 | // 기본 시간(00:00:00)을 추가하여 LocalDateTime 반환 27 | return Failable.success(LocalDateTime.of(date, LocalTime.MIDNIGHT)); 28 | } catch (DateTimeParseException e) { 29 | // 형식이 잘못되었을 경우 사용자 정의 예외 또는 메시지 반환 30 | return Failable.error("잘못된 날짜 형식입니다. 형식은 'yyyy-MM-dd'여야 합니다."); 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/crypto/Hash.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.crypto; 2 | import java.math.BigInteger; 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.util.Base64; 6 | import java.util.HashMap; 7 | 8 | public class Hash { 9 | public static String generateSHA512(String input) { 10 | try { 11 | MessageDigest md = MessageDigest.getInstance("SHA-512"); 12 | 13 | byte[] messageDigest = md.digest(input.getBytes()); 14 | 15 | // Convert byte array to hexadecimal string 16 | StringBuilder hexString = new StringBuilder(); 17 | for (byte b : messageDigest) { 18 | String hex = Integer.toHexString(0xff & b); 19 | if (hex.length() == 1) { 20 | hexString.append('0'); 21 | } 22 | hexString.append(hex); 23 | } 24 | 25 | return hexString.toString(); 26 | } catch (NoSuchAlgorithmException e) { 27 | // Handle NoSuchAlgorithmException (unavailable algorithm) 28 | e.printStackTrace(); 29 | return null; 30 | } 31 | } 32 | 33 | public static String generateSHA256(String input) { 34 | try { 35 | MessageDigest md = MessageDigest.getInstance("SHA-256"); 36 | 37 | byte[] messageDigest = md.digest(input.getBytes()); 38 | 39 | // Convert byte array to hexadecimal string 40 | StringBuilder hexString = new StringBuilder(); 41 | for (byte b : messageDigest) { 42 | String hex = Integer.toHexString(0xff & b); 43 | if (hex.length() == 1) { 44 | hexString.append('0'); 45 | } 46 | hexString.append(hex); 47 | } 48 | 49 | return hexString.toString(); 50 | } catch (NoSuchAlgorithmException e) { 51 | // Handle NoSuchAlgorithmException (unavailable algorithm) 52 | e.printStackTrace(); 53 | return null; 54 | } 55 | } 56 | 57 | public static String hexToBinary2(String hex) { 58 | HashMap lookup = new HashMap<>(); 59 | 60 | lookup.put('0', "0000"); 61 | lookup.put('1', "0001"); 62 | lookup.put('2', "0010"); 63 | lookup.put('3', "0011"); 64 | lookup.put('4', "0100"); 65 | lookup.put('5', "0101"); 66 | lookup.put('6', "0110"); 67 | lookup.put('7', "0111"); 68 | lookup.put('8', "1000"); 69 | lookup.put('9', "1001"); 70 | lookup.put('a', "1010"); 71 | lookup.put('b', "1011"); 72 | lookup.put('c', "1100"); 73 | lookup.put('d', "1101"); 74 | lookup.put('e', "1110"); 75 | lookup.put('f', "1111"); 76 | lookup.put('A', "1010"); 77 | lookup.put('B', "1011"); 78 | lookup.put('C', "1100"); 79 | lookup.put('D', "1101"); 80 | lookup.put('E', "1110"); 81 | lookup.put('F', "1111"); 82 | 83 | StringBuilder ret = new StringBuilder(); 84 | for (int i = 0, len = hex.length(); i < len; i++) { 85 | ret.append(lookup.get(hex.charAt(i))); 86 | } 87 | return ret.toString(); 88 | } 89 | 90 | public static String hexToBinary(String hex) { 91 | BigInteger num = new BigInteger(hex, 16); 92 | return num.toString(2); 93 | } 94 | 95 | public static String base64Encode(String str) { 96 | byte[] encodedBytes = Base64.getEncoder().encode(str.getBytes()); 97 | return new String(encodedBytes); 98 | } 99 | 100 | public static String base64Decode(String str) { 101 | byte[] decodedBytes = Base64.getDecoder().decode(str.getBytes()); 102 | return new String(decodedBytes); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/details/CustomUserDetails.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.details; 2 | 3 | import com.threlease.base.entites.AuthEntity; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | @Getter 14 | @AllArgsConstructor 15 | public class CustomUserDetails implements UserDetails { 16 | private AuthEntity auth; 17 | 18 | @Override 19 | public Collection getAuthorities() { 20 | return List.of(new SimpleGrantedAuthority(auth.getRole().toString())); 21 | } 22 | 23 | @Override 24 | public String getPassword() { 25 | return auth.getPassword(); 26 | } 27 | 28 | @Override 29 | public String getUsername() { 30 | return auth.getUsername(); 31 | } 32 | 33 | @Override 34 | public boolean isAccountNonExpired() { 35 | return true; 36 | } 37 | 38 | @Override 39 | public boolean isAccountNonLocked() { 40 | return true; 41 | } 42 | 43 | @Override 44 | public boolean isCredentialsNonExpired() { 45 | return true; 46 | } 47 | 48 | @Override 49 | public boolean isEnabled() { 50 | return true; 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/enums/Roles.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum Roles { 9 | ROLE_USER, ROLE_ADMIN 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/exception/TokenValidException.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.exception; 2 | 3 | public class TokenValidException extends RuntimeException { 4 | // 생성자 정의 5 | public TokenValidException(String message) { 6 | super(message); 7 | } 8 | 9 | public TokenValidException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/filter/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.filter; 2 | 3 | import com.threlease.base.common.provider.JwtProvider; 4 | import io.jsonwebtoken.JwtException; 5 | import jakarta.servlet.FilterChain; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.context.SecurityContextHolder; 12 | import org.springframework.web.filter.OncePerRequestFilter; 13 | 14 | import java.io.IOException; 15 | 16 | @RequiredArgsConstructor 17 | public class JwtAuthenticationFilter extends OncePerRequestFilter { 18 | private final JwtProvider jwtProvider; 19 | 20 | @Override 21 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 22 | String token = request.getHeader("Authorization"); 23 | 24 | try { 25 | if (jwtProvider.validateToken(token)) { 26 | Authentication auth = jwtProvider.getAuthentication(token); 27 | SecurityContextHolder.getContext().setAuthentication(auth); 28 | } 29 | } catch (JwtException e) { 30 | throw new RuntimeException(e); 31 | } 32 | 33 | filterChain.doFilter(request, response); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/handler/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.handler; 2 | 3 | import com.threlease.base.common.utils.responses.BasicResponse; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.validation.ObjectError; 6 | import org.springframework.web.bind.MethodArgumentNotValidException; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.RestControllerAdvice; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | @RestControllerAdvice 16 | public class GlobalExceptionHandler { 17 | @ExceptionHandler(value = {NullPointerException.class}) 18 | public ResponseEntity handleNullPointerException(NullPointerException ex, WebRequest request) { 19 | return ResponseEntity.status(400).body( 20 | BasicResponse.builder() 21 | .success(false) 22 | .message(Optional.of(ex.getMessage())) 23 | .data(Optional.empty()) 24 | .build() 25 | ); 26 | } 27 | 28 | @ExceptionHandler(MethodArgumentNotValidException.class) 29 | public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { 30 | List messagesRow = new ArrayList<>(); 31 | for (ObjectError error: ex.getBindingResult().getAllErrors()) { 32 | boolean status = true; 33 | 34 | for (String message: messagesRow) { 35 | if (message.equals(error.getDefaultMessage())) 36 | status = false; 37 | } 38 | 39 | if (status) 40 | messagesRow.add(error.getDefaultMessage()); 41 | } 42 | 43 | StringBuilder messages = new StringBuilder(); 44 | for (String error: messagesRow) { 45 | messages.append(error).append("\n"); 46 | } 47 | 48 | return ResponseEntity.status(400).body( 49 | BasicResponse.builder() 50 | .success(false) 51 | .message(Optional.of(messages.toString())) 52 | .build() 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/interceptors/TokenInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.interceptors; 2 | 3 | import com.threlease.base.common.exception.TokenValidException; 4 | import com.threlease.base.entites.AuthEntity; 5 | import com.threlease.base.functions.auth.AuthService; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.AllArgsConstructor; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | 12 | import java.util.Optional; 13 | 14 | /** 15 | * 토큰 검증 Interceptor 16 | */ 17 | @Component 18 | @AllArgsConstructor 19 | public class TokenInterceptor implements HandlerInterceptor { 20 | private final AuthService authService; 21 | 22 | @Override 23 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 24 | // Authorization 헤더 값 가져오기 25 | String authorizationHeader = request.getHeader("Authorization"); 26 | 27 | // 헤더가 없는 경우 처리 28 | if (authorizationHeader == null || authorizationHeader.isEmpty()) { 29 | throw new TokenValidException("400: Missing Authorization Header"); 30 | } 31 | 32 | // 추가적으로 토큰 검증 로직이 필요한 경우 구현 (예: JWT 토큰 검증) 33 | if (!isValidToken(authorizationHeader)) { 34 | throw new TokenValidException("403:Invalid Token"); 35 | } 36 | 37 | Optional user = authService.findOneByToken(authorizationHeader); 38 | 39 | if (user.isEmpty()) { 40 | throw new TokenValidException("404:User Not Found"); 41 | } 42 | 43 | request.setAttribute("user", user.get()); 44 | 45 | return true; // 다음 단계로 요청 진행 46 | } 47 | 48 | private boolean isValidToken(String token) { 49 | // 여기에 토큰 검증 로직 작성 (JWT 파싱, 서명 확인 등) 50 | return token.toLowerCase().startsWith("bearer "); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/provider/JwtProvider.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.provider; 2 | 3 | import com.threlease.base.entites.AuthEntity; 4 | import com.threlease.base.repositories.auth.AuthRepository; 5 | import com.threlease.base.common.service.CustomUserDetailsService; 6 | import io.jsonwebtoken.Claims; 7 | import io.jsonwebtoken.Jws; 8 | import io.jsonwebtoken.JwtException; 9 | import io.jsonwebtoken.Jwts; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.stereotype.Component; 15 | 16 | import javax.crypto.SecretKey; 17 | import java.util.Date; 18 | import java.util.Optional; 19 | 20 | @Component 21 | @RequiredArgsConstructor 22 | public class JwtProvider { 23 | 24 | private final long exp = 1000L * 60 * 60 * 8; 25 | private final String issuer = "auto-trades"; 26 | private final SecretKey key = Jwts.SIG.HS512.key().build(); 27 | 28 | private final AuthRepository authRepository; 29 | private final CustomUserDetailsService userDetailsService; 30 | 31 | public String sign(String payload) { 32 | return Jwts.builder() 33 | .subject(payload) 34 | .issuer(issuer) 35 | .issuedAt(new Date(System.currentTimeMillis())) 36 | .expiration(new Date(System.currentTimeMillis() + exp)) 37 | .signWith(key) 38 | .compact(); 39 | } 40 | 41 | public Optional> verify(String token) { 42 | if (token == null || token.isEmpty() || !token.substring(0, "Bearer ".length()).equalsIgnoreCase("Bearer ")) { 43 | return Optional.empty(); 44 | } 45 | token = token.split(" ")[1].trim(); 46 | 47 | try { 48 | Jws jws = Jwts.parser() 49 | .verifyWith(key) 50 | .build() 51 | .parseSignedClaims(token); 52 | 53 | if (jws.getPayload().getExpiration().before(new Date())){ 54 | return Optional.empty(); 55 | } 56 | 57 | return Optional.of(jws); 58 | } catch (JwtException e) { 59 | return Optional.empty(); 60 | } 61 | } 62 | 63 | public Optional findOneByToken(String token) { 64 | Optional> verify = this.verify(token); 65 | 66 | if (verify.isPresent()) { 67 | return authRepository.findOneByUUID(verify.get().getPayload().getSubject()); 68 | } else { 69 | return Optional.empty(); 70 | } 71 | } 72 | 73 | public boolean validateToken(String token) { 74 | if (token == null || token.isEmpty() || !token.substring(0, "Bearer ".length()).equalsIgnoreCase("Bearer ")) { 75 | return false; 76 | } 77 | token = token.split(" ")[1].trim(); 78 | 79 | try { 80 | Jws jws = Jwts.parser() 81 | .verifyWith(key) 82 | .build() 83 | .parseSignedClaims(token); 84 | 85 | return !jws.getPayload().getExpiration().before(new Date()); 86 | } catch (JwtException e) { 87 | return false; 88 | } 89 | } 90 | 91 | public Authentication getAuthentication(String token) { 92 | UserDetails userDetails = userDetailsService.loadUserByUsername(verify(token).isPresent() ? verify(token).get().getPayload().getSubject() : ""); 93 | return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/service/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.service; 2 | 3 | import com.threlease.base.common.details.CustomUserDetails; 4 | import com.threlease.base.entites.AuthEntity; 5 | import com.threlease.base.repositories.auth.AuthRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.Optional; 14 | 15 | @Slf4j 16 | @Component 17 | @RequiredArgsConstructor 18 | public class CustomUserDetailsService implements UserDetailsService { 19 | 20 | private final AuthRepository authRepository; 21 | 22 | @Override 23 | public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException { 24 | Optional member = authRepository.findOneByUUID(uuid); 25 | member 26 | .orElseThrow(() -> new UsernameNotFoundException("User not found in the database")); 27 | 28 | return new CustomUserDetails(member.get()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/utils/Failable.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.utils; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class Failable { 7 | private boolean isError; 8 | private T value; 9 | private E error; 10 | 11 | private Failable(T value, E error, boolean isError) { 12 | this.value = value; 13 | this.error = error; 14 | this.isError = isError; 15 | } 16 | 17 | public static Failable success(T value) { 18 | return new Failable<>(value, null, false); 19 | } 20 | 21 | public static Failable error(E error) { 22 | return new Failable<>(null, error, true); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/utils/enumeration/EnumValueValidator.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.utils.enumeration; 2 | 3 | import jakarta.validation.ConstraintValidator; 4 | import jakarta.validation.ConstraintValidatorContext; 5 | 6 | public class EnumValueValidator implements ConstraintValidator> { 7 | 8 | private Class> enumClass; 9 | private Enumeration annotation; 10 | 11 | @Override 12 | public void initialize(Enumeration constraintAnnotation) { 13 | enumClass = constraintAnnotation.enumClass(); 14 | this.annotation = constraintAnnotation; 15 | } 16 | 17 | @Override 18 | public boolean isValid(Enum value, ConstraintValidatorContext context) { 19 | if (annotation.optional()) { 20 | if (value == null) { 21 | return true; // Optional한 필드면 유효하다고 간주 22 | } 23 | } else { 24 | if (value == null) { 25 | context.disableDefaultConstraintViolation(); 26 | context.buildConstraintViolationWithTemplate(annotation.message()) 27 | .addConstraintViolation(); 28 | return false; 29 | } 30 | } 31 | 32 | for (Enum enumValue : enumClass.getEnumConstants()) { 33 | if (enumValue.equals(value)) { 34 | return true; 35 | } 36 | } 37 | 38 | context.disableDefaultConstraintViolation(); 39 | context.buildConstraintViolationWithTemplate(annotation.message()) 40 | .addConstraintViolation(); 41 | return false; 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/utils/enumeration/Enumeration.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.utils.enumeration; 2 | 3 | import jakarta.validation.Constraint; 4 | import jakarta.validation.Payload; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Target({ElementType.FIELD, ElementType.PARAMETER}) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Constraint(validatedBy = {EnumValueValidator.class}) 14 | public @interface Enumeration { 15 | String message() default "Invalid value Enum"; // 기본 메시지 16 | 17 | Class[] groups() default {}; 18 | 19 | Class[] payload() default {}; 20 | 21 | Class> enumClass(); 22 | 23 | boolean optional() default false; 24 | 25 | // 커스텀 메시지를 지원하기 위해 추가 26 | String customMessage() default ""; 27 | } -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/utils/random/GetRandom.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.utils.random; 2 | 3 | import java.math.BigInteger; 4 | import java.security.SecureRandom; 5 | 6 | public class GetRandom { 7 | private static final String NUMERIC = "0123456789"; 8 | private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; 9 | private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 10 | private static final String ALPHANUMERIC = NUMERIC + LOWERCASE + UPPERCASE; 11 | 12 | private static String getCharacters(RandomType type) { 13 | return switch (type) { 14 | case NUMBER -> NUMERIC; 15 | case LOWERCASE -> LOWERCASE; 16 | case UPPERCASE -> UPPERCASE; 17 | case ALL -> ALPHANUMERIC; 18 | default -> throw new IllegalArgumentException("Invalid type: " + type); 19 | }; 20 | } 21 | 22 | public static String run(RandomType type, int length) { 23 | StringBuilder result = new StringBuilder(); 24 | String characters = getCharacters(type); 25 | SecureRandom random = new SecureRandom(); 26 | 27 | for (int i = 0; i < length; i++) { 28 | int randomIndex = random.nextInt(characters.length()); 29 | result.append(characters.charAt(randomIndex)); 30 | } 31 | 32 | return result.toString(); 33 | } 34 | 35 | public static String generateRandomHexString(int length) { 36 | SecureRandom random = new SecureRandom(); 37 | byte[] bytes = new byte[length]; 38 | random.nextBytes(bytes); 39 | BigInteger num = new BigInteger(1, bytes); 40 | return String.format("%0" + (length << 1) + "x", num); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/utils/random/RandomType.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.utils.random; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum RandomType { 9 | ALL, NUMBER, LOWERCASE, UPPERCASE 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/utils/responses/BasicResponse.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.utils.responses; 2 | // compileOnly 'org.projectlombok:lombok' 3 | // annotationProcessor 'org.projectlombok:lombok' 4 | // com.google.gson.Gson 5 | 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | import com.google.gson.Gson; 11 | 12 | import java.util.Optional; 13 | 14 | @Data 15 | @Getter 16 | @Setter 17 | @Builder 18 | public class BasicResponse { 19 | private boolean success; 20 | private Optional message; 21 | private Optional data; 22 | 23 | public String toJson() { 24 | Gson gson = new Gson(); 25 | 26 | return gson.toJson(this); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/common/utils/responses/Messages.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.common.utils.responses; 2 | 3 | public class Messages { 4 | public static String SESSION_ERROR = "세션이 만료 되었거나 인증에 문제가 생겼습니다."; 5 | 6 | public static String NOT_FOUND_USER = "해당 유저를 찾을 수 없습니다."; 7 | 8 | public static String DUPLICATE_USER = "이미 해당 아이디를 사용하는 유저가 있습니다."; 9 | 10 | public static String WRONG_AUTH = "아이디 혹은 비밀번호를 확인해주세요."; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/entites/AuthEntity.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.entites; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.threlease.base.common.enums.Roles; 5 | import jakarta.persistence.*; 6 | import lombok.*; 7 | import org.springframework.data.annotation.CreatedDate; 8 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 9 | 10 | import java.time.LocalDateTime; 11 | 12 | @Builder 13 | @Entity 14 | @Getter 15 | @Setter 16 | @Table(name = "AuthEntity") 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | @EntityListeners(AuditingEntityListener.class) 20 | public class AuthEntity { 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.UUID) 23 | @Column(length = 36, nullable = false) 24 | private String uuid; 25 | 26 | @Column(length = 24, nullable = false, unique = true) 27 | private String username; 28 | 29 | @Column(length = 36, nullable = false) 30 | private String nickname; 31 | 32 | @JsonIgnore 33 | @Column(columnDefinition = "text", nullable = false) 34 | private String password; 35 | 36 | @JsonIgnore 37 | @Column(length = 32, nullable = false) 38 | private String salt; 39 | 40 | @CreatedDate 41 | private LocalDateTime createdAt; 42 | 43 | @Enumerated(EnumType.STRING) 44 | private Roles role; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/functions/auth/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.functions.auth; 2 | 3 | import com.threlease.base.entites.AuthEntity; 4 | import com.threlease.base.common.enums.Roles; 5 | import com.threlease.base.functions.auth.dto.LoginDto; 6 | import com.threlease.base.functions.auth.dto.SignUpDto; 7 | import com.threlease.base.common.crypto.Hash; 8 | import com.threlease.base.common.utils.random.GetRandom; 9 | import com.threlease.base.common.utils.random.RandomType; 10 | import com.threlease.base.common.utils.responses.BasicResponse; 11 | import com.threlease.base.common.utils.responses.Messages; 12 | import io.swagger.v3.oas.annotations.tags.Tag; 13 | import jakarta.validation.Valid; 14 | import lombok.AllArgsConstructor; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import java.time.LocalDateTime; 19 | import java.util.Objects; 20 | import java.util.Optional; 21 | 22 | @RestController 23 | @RequestMapping("/auth") 24 | @Tag(name = "Auth API") 25 | @AllArgsConstructor 26 | public class AuthController { 27 | private final AuthService authService; 28 | 29 | @PostMapping("/login") 30 | @Operation(summary = "로그인") 31 | private ResponseEntity> login( 32 | @RequestBody @Valid LoginDto dto 33 | ) { 34 | Optional auth = authService.findOneByUsername(dto.getUsername()); 35 | 36 | if (auth.isEmpty()) 37 | return ResponseEntity.status(404).body( 38 | BasicResponse.builder() 39 | .success(false) 40 | .message(Optional.of(Messages.NOT_FOUND_USER)) 41 | .build() 42 | ); 43 | 44 | if (!Objects.equals(auth.get().getPassword(), Hash.generateSHA512(dto.getPassword() + auth.get().getSalt()))) 45 | return ResponseEntity.status(403).body( 46 | BasicResponse.builder() 47 | .success(false) 48 | .message(Optional.of(Messages.WRONG_AUTH)) 49 | .build() 50 | ); 51 | 52 | return ResponseEntity.status(201).body( 53 | BasicResponse.builder() 54 | .success(true) 55 | .message(Optional.empty()) 56 | .data(Optional.ofNullable(authService.sign(auth.get()))) 57 | .build() 58 | ); 59 | } 60 | 61 | @PostMapping("/signup") 62 | @Operation(summary = "회원가입") 63 | private ResponseEntity> signUp( 64 | @RequestBody @Valid SignUpDto dto 65 | ) { 66 | Optional auth = authService.findOneByUsername(dto.getUsername()); 67 | 68 | if (auth.isPresent()) 69 | return ResponseEntity.status(403).body( 70 | BasicResponse.builder() 71 | .success(false) 72 | .message(Optional.of(Messages.DUPLICATE_USER)) 73 | .build() 74 | ); 75 | 76 | String salt = GetRandom.run(RandomType.ALL, 32); 77 | 78 | AuthEntity user = AuthEntity.builder() 79 | .username(dto.getUsername()) 80 | .password(Hash.generateSHA512(dto.getPassword() + salt)) 81 | .salt(salt) 82 | .role(Roles.ROLE_USER) 83 | .createdAt(LocalDateTime.now()) 84 | .build(); 85 | 86 | authService.authSave(user); 87 | 88 | return ResponseEntity.status(201).body( 89 | BasicResponse.builder() 90 | .success(true) 91 | .data(Optional.of(user)) 92 | .build() 93 | ); 94 | } 95 | 96 | @GetMapping("/@me") 97 | @Operation(summary = "인증") 98 | private ResponseEntity> verify( 99 | @RequestHeader("Authorization") String token 100 | ) { 101 | Optional user = authService.findOneByToken(token); 102 | 103 | return user.map(authEntity -> ResponseEntity.status(200).body( 104 | BasicResponse.builder() 105 | .success(true) 106 | .message(Optional.empty()) 107 | .data(Optional.of(authEntity)) 108 | .build() 109 | )).orElseGet(() -> ResponseEntity.status(403).body( 110 | BasicResponse.builder() 111 | .success(false) 112 | .message(Optional.of(Messages.SESSION_ERROR)) 113 | .build() 114 | )); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/functions/auth/AuthService.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.functions.auth; 2 | 3 | import com.threlease.base.entites.AuthEntity; 4 | import com.threlease.base.repositories.AuthRepository; 5 | import com.threlease.base.common.provider.JwtProvider; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.Optional; 9 | @Service 10 | public class AuthService { 11 | private final AuthRepository authRepository; 12 | private final JwtProvider jwtProvider; 13 | 14 | public AuthService(AuthRepository authRepository, JwtProvider jwtProvider) { 15 | this.authRepository = authRepository; 16 | this.jwtProvider = jwtProvider; 17 | } 18 | 19 | public Optional findOneByUUID(String uuid) { 20 | return authRepository.findOneByUUID(uuid); 21 | } 22 | public Optional findOneByUsername(String username) { 23 | return authRepository.findOneByUsername(username); 24 | } 25 | public void authSave(AuthEntity auth) { 26 | authRepository.save(auth); 27 | } 28 | 29 | public String sign(AuthEntity user) { 30 | return jwtProvider.sign(user.getUuid()); 31 | } 32 | 33 | public Optional findOneByToken(String token) { 34 | return jwtProvider.findOneByToken(token); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/functions/auth/dto/LoginDto.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.functions.auth.dto; 2 | 3 | import lombok.Data; 4 | import lombok.Getter; 5 | 6 | @Data 7 | @Getter 8 | public class LoginDto { 9 | private String username; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/functions/auth/dto/SignUpDto.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.functions.auth.dto; 2 | 3 | import lombok.Data; 4 | import lombok.Getter; 5 | 6 | @Data 7 | @Getter 8 | public class SignUpDto { 9 | private String username; 10 | private String nickname; 11 | private String password; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/repositories/auth/AuthRepository.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.repositories.auth; 2 | 3 | import com.threlease.base.entites.AuthEntity; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.query.Param; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.Optional; 12 | 13 | @Repository 14 | public interface AuthRepository extends JpaRepository, UserCustomRepository { 15 | @Query("SELECT u FROM AuthEntity u WHERE u.uuid = :uuid") 16 | Optional findOneByUUID(@Param("uuid") String uuid); 17 | 18 | @Query(value = "SELECT u FROM AuthEntity u") 19 | Page findByPagination(Pageable pageable); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/repositories/auth/UserCustomRepository.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.repositories.auth; 2 | 3 | import com.threlease.base.entites.AuthEntity; 4 | 5 | import java.util.Optional; 6 | 7 | public interface UserCustomRepository { 8 | Optional findOneByUsername(String username); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/threlease/base/repositories/auth/UserCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base.repositories.auth; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import com.threlease.base.entites.AuthEntity; 5 | import com.threlease.base.entites.QAuthEntity; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.Optional; 10 | 11 | @Repository 12 | @AllArgsConstructor 13 | public class UserCustomRepositoryImpl implements UserCustomRepository { 14 | private final JPAQueryFactory queryFactory; 15 | 16 | private final QAuthEntity authEntity = QAuthEntity.authEntity; 17 | 18 | @Override 19 | public Optional findOneByUsername(String username) { 20 | return Optional.ofNullable( 21 | queryFactory 22 | .selectFrom(authEntity) 23 | .where(authEntity.username.eq(username)) 24 | .fetchOne() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:mysql://localhost:3306/{tablename} 4 | username: root 5 | password: password 6 | driver-class-name: com.mysql.cj.jdbc.Driver 7 | jpa: 8 | hibernate: 9 | ddl-auto: update 10 | properties: # property ?? ?? 11 | hibernate: # hibernate property ?? 12 | format_sql: true 13 | servlet: 14 | multipart: 15 | max-file-size: 10MB 16 | max-request-size: 10MB 17 | 18 | server: 19 | forward-headers-strategy: FRAMEWORK 20 | servlet: 21 | contextPath: /api 22 | 23 | 24 | springdoc: 25 | swagger-ui: 26 | path: /swagger # swagger-ui 접근 경로에 대한 별칭, 해당 주소로 접속해도 http://localhost:8080/swagger-ui/index.html로 리다이렉션 됨. 27 | groups-order: DESC # path, query, body, response 순으로 출력 28 | tags-sorter: alpha # 태그를 알파벳 순으로 정렬 29 | operations-sorter: method # delete - get - patch - post - put 순으로 정렬, alpha를 사용하면 알파벳 순으로 정렬 가능 30 | paths-to-match: 31 | - /** # swagger-ui에 표시할 api의 엔드 포인트 패턴 32 | -------------------------------------------------------------------------------- /src/test/java/com/threlease/base/BaseApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.threlease.base; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class BaseApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------