from auth-service

This commit is contained in:
Hyojin Ahn 2025-11-19 08:33:51 -05:00
commit 0b6b790425
45 changed files with 2453 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

BIN
.mvn/wrapper/maven-wrapper.jar vendored Normal file

Binary file not shown.

2
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@ -0,0 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

34
README.md Normal file
View File

@ -0,0 +1,34 @@
# Spring Boot 3.0 Security with JWT Implementation
This project demonstrates the implementation of security using Spring Boot 3.0 and JSON Web Tokens (JWT). It includes the following features:
## Features
* User registration and login with JWT authentication
* Password encryption using BCrypt
* Role-based authorization with Spring Security
* Customized access denied handling
* Logout mechanism
* Refresh token
## Technologies
* Spring Boot 3.0
* Spring Security
* JSON Web Tokens (JWT)
* BCrypt
* Maven
## Getting Started
To get started with this project, you will need to have the following installed on your local machine:
* JDK 17+
* Maven 3+
To build and run the project, follow these steps:
* Clone the repository: `git clone https://github.com/ali-bouali/spring-boot-3-jwt-security.git`
* Navigate to the project directory: cd spring-boot-security-jwt
* Add database "jwt_security" to postgres
* Build the project: mvn clean install
* Run the project: mvn spring-boot:run
-> The application will be available at http://localhost:8080.

38
docker-compose.yml Normal file
View File

@ -0,0 +1,38 @@
services:
postgres:
container_name: postgres-sql
image: postgres
environment:
POSTGRES_USER: username
POSTGRES_PASSWORD: password
PGDATA: /data/postgres
volumes:
- postgres:/data/postgres
ports:
- "5432:5432"
networks:
- postgres
restart: unless-stopped
pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
PGADMIN_CONFIG_SERVER_MODE: 'False'
volumes:
- pgadmin:/var/lib/pgadmin
ports:
- "5050:80"
networks:
- postgres
restart: unless-stopped
networks:
postgres:
driver: bridge
volumes:
postgres:
pgadmin:

316
mvnw vendored Normal file
View File

@ -0,0 +1,316 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`\\unset -f command; \\command -v java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

188
mvnw.cmd vendored Normal file
View File

@ -0,0 +1,188 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

102
pom.xml Normal file
View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.goi</groupId>
<artifactId>auth-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>template</groupId>
<artifactId>layered-architecture-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,44 @@
package com.goi.erp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
@EntityScan(basePackages = {"com.goi.erp"})
@EnableJpaRepositories(basePackages = {"com.goi.erp"})
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
// @Bean
// public CommandLineRunner commandLineRunner(
// AuthenticationService service
// ) {
// return args -> {
// var admin = RegisterRequest.builder()
// .firstname("Admin")
// .lastname("Admin")
// .email("admin@mail.com")
// .password("password")
// .role(ADMIN)
// .build();
// System.out.println("Admin token: " + service.register(admin).getAccessToken());
//
// var manager = RegisterRequest.builder()
// .firstname("Admin")
// .lastname("Admin")
// .email("manager@mail.com")
// .password("password")
// .role(MANAGER)
// .build();
// System.out.println("Manager token: " + service.register(manager).getAccessToken());
//
// };
// }
}

View File

@ -0,0 +1,30 @@
package com.goi.erp.auditing;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.goi.erp.employee.EmployeeDetails;
import java.util.Optional;
public class ApplicationAuditAware implements AuditorAware<Integer> {
@Override
public Optional<Integer> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null ||
!authentication.isAuthenticated() ||
authentication instanceof AnonymousAuthenticationToken) {
return Optional.empty();
}
// EmployeeDetails로 캐스팅
EmployeeDetails employeeDetails = (EmployeeDetails) authentication.getPrincipal();
// 내부 PK(empId) 반환
return Optional.ofNullable(employeeDetails.getEmployee().getEmpId());
}
}

View File

@ -0,0 +1,37 @@
package com.goi.erp.auth;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService service;
// @PostMapping("/register")
// public ResponseEntity<AuthenticationResponse> register(
// @RequestBody RegisterRequest request
// ) {
// return ResponseEntity.ok(service.register(request));
// }
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
return ResponseEntity.ok(service.authenticate(request));
}
@PostMapping("/refresh-token")
public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
service.refreshToken(request, response);
}
}

View File

@ -0,0 +1,16 @@
package com.goi.erp.auth;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequest {
private String empLoginId; // 기존 email 대신
private String empLoginPassword;
}

View File

@ -0,0 +1,19 @@
package com.goi.erp.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("refresh_token")
private String refreshToken;
}

View File

@ -0,0 +1,158 @@
package com.goi.erp.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goi.erp.config.JwtService;
import com.goi.erp.token.Token;
import com.goi.erp.token.TokenRepository;
import com.goi.erp.token.TokenType;
import com.goi.erp.employee.Employee;
import com.goi.erp.employee.EmployeeRepository;
import com.goi.erp.employee.EmployeeRole;
import com.goi.erp.employee.EmployeeRoleRepository;
import com.goi.erp.role.RolePermissionRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
private final TokenRepository tokenRepository;
private final PasswordEncoder passwordEncoder;
private final RolePermissionRepository rolePermissionRepository;
private final JwtService jwtService;
// private final AuthenticationManager authenticationManager;
// public AuthenticationResponse register(RegisterRequest request) {
// var user = User.builder().firstname(request.getFirstname()).lastname(request.getLastname())
// .email(request.getEmail()).password(passwordEncoder.encode(request.getPassword()))
// .role(request.getRole()).build();
// var savedUser = repository.save(user);
// var jwtToken = jwtService.generateToken(user);
// var refreshToken = jwtService.generateRefreshToken(user);
// saveUserToken(savedUser, jwtToken);
// return AuthenticationResponse.builder().accessToken(jwtToken).refreshToken(refreshToken).build();
// }
// 로그인 처리
public AuthenticationResponse authenticate(AuthenticationRequest request) {
// 1. Employee 조회
Employee employee = employeeRepository.findByEmpLoginId(request.getEmpLoginId())
.orElseThrow(() -> new RuntimeException("Employee not found"));
// 2. 비밀번호 검증
if (!passwordEncoder.matches(request.getEmpLoginPassword(), employee.getEmpLoginPassword())) {
throw new RuntimeException("Invalid password");
}
// 3. EmployeeRole 조회 Role 이름 리스트 생성
List<EmployeeRole> activeRoles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
List<String> roles = activeRoles.stream()
.map(er -> er.getRoleInfo().getRoleName())
.collect(Collectors.toList());
// 4. Role Permission 조회
List<String> permissions = activeRoles.stream()
.flatMap(er -> rolePermissionRepository.findByRoleId(er.getRoleInfo().getRoleId()).stream())
.map(rp -> rp.getPermissionInfo().getPermModule() + ":" + rp.getPermissionInfo().getPermAction() + ":" + rp.getPermissionInfo().getPermScope())
.distinct()
.collect(Collectors.toList());
// 5. generate token
String jwtToken = jwtService.generateToken(employee, roles, permissions);
String refreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
// 기존 토큰 회수 토큰 저장
revokeAllEmployeeTokens(employee);
saveEmployeeToken(employee, jwtToken);
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken)
.build();
}
// JWT 토큰 저장
private void saveEmployeeToken(Employee employee, String jwtToken) {
Token token = Token.builder()
.employee(employee)
.token(jwtToken)
.tokenType(TokenType.BEARER)
.expired(false)
.revoked(false)
.build();
tokenRepository.save(token);
}
// 기존 토큰 회수
private void revokeAllEmployeeTokens(Employee employee) {
List<Token> validTokens = tokenRepository.findAllValidTokenByEmployee(employee.getEmpId());
if (validTokens.isEmpty()) return;
validTokens.forEach(token -> {
token.setExpired(true);
token.setRevoked(true);
});
tokenRepository.saveAll(validTokens);
}
// 리프레시 토큰 처리
public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return;
}
final String refreshToken = authHeader.substring(7);
String empLoginId = jwtService.extractUsername(refreshToken);
if (empLoginId != null) {
Employee employee = employeeRepository.findByEmpLoginId(empLoginId)
.orElseThrow(() -> new RuntimeException("Employee not found"));
if (jwtService.isTokenValid(refreshToken, employee)) {
// 3. EmployeeRole 조회 Role 이름 리스트 생성
List<EmployeeRole> activeRoles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
List<String> roles = activeRoles.stream()
.map(er -> er.getRoleInfo().getRoleName())
.collect(Collectors.toList());
// 4. Role Permission 조회
List<String> permissions = activeRoles.stream()
.flatMap(er -> rolePermissionRepository.findByRoleId(er.getRoleInfo().getRoleId()).stream())
.map(rp -> rp.getPermissionInfo().getPermModule() + ":" + rp.getPermissionInfo().getPermAction() + ":" + rp.getPermissionInfo().getPermScope())
.distinct()
.collect(Collectors.toList());
// 5. generate token
String accessToken = jwtService.generateToken(employee, roles, permissions);
revokeAllEmployeeTokens(employee);
saveEmployeeToken(employee, accessToken);
AuthenticationResponse authResponse = AuthenticationResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
new ObjectMapper().writeValue(response.getOutputStream(), authResponse);
}
}
}
}

View File

@ -0,0 +1,21 @@
package com.goi.erp.auth;
import com.goi.erp.user.Role;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
private String firstname;
private String lastname;
private String email;
private String password;
private Role role;
}

View File

@ -0,0 +1,83 @@
package com.goi.erp.config;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.goi.erp.auditing.ApplicationAuditAware;
import com.goi.erp.employee.Employee;
import com.goi.erp.employee.EmployeeRole;
import com.goi.erp.employee.EmployeeRoleRepository;
import com.goi.erp.employee.EmployeeDetails;
import com.goi.erp.employee.EmployeeRepository;
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
@Bean
public UserDetailsService userDetailsService() {
return username -> {
Employee employee = employeeRepository.findByEmpLoginId(username)
.orElseThrow(() -> new UsernameNotFoundException("Employee not found"));
// EmployeeRole 조회
List<EmployeeRole> roles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
// EmployeeDetails 생성
return new EmployeeDetails(employee, roles);
};
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuditorAware<Integer> auditorAware() {
return new ApplicationAuditAware();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
String rawPassword = "1111"; // 테스트할 비밀번호
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String hashedPassword = passwordEncoder.encode(rawPassword);
System.out.println("Raw password: " + rawPassword);
System.out.println("Hashed password: " + hashedPassword);
// 확인용 matches 테스트
boolean matches = passwordEncoder.matches(rawPassword, hashedPassword);
System.out.println("Matches: " + matches);
}
}

View File

@ -0,0 +1,91 @@
package com.goi.erp.config;
import com.goi.erp.employee.Employee;
import com.goi.erp.employee.EmployeeDetails;
import com.goi.erp.employee.EmployeeRepository;
import com.goi.erp.employee.EmployeeRole;
import com.goi.erp.employee.EmployeeRoleRepository;
import com.goi.erp.token.TokenRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
private final TokenRepository tokenRepository;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
// 인증 API는 필터링 제외
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}
final String authHeader = request.getHeader("Authorization");
final String jwt;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
final String empUuidStr = jwtService.extractUsername(jwt);
if (empUuidStr != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UUID empUuid = UUID.fromString(empUuidStr);
// Employee 조회
Employee employee = employeeRepository.findByEmpUuid(empUuid)
.orElseThrow(() -> new RuntimeException("Employee not found"));
// Role 조회
List<EmployeeRole> roles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
EmployeeDetails employeeDetails = new EmployeeDetails(employee, roles);
// DB 토큰 검증
boolean isTokenValid = tokenRepository.findByToken(jwt)
.map(t -> !t.isExpired() && !t.isRevoked())
.orElse(false);
// JWT 유효성 검증 + DB 토큰 검증
if (jwtService.isTokenValid(jwt, employee) && isTokenValid) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
employeeDetails,
null,
employeeDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,132 @@
package com.goi.erp.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.goi.erp.employee.Employee;
@Service
public class JwtService {
@Value("${application.security.jwt.secret-key}")
private String secretKey;
@Value("${application.security.jwt.expiration}")
private long jwtExpiration;
@Value("${application.security.jwt.refresh-token.expiration}")
private long refreshExpiration;
// =================== 기존 UserDetails용 ===================
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(Employee employee, List<String> roles, List<String> permissions) {
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("roles", roles);
// Admin 계정 여부 확인
boolean isAdmin = roles.stream().anyMatch(r -> r.equalsIgnoreCase("Admin"));
if (isAdmin) {
// Admin이면 permissions를 ALL로 단순화
extraClaims.put("permissions", List.of("ALL"));
} else {
// 일반 계정이면 상세 권한 넣기
extraClaims.put("permissions", permissions);
}
return buildToken(extraClaims, employee.getEmpUuid().toString(), jwtExpiration);
}
public String generateRefreshToken(Employee employee, List<String> roles, List<String> permissions) {
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("roles", roles);
// Admin 계정 여부 확인
boolean isAdmin = roles.stream().anyMatch(r -> r.equalsIgnoreCase("Admin"));
if (isAdmin) {
// Admin이면 permissions를 ALL로 단순화
extraClaims.put("permissions", List.of("ALL"));
} else {
// 일반 계정이면 상세 권한 넣기
extraClaims.put("permissions", permissions);
}
return buildToken(extraClaims, employee.getEmpUuid().toString(), refreshExpiration);
}
private String buildToken(Map<String, Object> extraClaims, String subject, long expiration) {
return Jwts.builder().setClaims(extraClaims).setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256).compact();
}
public boolean isTokenValid(String token, Employee employee) {
final String username = extractUsername(token);
return (username.equals(employee.getEmpUuid().toString())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(getSignInKey()).build().parseClaimsJws(token).getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
public static void main(String[] args) {
JwtService jwtService = new JwtService();
jwtService.secretKey = "D0HaHnTPKLkUO9ULL1Ulm6XDZjhzuFtvTCcxTxSoCS8=";
String token = "eyJhbGciOiJIUzI1NiJ9.eyJwZXJtaXNzaW9ucyI6WyJIOlI6UCIsIk86QzpBIiwiTzpSOkEiLCJPOlU6QSIsIk86RDpBIiwiUzpDOkEiLCJTOlI6QSIsIlM6VTpBIl0sInJvbGVzIjpbIk9wZXJhdGlvbnMgTWFuYWdlciJdLCJzdWIiOiJmZGE1NGZkZS03MTBmLTQ4ZDItYTRmYi00NzM2YjJhM2RhNWEiLCJpYXQiOjE3NjMxMzU4MzMsImV4cCI6MTc2MzIyMjIzM30.ie38b2JnkP3k4Vz7TzAwI7oRgOsIFYf0yMYADq5EhNM";
// user 정보
Claims claims = jwtService.extractAllClaims(token);
System.out.println("Subject (emp_uuid): " + claims.getSubject());
System.out.println("Roles: " + claims.get("roles"));
System.out.println("Roles: " + claims.get("permissions"));
System.out.println("IssuedAt: " + claims.getIssuedAt());
System.out.println("Expiration: " + claims.getExpiration());
// 모든 Claims 확인
// Claims claims = Jwts.parserBuilder()
// .setSigningKey(Keys.hmacShaKeyFor("<secret_key>".getBytes()))
// .build()
// .parseClaimsJws(token)
// .getBody();
System.out.println("Claims: " + claims);
}
}

View File

@ -0,0 +1,40 @@
package com.goi.erp.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;
import com.goi.erp.token.TokenRepository;
@Service
@RequiredArgsConstructor
public class LogoutService implements LogoutHandler {
private final TokenRepository tokenRepository;
@Override
public void logout(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) {
final String authHeader = request.getHeader("Authorization");
final String jwt;
if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
return;
}
jwt = authHeader.substring(7);
var storedToken = tokenRepository.findByToken(jwt)
.orElse(null);
if (storedToken != null) {
storedToken.setExpired(true);
storedToken.setRevoked(true);
tokenRepository.save(storedToken);
SecurityContextHolder.clearContext();
}
}
}

View File

@ -0,0 +1,54 @@
package com.goi.erp.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
@OpenAPIDefinition(
info = @Info(
contact = @Contact(
name = "Alibou",
email = "contact@aliboucoding.com",
url = "https://aliboucoding.com/course"
),
description = "OpenApi documentation for Spring Security",
title = "OpenApi specification - Alibou",
version = "1.0",
license = @License(
name = "Licence name",
url = "https://some-url.com"
),
termsOfService = "Terms of service"
),
servers = {
@Server(
description = "Local ENV",
url = "http://localhost:8080"
),
@Server(
description = "PROD ENV",
url = "https://aliboucoding.com/course"
)
},
security = {
@SecurityRequirement(
name = "bearerAuth"
)
}
)
@SecurityScheme(
name = "bearerAuth",
description = "JWT auth description",
scheme = "bearer",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER
)
public class OpenApiConfig {
}

View File

@ -0,0 +1,81 @@
package com.goi.erp.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import static com.goi.erp.user.Permission.ADMIN_CREATE;
import static com.goi.erp.user.Permission.ADMIN_DELETE;
import static com.goi.erp.user.Permission.ADMIN_READ;
import static com.goi.erp.user.Permission.ADMIN_UPDATE;
import static com.goi.erp.user.Permission.MANAGER_CREATE;
import static com.goi.erp.user.Permission.MANAGER_DELETE;
import static com.goi.erp.user.Permission.MANAGER_READ;
import static com.goi.erp.user.Permission.MANAGER_UPDATE;
import static com.goi.erp.user.Role.ADMIN;
import static com.goi.erp.user.Role.MANAGER;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {
private static final String[] WHITE_LIST_URL = {
"/auth/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/swagger-ui.html"};
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(req ->
req.requestMatchers(WHITE_LIST_URL)
.permitAll()
.requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())
.requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name())
.requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name())
.requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name())
.anyRequest()
.authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout(logout ->
logout.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
)
;
return http.build();
}
}

View File

@ -0,0 +1,40 @@
package com.goi.erp.demo;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
@GetMapping
@PreAuthorize("hasAuthority('admin:read')")
public String get() {
return "GET:: admin controller";
}
@PostMapping
@PreAuthorize("hasAuthority('admin:create')")
@Hidden
public String post() {
return "POST:: admin controller";
}
@PutMapping
@PreAuthorize("hasAuthority('admin:update')")
@Hidden
public String put() {
return "PUT:: admin controller";
}
@DeleteMapping
@PreAuthorize("hasAuthority('admin:delete')")
@Hidden
public String delete() {
return "DELETE:: admin controller";
}
}

View File

@ -0,0 +1,19 @@
package com.goi.erp.demo;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/demo-controller")
@Hidden
public class DemoController {
@GetMapping
public ResponseEntity<String> sayHello() {
return ResponseEntity.ok("Hello from secured endpoint");
}
}

View File

@ -0,0 +1,50 @@
package com.goi.erp.demo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/management")
@Tag(name = "Management")
public class ManagementController {
@Operation(
description = "Get endpoint for manager",
summary = "This is a summary for management get endpoint",
responses = {
@ApiResponse(
description = "Success",
responseCode = "200"
),
@ApiResponse(
description = "Unauthorized / Invalid Token",
responseCode = "403"
)
}
)
@GetMapping
public String get() {
return "GET:: management controller";
}
@PostMapping
public String post() {
return "POST:: management controller";
}
@PutMapping
public String put() {
return "PUT:: management controller";
}
@DeleteMapping
public String delete() {
return "DELETE:: management controller";
}
}

View File

@ -0,0 +1,49 @@
package com.goi.erp.employee;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "employee")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "emp_id")
private Integer empId; // 내부 PK, 외부 노출 X
@Column(name = "emp_uuid", unique = true, nullable = false)
private UUID empUuid; // 외부 키로 사용
@Column(name = "emp_login_id", unique = true, nullable = false)
private String empLoginId;
@Column(name = "emp_login_password", nullable = false)
private String empLoginPassword;
@Column(name = "emp_first_name")
private String empFirstName;
@Column(name = "emp_last_name")
private String empLastName;
@Column(name = "emp_dept_id")
private Integer empDeptId;
@Column(name = "emp_status", columnDefinition = "CHAR(1)")
private String empStatus;
}

View File

@ -0,0 +1,62 @@
package com.goi.erp.employee;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Getter
public class EmployeeDetails implements UserDetails {
// UID는 객체를 직렬화 다시 역직렬화할 , 클래스 버전이 맞는지 확인하는 용도
private static final long serialVersionUID = 1L;
private final Employee employee;
private final List<GrantedAuthority> authorities;
public EmployeeDetails(Employee employee, List<EmployeeRole> roles) {
this.employee = employee;
// EmployeeRole GrantedAuthority 변환
this.authorities = roles.stream()
.map(er -> new SimpleGrantedAuthority(er.getRoleInfo().getRoleName()))
.collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return employee.getEmpLoginPassword();
}
@Override
public String getUsername() {
return employee.getEmpLoginId();
}
@Override
public boolean isAccountNonExpired() {
return true; // 필요에 따라 Employee 상태로 제어 가능
}
@Override
public boolean isAccountNonLocked() {
return !"I".equals(employee.getEmpStatus()); // : 'I' 잠금
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return "A".equals(employee.getEmpStatus()); // Active 상태만 사용 가능
}
}

View File

@ -0,0 +1,30 @@
package com.goi.erp.employee;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class EmployeeDetailsService implements UserDetailsService {
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
@Override
public UserDetails loadUserByUsername(String empLoginId) throws UsernameNotFoundException {
// Employee 조회
Employee employee = employeeRepository.findByEmpLoginId(empLoginId)
.orElseThrow(() -> new UsernameNotFoundException("Employee not found"));
// EmployeeRole 조회 (활성화된 역할만)
List<EmployeeRole> roles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
// EmployeeDetails에 Employee + 역할 전달
return new EmployeeDetails(employee, roles);
}
}

View File

@ -0,0 +1,13 @@
package com.goi.erp.employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
Optional<Employee> findByEmpLoginId(String empLoginId);
Optional<Employee> findByEmpUuid(UUID empUuid);
}

View File

@ -0,0 +1,49 @@
package com.goi.erp.employee;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.util.UUID;
import com.goi.erp.role.RoleInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "employee_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EmployeeRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "emr_id")
private Integer emrId; // 내부 PK
@Column(name = "emr_uuid", unique = true, nullable = false)
private UUID emrUuid; // 외부용 UUID
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emr_emp_id", nullable = false) // DB 컬럼명 지정
private Employee employee;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emr_role_id", nullable = false) // DB 컬럼명 지정
private RoleInfo roleInfo;
@Column(name = "emr_revoked_at")
private LocalDateTime emrRevokedAt;
}

View File

@ -0,0 +1,19 @@
package com.goi.erp.employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface EmployeeRoleRepository extends JpaRepository<EmployeeRole, Integer> {
@Query("""
SELECT er
FROM EmployeeRole er
WHERE er.employee.id = :empId
AND er.emrRevokedAt IS NULL
""")
List<EmployeeRole> findActiveRolesByEmployeeId(Integer empId);
}

View File

@ -0,0 +1,46 @@
package com.goi.erp.permission;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "permission_info",
uniqueConstraints = @UniqueConstraint(columnNames = {"perm_module", "perm_action", "perm_scope"}))
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PermissionInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "perm_id")
private Integer permId;
@Column(name = "perm_uuid", nullable = false, updatable = false)
private UUID permUuid;
@Column(name = "perm_module", nullable = false)
private String permModule;
@Column(name = "perm_action", nullable = false, length = 1)
private String permAction; // C/R/U/D
@Column(name = "perm_scope", nullable = false, length = 10)
private String permScope; // Self / Part / All
@Column(name = "perm_desc")
private String permDesc;
}

View File

@ -0,0 +1,15 @@
package com.goi.erp.permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface PermissionInfoRepository extends JpaRepository<PermissionInfo, Integer> {
Optional<PermissionInfo> findByPermUuid(UUID permUuid);
// 특정 모듈 + 액션 + 범위에 해당하는 권한 조회
Optional<PermissionInfo> findByPermModuleAndPermActionAndPermScope(String module, String action, String scope);
}

View File

@ -0,0 +1,48 @@
package com.goi.erp.role;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "role_info")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RoleInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Integer roleId;
@Column(name = "role_uuid", unique = true, nullable = false)
private UUID roleUuid;
@Column(name = "role_name", unique = true, nullable = false)
private String roleName;
@Column(name = "role_desc")
private String roleDesc;
@Column(name = "role_level")
private Integer roleLevel;
@OneToMany(mappedBy = "roleInfo", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<RolePermission> rolePermissions;
}

View File

@ -0,0 +1,14 @@
package com.goi.erp.role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface RoleInfoRepository extends JpaRepository<RoleInfo, Integer> {
Optional<RoleInfo> findByRoleUuid(UUID roleUuid);
Optional<RoleInfo> findByRoleName(String roleName);
}

View File

@ -0,0 +1,40 @@
package com.goi.erp.role;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import com.goi.erp.permission.PermissionInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "role_permission")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RolePermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "rpr_id")
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rpr_role_id", nullable = false)
private RoleInfo roleInfo;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rpr_perm_id", nullable = false)
private PermissionInfo permissionInfo;
}

View File

@ -0,0 +1,26 @@
package com.goi.erp.role;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface RolePermissionRepository extends JpaRepository<RolePermission, Integer> {
// 특정 Role의 모든 권한 조회
@Query("SELECT r FROM RolePermission r WHERE r.roleInfo.id = :roleId")
List<RolePermission> findByRoleId(@Param("roleId") Integer roleId);
// Role과 Permission 매핑 존재 여부 확인
@Query("""
SELECT CASE WHEN COUNT(r) > 0 THEN true ELSE false END
FROM RolePermission r
WHERE r.roleInfo.id = :roleId
AND r.permissionInfo.id = :permId
""")
boolean existsByRoleInfoAndPermissionInfo(@Param("roleId") Integer roleId,
@Param("permId") Integer permId);
}

View File

@ -0,0 +1,50 @@
package com.goi.erp.token;
import com.goi.erp.employee.Employee;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "employee_token")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Token {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "emt_id")
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emt_emp_id", nullable = false)
private Employee employee;
@Column(name = "emt_token", nullable = false)
private String token;
@Enumerated(EnumType.STRING)
@Column(name = "emt_token_type")
private TokenType tokenType;
@Column(name = "emt_is_expired")
private boolean expired;
@Column(name = "emt_is_revoked")
private boolean revoked;
}

View File

@ -0,0 +1,22 @@
package com.goi.erp.token;
import com.goi.erp.employee.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface TokenRepository extends JpaRepository<Token, Integer> {
@Query("""
select t from Token t
where t.employee.id = :employeeId
and (t.expired = false or t.revoked = false)
""")
List<Token> findAllValidTokenByEmployee(Integer employeeId);
Optional<Token> findByToken(String token);
List<Token> findByEmployee(Employee employee);
}

View File

@ -0,0 +1,5 @@
package com.goi.erp.token;
public enum TokenType {
BEARER
}

View File

@ -0,0 +1,15 @@
package com.goi.erp.user;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ChangePasswordRequest {
private String currentPassword;
private String newPassword;
private String confirmationPassword;
}

View File

@ -0,0 +1,22 @@
package com.goi.erp.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Permission {
ADMIN_READ("admin:read"),
ADMIN_UPDATE("admin:update"),
ADMIN_CREATE("admin:create"),
ADMIN_DELETE("admin:delete"),
MANAGER_READ("management:read"),
MANAGER_UPDATE("management:update"),
MANAGER_CREATE("management:create"),
MANAGER_DELETE("management:delete")
;
@Getter
private final String permission;
}

View File

@ -0,0 +1,59 @@
package com.goi.erp.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import static com.goi.erp.user.Permission.ADMIN_CREATE;
import static com.goi.erp.user.Permission.ADMIN_DELETE;
import static com.goi.erp.user.Permission.ADMIN_READ;
import static com.goi.erp.user.Permission.ADMIN_UPDATE;
import static com.goi.erp.user.Permission.MANAGER_CREATE;
import static com.goi.erp.user.Permission.MANAGER_DELETE;
import static com.goi.erp.user.Permission.MANAGER_READ;
import static com.goi.erp.user.Permission.MANAGER_UPDATE;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public enum Role {
USER(Collections.emptySet()),
ADMIN(
Set.of(
ADMIN_READ,
ADMIN_UPDATE,
ADMIN_DELETE,
ADMIN_CREATE,
MANAGER_READ,
MANAGER_UPDATE,
MANAGER_DELETE,
MANAGER_CREATE
)
),
MANAGER(
Set.of(
MANAGER_READ,
MANAGER_UPDATE,
MANAGER_DELETE,
MANAGER_CREATE
)
)
;
@Getter
private final Set<Permission> permissions;
public List<SimpleGrantedAuthority> getAuthorities() {
var authorities = getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
}

View File

@ -0,0 +1,27 @@
spring:
datasource:
url: jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/${DB_NAME:goi}
username: ${DB_USER}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
application:
security:
jwt:
secret-key: ${SECRET_KEY}
expiration: 86400000 # a day
refresh-token:
expiration: 604800000 # 7 days
server:
port: 8080
servlet:
context-path: /auth-service

View File

@ -0,0 +1,13 @@
package com.goi.security;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SecurityApplicationTests {
@Test
void contextLoads() {
}
}