Merge branch 'master' into owner-vue

This commit is contained in:
Sangbum Park
2022-02-23 18:37:48 +09:00
committed by GitHub
56 changed files with 2431 additions and 27 deletions

37
config-service/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,33 @@
plugins {
id 'org.springframework.boot' version '2.6.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.just-pickup'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.0")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
config-service/gradlew vendored Executable file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# 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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
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
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
config-service/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem 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, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
rootProject.name = 'config-service'

View File

@@ -0,0 +1,15 @@
package com.justpickup.configservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,66 @@
server:
port: 8888
spring:
application:
name: config-service
cloud:
config:
server:
git:
uri: git@github.com:Development-team-1/config-repo.git
ignore-local-ssh-settings: true
private-key: |
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAzWCoIEVJWhiNTjocwtzc49SJNyor5Hn4t4GQ9BciDe78IQgL
MxynYyHO1RcgDlSjqoVxm8l1M4Tc6AprpmusqsCnHJAOAtMAs0g2onDuTwGj7G33
L/J9AiMIN2uODUN6ehY61+iiKHqgBWMwJYfFKGTJzSRJcFb0GsISMLvMeY10N4bG
dyFZdFMYvMXJUrcJ1dl78RJrxC54NvZkMO+02iH4G3/LA4U75XCAuZiMuj/Nj41o
Px2hwX7ZCje+8HzsMVNVjzyz7sbvkTWXl4b0KXVqe+iFAajmWNNr2/WX05cWMORd
KTO/QmdQhXG8NTFMlTzuE83SSp5KbjGomkFtffdA/uhshlYY+LF1n99+Ft+0vUwd
0xESgUNKvcTpvyHdoBQYIpqn00KUX1a7FOTx4pE4AvICl6owEuw+rrKbxIOu171x
z99C8hbqVGdreEbBmXI+HeRkRP0Qn9gV5iczzES+L4q/HpHd9SjtsYPTiuqeRQ3y
UVELhZqEAinQhIi0grv80+aJp5dIrYoJWNK9pTx3sO7xcsZyxEuej7MhFIAG1423
U7KiY026UvqCpa0aL8847gd2/mjFsjaaOR/zxlhuX0MHTgB8Fub4lPV14Plsjg8t
nlvQxu43JaxeMx8UPaatjaYK7490liHfOoNujc10PZjVWHp3fbIAHEE0GNkCAwEA
AQKCAgA2CeSy+Qgf6K3lk2zV6P9GAzpHhZUe/Ojwf+sADsYrHGLC3jb0L+nrL44Y
i4knvCWETLBj6VIpSCBH/dXtaA96rk5KOa02GQbqJWoau9Q4fZoAL78Goqs50LXs
vyQVYVfsn8TD4rYoKHvKbxrQBTVfN4XRsGaSZI6+K6pVoFRg91NI+PFnjyLWdLbh
lLkwRnEUD7GI8OviJsgCzjn2llvjuWRq9+kW+AuWh0+XhgghSYBw8Sgo00cofpPl
GF3pMTWElQTCN5qwjS+j6ZIFkMDV18s45yl4ElTpsleC/NACIJL6MlxA5xjABCcv
tYlsBZi8+9c45sMaVyQGcpKGZAjGzv8IIrwspItaCUqNIJ2J9M51xecVQLMXbydg
W9osiIoJ9b6xOdgWM0UwZu+M5SQXnDq2zvDiKGdp4Cz+LzH++zC3w44mVDMxo8Nk
luzbiFUtqUnIOCnbVBRxoViz+lg9rrHMB1BYwamquoj5YqZBwiIuPaqu1NhthxU7
11NJ2e4iaanf73Ugozsp7zoNsaey6a3dQqUSNZYkaehSGq36ajMPS4fj6cJi6pN4
UifxT3Dm0Xmc65G/j+3Wqhdp32RbfbFj2y6BNh7NyUX4wgliEfzbuz8f/pA3nd8U
92FkaIneW7KoobVt0UlEu2R9/pRkztLr5geiK1XfTFJEJFe5wQKCAQEA/BEDAEda
cKm+PRTanhxL2W7g0Uj2e8k7+lADRhVCc4WJ+oaqzgS8rJH1aQVyn9W7oSXRqsfd
btdGj289egoYzWa+UQqF5hB2LOpIMSFgz6845C/eAk50tKS5SzndgQQZjQsqeDf+
gZGLc5tR0eVmethaQZEbYaAAmtCeVIJkoQiC+Pu6CXsLo1nUesiB/sxuqzTpP6oY
FNKrar9VHR/AV/4MaYA3eOSE0RNewNDIwEJl8o5QqXpo9kaG9HHMIz9FyIglUT0k
fAvbaYqdYQFcMx0jO34iaIqPXNIkmxXfcu+MJVSI+FqOOI9GXqVuJQa66UZu8gZ2
JSdPBZOQ5MNKBQKCAQEA0JUgSEgZD6fmmKb8FAiAlTOXpkmAmiaHlSBECbBvk5Vk
T3pxUZN4R8+lba42qGI2fcihmzOVz5pSwJcr6xJSQ3+ygjpeEcJXd+kAeq1X7/cI
CaPQGHL+N3D7bSx6YbbpqaIdV/DiocA0M4PXTEWbfmHZ75SCmww759jgaRAcgIYb
jlPMOuz/PGIXyrm6r0hCmgm6jbFXF2cPUSd/bPhnsNSxRXtZe/qbm37yPdkBGTqC
RSyRiEl/ZConR+MgV2i/AKeN502hV8g9RTKN0ZZ775joa5qm6yK7qf+YZe15WhoF
VK0366d61XRVz1JcvhhwFvSUvTPVC/mA4uoSty4HxQKCAQEAghPVVKN9zH4MKkFE
wCsiCUbqxZW9fQvP49C+n8AA1tSm653rLv3Kz9NSraueB1WoyfRWSqdxYe29/+8L
7pqfSgAnXnLrvlnoYi1Uq+Xpq8NkvHznOJF8MGyBIozt/dI7zUlh1UAwGnUdXb9d
bc/QHrzfds6HLs3/AeV+j2XqwB9AV3SwAIx66QVGXUZzryWYRsm7RJtmafh9dUzf
g3QLMl1r0lXUSpLC55HzZ9VWg8DAE3fVsF/3IoAqzKKdEeNpA58egtnmpg0IYWKi
7JhjGA2FTQI+h2xZpzDaqx2SfiGYVPtW47L3icCGM/ly6bCbbB5oyoUDGxE5+kq4
jxUocQKCAQEAs6sA98JC9B1ooK2WDZuVRu/9/RrrT91IhMgaU05LjhDtKxkJa39f
6FuQ3/1kz35p+cdMjWfN79m61nJhPke13LauiUbFqP6CYaOu6f1O9kEQB1237pd/
KzqDGPNrJ6hrdddS92Fjlnj9fjJezjBXVHHtTFXcern7ECbchyN9qafbLKo6Dbf/
03+bhuCdUkcoN4+RxmOv5VS8+ObQ+IiwqL1NRdCOsCCa0UJ0X5oBPD4N4JAXfBHz
TdCRrXaTcTek72dk8I0KIZc2T9jQGG1LrINGEQpSJdDxXenw2DvKgDZRyTK8LJ1D
TYK8N612fbiHNP7Q8HkpVvtAbjW6kVdxEQKCAQBu0b0yRXTJPHAgrGlHhxAbzfGI
wInIpnojn8WneQ0r+7ks0i75C0DfSgvrWNgJsSOjDZUQ5i1G99jo/gYtSofkZY81
ImQBTCwHxm99CJ9mPfnStNHSDf4ZS8BGnTBZAWDgf35h44CChVJkILp6RokMPm4f
e6ZHYvz0v3BnWvtSclS9IeEGsgsPM5zXjeFigQIj1FlH+TF+8ObGGqOiTO1lsUjK
adr+R5QGfZ2MfCLX+QgqQwAsHqavqoTIPCekC/VG6iwNfNpJucoVOSm98qcBy0M7
Rk6oePHuShNQl1yy6DfYusDIsdZxwyk8nywh+hsmpUSmx5z2pcSAgy/pR9h1
-----END RSA PRIVATE KEY-----

View File

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

View File

@@ -28,6 +28,9 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'io.jsonwebtoken:jjwt:0.9.1'
// https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos
implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'

View File

@@ -0,0 +1,73 @@
package com.justpickup.customerapigatewayservice.filter;
import com.justpickup.customerapigatewayservice.security.JwtTokenProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public AuthorizationHeaderFilter(JwtTokenProvider jwtTokenProvider) {
super(Config.class);
this.jwtTokenProvider = jwtTokenProvider;
}
static class Config {
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
}
String authorizationHeader = headers.get(HttpHeaders.AUTHORIZATION).get(0);
// JWT 토큰 판별
String token = authorizationHeader.replace("Bearer", "");
if (!jwtTokenProvider.validateJwtToken(token)) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
String subject = jwtTokenProvider.getUserId(token);
if (false == jwtTokenProvider.getRoles(token).contains("Customer")) {
return onError(exchange, "권한 없음", HttpStatus.UNAUTHORIZED);
}
ServerHttpRequest newRequest = request.mutate()
.header("user-id", subject)
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
// Mono(단일 값), Flux(다중 값) -> Spring WebFlux
private Mono<Void> onError(ServerWebExchange exchange, String errorMsg, HttpStatus httpStatus) {
log.error(errorMsg);
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
}

View File

@@ -0,0 +1,103 @@
package com.justpickup.customerapigatewayservice.security;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Component
@Slf4j
public class JwtTokenProvider {
@Value("${token.access-expired-time}")
private long ACCESS_EXPIRED_TIME;
@Value("${token.refresh-expired-time}")
private long REFRESH_EXPIRED_TIME;
@Value("${token.secret}")
private String SECRET;
public String createJwtAccessToken(String userId, String uri, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userId);
claims.put("roles", roles);
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(uri)
.compact();
}
public String createJwtRefreshToken() {
Claims claims = Jwts.claims();
claims.put("value", UUID.randomUUID());
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + REFRESH_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public String getUserId(String token) {
return getClaimsFromJwtToken(token).getSubject();
}
private Claims getClaimsFromJwtToken(String token) {
try {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
public String getRefreshTokenId(String token) {
return getClaimsFromJwtToken(token).get("value").toString();
}
public List<String> getRoles(String token) {
return (List<String>) getClaimsFromJwtToken(token).get("roles");
}
public boolean validateJwtToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
return false;
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
return false;
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
return false;
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
return false;
}
}
public boolean equalRefreshTokenId(String refreshTokenId, String refreshToken) {
String compareToken = this.getRefreshTokenId(refreshToken);
return refreshTokenId.equals(compareToken);
}
}

View File

@@ -12,3 +12,72 @@ eureka:
spring: spring:
application: application:
name: customer-apigateway-service name: customer-apigateway-service
cloud:
gateway:
routes:
- id: owner-frontend-service
uri: lb://CUSTOMER-FRONTEND-SERVICE
predicates:
- Path=/customer-frontend-service/**
filters:
- RewritePath=/customer-frontend-service/(?<segment>.*),/$\{segment}
- id: order-service
uri: lb://ORDER-SERVCIE
predicates:
- Path=/order-service/**
filters:
- RewritePath=/order-service/(?<segment>.*),/$\{segment}
- id: store-service
uri: lb://STORE-SERVCIE
predicates:
- Path=/store-service/**
filters:
- RewritePath=/store-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/login
- Method=POST
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/refreshToken
- Method=GET
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/logout
- Method=POST
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/oauth2/authorization/*
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/*/oauth2/code/*
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
filters:
- AuthorizationHeaderFilter
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
token:
access-expired-time: 3600000
refresh-expired-time: 604800000
secret: my-secret
refresh-token-name: refresh-token
access-token-name: access-token

View File

@@ -7,9 +7,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
@Controller @Controller
@Slf4j @Slf4j
@RequestMapping("/")
public class CustomerController { public class CustomerController {
@GetMapping("/")
public String index(){
return "/index";
}
@GetMapping("/food-home") @GetMapping("/food-home")
public String hello(){ public String hello(){

View File

@@ -5,11 +5,7 @@
<div layout:fragment="content" id="page"> <div layout:fragment="content" id="page">
<script type="text/javascript" >
function init(){
}
</script>
<div class="page-content header-clear-medium" > <div class="page-content header-clear-medium" >
<div class="splide single-slider slider-no-dots slider-no-arrows mb-4" id="single-slider-1"> <div class="splide single-slider slider-no-dots slider-no-arrows mb-4" id="single-slider-1">

View File

@@ -0,0 +1,481 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout}">
<div layout:fragment="content" id="page">
<div id="footer-bar" class="footer-bar-1">
<a href="index.html" class="active-nav"><i class="fa fa-home"></i><span>Home</span></a>
<a href="index-components.html"><i class="fa fa-star"></i><span>Features</span></a>
<a href="index-pages.html"><i class="fa fa-heart"></i><span>Pages</span></a>
<a href="index-search.html"><i class="fa fa-search"></i><span>Search</span></a>
<a href="#" data-menu="menu-settings"><i class="fa fa-cog"></i><span>Settings</span></a>
</div>
<div class="page-content header-clear-small">
<div class="splide single-slider slider-no-arrows slider-no-dots" id="single-slider-home">
<div class="splide__track">
<div class="splide__list">
<div class="splide__slide">
<div class="card rounded-m shadow-l mx-3">
<div class="card-bottom text-center mb-0">
<h1 class="color-white font-700 mb-n1">StickyMobile</h1>
<p class="color-white opacity-80 mb-4">The Menu Everyone Requested.</p>
</div>
<div class="card-overlay bg-gradient"></div>
<img class="img-fluid" src="images/pictures/13.jpg">
</div>
</div>
<div class="splide__slide">
<div class="card rounded-m shadow-l mx-3">
<div class="card-bottom text-center mb-0">
<h1 class="color-white font-700 mb-n1">Carefuly Built</h1>
<p class="color-white opacity-80 mb-4">Flexibility, Speed, Ease of Use.</p>
</div>
<div class="card-overlay bg-gradient"></div>
<img class="img-fluid" src="images/pictures/28.jpg">
</div>
</div>
<div class="splide__slide">
<div class="card rounded-m shadow-l mx-3">
<div class="card-bottom text-center mb-0">
<h1 class="color-white font-700 mb-n1">Elite Quality</h1>
<p class="color-white opacity-80 mb-4">Mobile Website, App or PWA Ready.</p>
</div>
<div class="card-overlay bg-gradient"></div>
<img class="img-fluid" src="images/pictures/29.jpg">
</div>
</div>
</div>
</div>
</div>
<div class="splide single-slider slider-no-arrows slider-no-dots" id="single-slider-cta">
<div class="splide__track">
<div class="splide__list">
<div class="splide__slide">
<div class="card card-style">
<div class="content mb-0">
<h1 class="text-center mb-0">Call to Actions</h1>
<p class="text-center color-highlight mt-n1 font-12">Sticky comes Prepared for You</p>
<p class="boxed-text-xl mt-n3">
Call to action are highly important, but we can't choose a style for you, so we created a few!
</p>
<a href="#" class="btn btn-m btn-center-l bg-red-dark text-uppercase font-900 text-uppercase rounded-s shadow-xl mb-4">Swipe Left to Begin</a>
</div>
</div>
</div>
<div class="splide__slide">
<div class="card card-style">
<div class="content">
<h1 class="text-center mb-0">Let's get Social</h1>
<p class="text-center color-highlight mt-n1 font-12">Follow & Get in Touch with Us</p>
<p class="boxed-text-xl mt-n3">
It's easy. Just add your links and you're ready to go. Social links are wildely available in Sticky
</p>
<p class="text-center pb-2">
<a href="#" class="icon icon-s rounded-s shadow-xl color-white bg-facebook"><i class="fab fa-facebook-f"></i></a>
<a href="#" class="icon icon-s rounded-s shadow-xl color-white bg-phone ms-2 me-2"><i class="fa fa-phone"></i></a>
<a href="#" class="icon icon-s rounded-s shadow-xl color-white bg-twitter"><i class="fab fa-twitter"></i></a>
</p>
</div>
</div>
</div>
<div class="splide__slide">
<div class="card card-style">
<div class="content mb-0">
<h1 class="text-center mb-0">This or That?</h1>
<p class="text-center color-highlight mt-n1 font-12">Which is your Favorite?</p>
<p class="boxed-text-xl mt-n3">
Multiple choices are awesome, highlighting them is even easier with our Call to Actions
</p>
<p class="text-center mt-n3 mb-0 pb-0">
<a href="#" class="btn btn-m bg-green-dark text-uppercase font-900 text-uppercase rounded-s shadow-xl mb-4 mt-3">Call Now</a>
<a href="#" class="icon icon-m rounded-s opacity-40 color-theme ms-3 me-3">or</a>
<a href="#" class="btn btn-m bg-red-dark text-uppercase font-900 text-uppercase rounded-s shadow-xl mb-4 mt-3">GET A QUOTE</a>
</p>
</div>
</div>
</div>
<div class="splide__slide">
<div class="card card-style">
<div class="content mb-0">
<h1 class="text-center mb-0">Direct Action</h1>
<p class="text-center color-highlight mt-n1 mb-2 font-12">Invite Users to Tap Button</p>
<p class="boxed-text-xl">
Make a button stand out at the top of your page so it's easily accessible as a first option.
</p>
<a href="#" class="btn btn-m btn-center-l bg-red-dark text-uppercase font-900 text-uppercase rounded-s shadow-xl mb-4">Purchase today for $25</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card card-style">
<div class="content mb-0">
<h1 class="text-center mb-0">Packed with Goodies</h1>
<p class="text-center color-highlight font-11 mt-n1">The Absolute Best Products & Care for You</p>
<p class="boxed-text-xl mt-n3">
Over 10 years of Experience in Building Gorgeous Products only for you! We are Envato Elite.
</p>
<div class="divider"></div>
</div>
<div class="row me-2 ms-2 mb-0">
<div class="col-6 text-center">
<i class="fa fa-trophy color-yellow-dark fa-3x"></i>
<h2 class="mt-3 mb-1">Future Proof</h2>
<p>Built to last, with the latest quality code</p>
</div>
<div class="col-6 text-center">
<i class="fab fa-cloudscale color-highlight fa-3x"></i>
<h2 class="mt-3 mb-1">Powerful</h2>
<p>Speed, Features and Flexibility all in One!</p>
</div>
<div class="col-6 text-center">
<i class="fa fa-check color-green-dark fa-3x"></i>
<h2 class="mt-3 mb-1">Easy to Use</h2>
<p>Customers love our work for it's ease.</p>
</div>
<div class="col-6 text-center">
<i class="fa fa-life-ring color-blue-dark fa-3x"></i>
<h2 class="mt-3 mb-1">Customer Care</h2>
<p>We treat others like we want to be treated.</p>
</div>
</div>
</div>
<div class="splide double-slider slider-no-arrows slider-no-dots" id="double-slider-home-1">
<div class="splide__track">
<div class="splide__list">
<div class="splide__slide">
<div data-card-height="250" class="card mx-3 rounded-m shadow-l bg-13">
<div class="card-bottom text-center">
<h2 class="color-white font-900 mb-0">EazyMobile</h2>
<p class="text-center mb-3">
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
</p>
<a href="#" class="btn btn-s btn-full text-uppercase font-900 bg-red-dark rounded-s me-2 ms-2 mb-2">Purchase</a>
</div>
<div class="card-overlay bg-gradient"></div>
</div>
</div>
<div class="splide__slide">
<div data-card-height="250" class="card mx-3 rounded-m shadow-l bg-27">
<div class="card-bottom text-center">
<h2 class="color-white font-900 mb-0">UltraMobile</h2>
<p class="text-center mb-3">
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
</p>
<a href="#" class="btn btn-s btn-full text-uppercase font-900 bg-red-dark rounded-s me-2 ms-2 mb-2">Purchase</a>
</div>
<div class="card-overlay bg-gradient"></div>
</div>
</div>
<div class="splide__slide">
<div data-card-height="250" class="card mx-3 rounded-m shadow-l bg-17">
<div class="card-bottom text-center">
<h2 class="color-white font-900 mb-0">KolorMobile</h2>
<p class="text-center mb-3">
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
</p>
<a href="#" class="btn btn-s btn-full text-uppercase font-900 bg-red-dark rounded-s me-2 ms-2 mb-2">Purchase</a>
</div>
<div class="card-overlay bg-gradient"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card card-style">
<div class="content mb-4">
<h1 class="text-center mb-0">Care & Quality</h1>
<p class="text-center color-highlight font-11 mt-n1 pb-0">No stone left unturned, no aspect overlooked.</p>
<p class="text-center font-20 mt-n2">
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
<i class="fa fa-star color-yellow-dark"></i>
</p>
<div class="splide single-slider slider-no-arrows slider-no-dots" id="single-slider-home-quotes">
<div class="splide__track">
<div class="splide__list">
<div class="splide__slide">
<h2 class="text-center font-300 line-height-xl content mb-0 mt-0">
The code is always great with any Enabled template, the customer support that wins me over always.
</h2>
</div>
<div class="splide__slide">
<h2 class="text-center font-300 line-height-xl content mb-0 mt-0">
The best support I have ever had, it's so good I purchased another theme. Highlighy Recommended.
</h2>
</div>
</div>
</div>
</div>
<a href="#" class="btn btn-m btn-center-l text-uppercase font-900 bg-red-dark rounded-sm shadow-xl mt-4 mb-0">More Testimonials</a>
</div>
</div>
<div class="splide double-slider slider-no-arrows slider-no-dots" id="double-slider-home-2">
<div class="splide__track">
<div class="splide__list">
<div class="splide__slide">
<div data-card-height="180" class="card mx-3 rounded-m shadow-l bg-18">
<div class="card-top ms-3 mt-3">
<i class="fa fa-bolt fa-4x color-red-dark"></i>
</div>
<div class="card-bottom ms-3">
<h2 class="color-white font-900 mb-0">Performance</h2>
<p class="color-white font-11 mt-n1 mb-2">Fast and feature filled</p>
</div>
<div class="card-overlay bg-black opacity-80"></div>
</div>
</div>
<div class="splide__slide">
<div data-card-height="180" class="card mx-3 rounded-m shadow-l bg-14">
<div class="card-top ms-3 mt-3">
<i class="fa fa-trophy fa-4x color-blue-dark"></i>
</div>
<div class="card-bottom ms-3">
<h2 class="color-white font-900 mb-0">Elite Care</h2>
<p class="color-white font-11 mt-n1 mb-2">Built by the Best for You</p>
</div>
<div class="card-overlay bg-black opacity-80"></div>
</div>
</div>
<div class="splide__slide">
<div data-card-height="180" class="card mx-3 rounded-m shadow-l bg-3">
<div class="card-top ms-3 mt-3">
<i class="fa fa-star fa-4x color-yellow-dark"></i>
</div>
<div class="card-bottom ms-3">
<h2 class="color-white font-900 mb-0">Quality</h2>
<p class="color-white font-11 mt-n1 mb-2">Built with Care and Detail</p>
</div>
<div class="card-overlay bg-black opacity-80"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card card-style">
<div class="content mt-0 mb-0">
<div class="list-group list-custom-large">
<a href="#" data-toggle-theme data-trigger-switch="toggle-dark-home" class="border-0">
<i class="fa font-12 fa-moon rounded-s bg-highlight color-white me-3"></i>
<span class="font-600">Dark Mode</span>
<strong>Sticky will Remember</strong>
<div class="custom-control scale-switch ios-switch">
<input data-toggle-theme type="checkbox" class="ios-input" id="toggle-dark-home">
<label class="custom-control-label" for="toggle-dark-home"></label>
</div>
<i class="fa fa-angle-right"></i>
</a>
</div>
</div>
</div>
<div class="card card-style">
<div class="content mb-0">
<h1 class="text-center mb-0">Get Sticky Today</h1>
<p class="text-center color-highlight font-11 mt-n1 pb-0">Tons of Awesome Features just for You.</p>
<p class="boxed-text-xl mt-n3">
Fast, easy to use and filled with features. Get Sticky Today and give your site the Mobile Feeling it deserves.
</p>
<a href="#" class="btn btn-m btn-center-l text-uppercase font-900 bg-red-dark rounded-sm shadow-xl mb-4">Purchase Now - $25</a>
</div>
</div>
<div class="footer card card-style">
<a href="#" class="footer-title"><span class="color-highlight">StickyMobile</span></a>
<p class="footer-text"><span>Made with <i class="fa fa-heart color-highlight font-16 ps-2 pe-2"></i> by Enabled</span><br><br>Powered by the best Mobile Website Developer on Envato Market. Elite Quality. Elite Products.</p>
<div class="text-center mb-3">
<a href="#" class="icon icon-xs rounded-sm shadow-l me-1 bg-facebook"><i class="fab fa-facebook-f"></i></a>
<a href="#" class="icon icon-xs rounded-sm shadow-l me-1 bg-twitter"><i class="fab fa-twitter"></i></a>
<a href="#" class="icon icon-xs rounded-sm shadow-l me-1 bg-phone"><i class="fa fa-phone"></i></a>
<a href="#" data-menu="menu-share" class="icon icon-xs rounded-sm me-1 shadow-l bg-red-dark"><i class="fa fa-share-alt"></i></a>
<a href="#" class="back-to-top icon icon-xs rounded-sm shadow-l bg-dark-light"><i class="fa fa-angle-up"></i></a>
</div>
<p class="footer-copyright">Copyright &copy; Enabled <span id="copyright-year">2017</span>. All Rights Reserved.</p>
<p class="footer-links"><a href="#" class="color-highlight">Privacy Policy</a> | <a href="#" class="color-highlight">Terms and Conditions</a> | <a href="#" class="back-to-top color-highlight"> Back to Top</a></p>
<div class="clear"></div>
</div>
</div>
<!-- End of Page Content-->
<!-- All Menus, Action Sheets, Modals, Notifications, Toasts, Snackbars get Placed outside the <div class="page-content"> -->
<div id="menu-settings" class="menu menu-box-bottom menu-box-detached">
<div class="menu-title mt-0 pt-0"><h1>Settings</h1><p class="color-highlight">Flexible and Easy to Use</p><a href="#" class="close-menu"><i class="fa fa-times"></i></a></div>
<div class="divider divider-margins mb-n2"></div>
<div class="content">
<div class="list-group list-custom-small">
<a href="#" data-toggle-theme data-trigger-switch="switch-dark-mode" class="pb-2 ms-n1">
<i class="fa font-12 fa-moon rounded-s bg-highlight color-white me-3"></i>
<span>Dark Mode</span>
<div class="custom-control scale-switch ios-switch">
<input data-toggle-theme type="checkbox" class="ios-input" id="switch-dark-mode">
<label class="custom-control-label" for="switch-dark-mode"></label>
</div>
<i class="fa fa-angle-right"></i>
</a>
</div>
<div class="list-group list-custom-large">
<a data-menu="menu-highlights" href="#">
<i class="fa font-14 fa-tint bg-green-dark rounded-s"></i>
<span>Page Highlight</span>
<strong>16 Colors Highlights Included</strong>
<span class="badge bg-highlight color-white">HOT</span>
<i class="fa fa-angle-right"></i>
</a>
<a data-menu="menu-backgrounds" href="#" class="border-0">
<i class="fa font-14 fa-cog bg-blue-dark rounded-s"></i>
<span>Background Color</span>
<strong>10 Page Gradients Included</strong>
<span class="badge bg-highlight color-white">NEW</span>
<i class="fa fa-angle-right"></i>
</a>
</div>
</div>
</div>
<!-- Menu Settings Highlights-->
<div id="menu-highlights" class="menu menu-box-bottom menu-box-detached">
<div class="menu-title"><h1>Highlights</h1><p class="color-highlight">Any Element can have a Highlight Color</p><a href="#" class="close-menu"><i class="fa fa-times"></i></a></div>
<div class="divider divider-margins mb-n2"></div>
<div class="content">
<div class="highlight-changer">
<a href="#" data-change-highlight="blue"><i class="fa fa-circle color-blue-dark"></i><span class="color-blue-light">Default</span></a>
<a href="#" data-change-highlight="red"><i class="fa fa-circle color-red-dark"></i><span class="color-red-light">Red</span></a>
<a href="#" data-change-highlight="orange"><i class="fa fa-circle color-orange-dark"></i><span class="color-orange-light">Orange</span></a>
<a href="#" data-change-highlight="pink2"><i class="fa fa-circle color-pink2-dark"></i><span class="color-pink-dark">Pink</span></a>
<a href="#" data-change-highlight="magenta"><i class="fa fa-circle color-magenta-dark"></i><span class="color-magenta-light">Purple</span></a>
<a href="#" data-change-highlight="aqua"><i class="fa fa-circle color-aqua-dark"></i><span class="color-aqua-light">Aqua</span></a>
<a href="#" data-change-highlight="teal"><i class="fa fa-circle color-teal-dark"></i><span class="color-teal-light">Teal</span></a>
<a href="#" data-change-highlight="mint"><i class="fa fa-circle color-mint-dark"></i><span class="color-mint-light">Mint</span></a>
<a href="#" data-change-highlight="green"><i class="fa fa-circle color-green-light"></i><span class="color-green-light">Green</span></a>
<a href="#" data-change-highlight="grass"><i class="fa fa-circle color-green-dark"></i><span class="color-green-dark">Grass</span></a>
<a href="#" data-change-highlight="sunny"><i class="fa fa-circle color-yellow-light"></i><span class="color-yellow-light">Sunny</span></a>
<a href="#" data-change-highlight="yellow"><i class="fa fa-circle color-yellow-dark"></i><span class="color-yellow-light">Goldish</span></a>
<a href="#" data-change-highlight="brown"><i class="fa fa-circle color-brown-dark"></i><span class="color-brown-light">Wood</span></a>
<a href="#" data-change-highlight="night"><i class="fa fa-circle color-dark-dark"></i><span class="color-dark-light">Night</span></a>
<a href="#" data-change-highlight="dark"><i class="fa fa-circle color-dark-light"></i><span class="color-dark-light">Dark</span></a>
<div class="clearfix"></div>
</div>
<a href="#" data-menu="menu-settings" class="mb-3 btn btn-full btn-m rounded-sm bg-highlight shadow-xl text-uppercase font-900 mt-4">Back to Settings</a>
</div>
</div>
<!-- Menu Settings Backgrounds-->
<div id="menu-backgrounds" class="menu menu-box-bottom menu-box-detached">
<div class="menu-title"><h1>Backgrounds</h1><p class="color-highlight">Change Page Color Behind Content Boxes</p><a href="#" class="close-menu"><i class="fa fa-times"></i></a></div>
<div class="divider divider-margins mb-n2"></div>
<div class="content">
<div class="background-changer">
<a href="#" data-change-background="default"><i class="bg-theme"></i><span class="color-dark-dark">Default</span></a>
<a href="#" data-change-background="plum"><i class="body-plum"></i><span class="color-plum-dark">Plum</span></a>
<a href="#" data-change-background="magenta"><i class="body-magenta"></i><span class="color-dark-dark">Magenta</span></a>
<a href="#" data-change-background="dark"><i class="body-dark"></i><span class="color-dark-dark">Dark</span></a>
<a href="#" data-change-background="violet"><i class="body-violet"></i><span class="color-violet-dark">Violet</span></a>
<a href="#" data-change-background="red"><i class="body-red"></i><span class="color-red-dark">Red</span></a>
<a href="#" data-change-background="green"><i class="body-green"></i><span class="color-green-dark">Green</span></a>
<a href="#" data-change-background="sky"><i class="body-sky"></i><span class="color-sky-dark">Sky</span></a>
<a href="#" data-change-background="orange"><i class="body-orange"></i><span class="color-orange-dark">Orange</span></a>
<a href="#" data-change-background="yellow"><i class="body-yellow"></i><span class="color-yellow-dark">Yellow</span></a>
<div class="clearfix"></div>
</div>
<a href="#" data-menu="menu-settings" class="mb-3 btn btn-full btn-m rounded-sm bg-highlight shadow-xl text-uppercase font-900 mt-4">Back to Settings</a>
</div>
</div>
<!-- Menu Share -->
<div id="menu-share" class="menu menu-box-bottom menu-box-detached">
<div class="menu-title mt-n1"><h1>Share the Love</h1><p class="color-highlight">Just Tap the Social Icon. We'll add the Link</p><a href="#" class="close-menu"><i class="fa fa-times"></i></a></div>
<div class="content mb-0">
<div class="divider mb-0"></div>
<div class="list-group list-custom-small list-icon-0">
<a href="auto_generated" class="shareToFacebook external-link">
<i class="font-18 fab fa-facebook-square color-facebook"></i>
<span class="font-13">Facebook</span>
<i class="fa fa-angle-right"></i>
</a>
<a href="auto_generated" class="shareToTwitter external-link">
<i class="font-18 fab fa-twitter-square color-twitter"></i>
<span class="font-13">Twitter</span>
<i class="fa fa-angle-right"></i>
</a>
<a href="auto_generated" class="shareToLinkedIn external-link">
<i class="font-18 fab fa-linkedin color-linkedin"></i>
<span class="font-13">LinkedIn</span>
<i class="fa fa-angle-right"></i>
</a>
<a href="auto_generated" class="shareToWhatsApp external-link">
<i class="font-18 fab fa-whatsapp-square color-whatsapp"></i>
<span class="font-13">WhatsApp</span>
<i class="fa fa-angle-right"></i>
</a>
<a href="auto_generated" class="shareToMail external-link border-0">
<i class="font-18 fa fa-envelope-square color-mail"></i>
<span class="font-13">Email</span>
<i class="fa fa-angle-right"></i>
</a>
</div>
</div>
</div>
<!-- Be sure this is on your main visiting page, for example, the index.html page-->
<!-- Install Prompt for Android -->
<div id="menu-install-pwa-android" class="menu menu-box-bottom menu-box-detached rounded-l">
<div class="boxed-text-l mt-4 pb-3">
<img class="rounded-l mb-3" src="app/icons/icon-128x128.png" alt="img" width="90">
<h4 class="mt-3">Add Sticky on your Home Screen</h4>
<p>
Install Sticky on your home screen, and access it just like a regular app. It really is that simple!
</p>
<a href="#" class="pwa-install btn btn-s rounded-s shadow-l text-uppercase font-900 bg-highlight mb-2">Add to Home Screen</a><br>
<a href="#" class="pwa-dismiss close-menu color-gray-dark text-uppercase font-900 opacity-60 font-10 pt-2">Maybe later</a>
<div class="clear"></div>
</div>
</div>
<!-- Install instructions for iOS -->
<div id="menu-install-pwa-ios"
class="menu menu-box-bottom menu-box-detached rounded-l">
<div class="boxed-text-xl mt-4 pb-3">
<img class="rounded-l mb-3" src="app/icons/icon-128x128.png" alt="img" width="90">
<h4 class="mt-3">Add Sticky on your Home Screen</h4>
<p class="mb-0 pb-0">
Install Sticky, and access it like a regular app. Open your Safari menu and tap "Add to Home Screen".
</p>
<div class="clearfix pt-3"></div>
<a href="#" class="pwa-dismiss close-menu color-highlight text-uppercase font-700">Maybe later</a>
</div>
</div>
<script type="text/javascript" >
function init(){
alert()
}
</script>
</div>
</html>

View File

@@ -26,8 +26,12 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway' implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
// https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos // https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos
implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64' implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64'
// https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api
implementation 'javax.xml.bind:jaxb-api:2.3.1'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-devtools'

View File

@@ -0,0 +1,73 @@
package com.justpickup.ownerapigatewayservice.filter;
import com.justpickup.ownerapigatewayservice.security.JwtTokenProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public AuthorizationHeaderFilter(JwtTokenProvider jwtTokenProvider) {
super(Config.class);
this.jwtTokenProvider = jwtTokenProvider;
}
static class Config {
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
}
String authorizationHeader = headers.get(HttpHeaders.AUTHORIZATION).get(0);
// JWT 토큰 판별
String token = authorizationHeader.replace("Bearer", "");
if (!jwtTokenProvider.validateJwtToken(token)) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
String subject = jwtTokenProvider.getUserId(token);
if (false == jwtTokenProvider.getRoles(token).contains("StoreOwner")) {
return onError(exchange, "권한 없음", HttpStatus.UNAUTHORIZED);
}
ServerHttpRequest newRequest = request.mutate()
.header("user-id", subject)
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
// Mono(단일 값), Flux(다중 값) -> Spring WebFlux
private Mono<Void> onError(ServerWebExchange exchange, String errorMsg, HttpStatus httpStatus) {
log.error(errorMsg);
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
}

View File

@@ -0,0 +1,103 @@
package com.justpickup.ownerapigatewayservice.security;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Component
@Slf4j
public class JwtTokenProvider {
@Value("${token.access-expired-time}")
private long ACCESS_EXPIRED_TIME;
@Value("${token.refresh-expired-time}")
private long REFRESH_EXPIRED_TIME;
@Value("${token.secret}")
private String SECRET;
public String createJwtAccessToken(String userId, String uri, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userId);
claims.put("roles", roles);
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(uri)
.compact();
}
public String createJwtRefreshToken() {
Claims claims = Jwts.claims();
claims.put("value", UUID.randomUUID());
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + REFRESH_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public String getUserId(String token) {
return getClaimsFromJwtToken(token).getSubject();
}
private Claims getClaimsFromJwtToken(String token) {
try {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
public String getRefreshTokenId(String token) {
return getClaimsFromJwtToken(token).get("value").toString();
}
public List<String> getRoles(String token) {
return (List<String>) getClaimsFromJwtToken(token).get("roles");
}
public boolean validateJwtToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
return false;
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
return false;
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
return false;
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
return false;
}
}
public boolean equalRefreshTokenId(String refreshTokenId, String refreshToken) {
String compareToken = this.getRefreshTokenId(refreshToken);
return refreshTokenId.equals(compareToken);
}
}

View File

@@ -46,12 +46,35 @@ spring:
- id: user-service - id: user-service
uri: lb://USER-SERVICE uri: lb://USER-SERVICE
predicates: predicates:
- Path=/user-service/** - Path=/user-service/login
- Method=POST
filters: filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment} - RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: owner-vue - id: user-service
uri: http://localhost:8080 uri: lb://USER-SERVICE
predicates: predicates:
- Path=/owner-vue/** - Path=/user-service/refreshToken
- Method=GET
filters: filters:
- RewritePath=/owner-vue/(?<segment>.*),/$\{segment} - RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/logout
- Method=POST
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
filters:
- AuthorizationHeaderFilter
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
token:
access-expired-time: 3600000
refresh-expired-time: 604800000
secret: my-secret
refresh-token-name: refresh-token
access-token-name: access-token

View File

@@ -34,11 +34,16 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
/*implementation 'org.springframework.boot:spring-boot-starter-amqp'*/ /*implementation 'org.springframework.boot:spring-boot-starter-amqp'*/
/*implementation 'org.springframework.boot:spring-boot-starter-security'*/ implementation 'org.springframework.boot:spring-boot-starter-security'
/*implementation 'org.springframework.cloud:spring-cloud-starter-config'*/ compileOnly 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
/*implementation 'org.springframework.kafka:spring-kafka'*/ /*implementation 'org.springframework.kafka:spring-kafka'*/
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// https://mvnrepository.com/artifact/com.github.gavlyukovskiy/p6spy-spring-boot-starter // https://mvnrepository.com/artifact/com.github.gavlyukovskiy/p6spy-spring-boot-starter
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0'
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-devtools'

View File

@@ -1,8 +1,12 @@
package com.justpickup.userservice; package com.justpickup.userservice;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import com.justpickup.userservice.domain.user.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
@SpringBootApplication @SpringBootApplication
@EnableEurekaClient @EnableEurekaClient
@@ -12,4 +16,13 @@ public class UserServiceApplication {
SpringApplication.run(UserServiceApplication.class, args); SpringApplication.run(UserServiceApplication.class, args);
} }
@Bean
CommandLineRunner run(UserService userService) {
return args -> {
StoreOwnerDto park = StoreOwnerDto.builder()
.email("test@gmail.com").password("1234").name("Park").phoneNumber("010-1234-5678")
.build();
userService.saveStoreOwner(park);
};
}
} }

View File

@@ -0,0 +1,11 @@
package com.justpickup.userservice.domain.jwt.exception;
import com.justpickup.userservice.global.exception.CustomException;
import org.springframework.http.HttpStatus;
public class AccessTokenNotValidException extends CustomException {
public AccessTokenNotValidException(String message) {
super(HttpStatus.FORBIDDEN, message);
}
}

View File

@@ -0,0 +1,14 @@
package com.justpickup.userservice.domain.jwt.exception;
import com.justpickup.userservice.global.dto.Result;
import lombok.Getter;
@Getter
public class RefreshTokenNotValidException extends RuntimeException {
private Result result;
public RefreshTokenNotValidException(String message) {
this.result = Result.createErrorResult(message);
}
}

View File

@@ -0,0 +1,25 @@
package com.justpickup.userservice.domain.jwt.redis;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@Getter
@RedisHash("refresh_token")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken {
@Id
private String userId;
private String refreshTokenId;
public static RefreshToken of(String userId, String refreshTokenId) {
RefreshToken refreshToken = new RefreshToken();
refreshToken.userId = userId;
refreshToken.refreshTokenId = refreshTokenId;
return refreshToken;
}
}

View File

@@ -0,0 +1,7 @@
package com.justpickup.userservice.domain.jwt.repository;
import com.justpickup.userservice.domain.jwt.redis.RefreshToken;
import org.springframework.data.repository.CrudRepository;
public interface RefreshTokenRedisRepository extends CrudRepository<RefreshToken, String> {
}

View File

@@ -0,0 +1,94 @@
package com.justpickup.userservice.domain.jwt.service;
import com.justpickup.userservice.domain.user.dto.CustomerDto;
import com.justpickup.userservice.domain.user.dto.OAuthAttributeDto;
import com.justpickup.userservice.domain.user.entity.Customer;
import com.justpickup.userservice.domain.user.repository.CustomerRepository;
import com.justpickup.userservice.domain.user.repository.UserRepository;
import com.justpickup.userservice.domain.user.service.UserService;
import com.justpickup.userservice.domain.user.service.UserServiceImpl;
import com.justpickup.userservice.global.utils.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@Service
public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final CustomerRepository customerRepository;
private final HttpServletResponse response;
private final HttpServletRequest request;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;
private final UserServiceImpl userServiceImpl;
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest,OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
//OAuth 서비스 id
String registrationId = userRequest.getClientRegistration().getRegistrationId();
//OAuth 로그인 진행시 키가 되는 필드값
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
// OAuth2UserService
OAuthAttributeDto attributeDto = OAuthAttributeDto.of(registrationId, userNameAttributeName,oAuth2User.getAttributes());
Customer customer = saveCustomer(attributeDto);
// TODO: 2022/02/16 Response에 token 담아 보내기
String userEmail = customer.getEmail();
Collection<? extends GrantedAuthority> authorities = userServiceImpl.loadUserByUsername(userEmail).getAuthorities();
List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
String accessToken = jwtTokenProvider.createJwtAccessToken(userEmail, request.getRequestURI(), roles);
String refreshToken = jwtTokenProvider.createJwtRefreshToken();
refreshTokenService.updateRefreshToken(customer.getId(), jwtTokenProvider.getRefreshTokenId(refreshToken));
response.setHeader("Access-token",accessToken);
response.setHeader("refresh-token",refreshToken);
return new DefaultOAuth2User(
authorities
, attributeDto.getAttributes()
, attributeDto.getNameAttributeKey());
}
@Transactional
public Customer saveCustomer(OAuthAttributeDto attributeDto){
return customerRepository.save(
customerRepository.findByEmail(attributeDto.getEmail())
.orElse(attributeDto.toEntity(attributeDto))
);
}
}

View File

@@ -0,0 +1,9 @@
package com.justpickup.userservice.domain.jwt.service;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
public interface RefreshTokenService {
void updateRefreshToken(Long id, String uuid);
JwtTokenDto refreshJwtToken(String accessToken, String refreshToken);
void logoutToken(String accessToken);
}

View File

@@ -0,0 +1,96 @@
package com.justpickup.userservice.domain.jwt.service;
import com.justpickup.userservice.domain.jwt.exception.AccessTokenNotValidException;
import com.justpickup.userservice.domain.jwt.exception.RefreshTokenNotValidException;
import com.justpickup.userservice.domain.jwt.redis.RefreshToken;
import com.justpickup.userservice.domain.jwt.repository.RefreshTokenRedisRepository;
import com.justpickup.userservice.global.utils.JwtTokenProvider;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
import com.justpickup.userservice.domain.user.entity.User;
import com.justpickup.userservice.domain.user.exception.NotExistUserException;
import com.justpickup.userservice.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class RefreshTokenServiceImpl implements RefreshTokenService {
private final UserDetailsService userDetailsService;
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
private final RefreshTokenRedisRepository refreshTokenRedisRepository;
@Transactional
@Override
public void updateRefreshToken(Long id, String uuid) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + id + "는 없는 사용자입니다."));
refreshTokenRedisRepository.save(RefreshToken.of(user.getId().toString(), uuid));
}
@Transactional
@Override
public JwtTokenDto refreshJwtToken(String accessToken, String refreshToken) {
String userId = jwtTokenProvider.getUserId(accessToken);
RefreshToken findRefreshToken = refreshTokenRedisRepository.findById(userId)
.orElseThrow(()
-> new RefreshTokenNotValidException("사용자 고유번호 : " + userId + "는 등록된 리프레쉬 토큰이 없습니다.")
);
// refresh token 검증
String findRefreshTokenId = findRefreshToken.getRefreshTokenId();
if (!jwtTokenProvider.validateJwtToken(refreshToken) ||
!jwtTokenProvider.equalRefreshTokenId(findRefreshTokenId, refreshToken)) {
refreshTokenRedisRepository.delete(findRefreshToken);
throw new RefreshTokenNotValidException("Not validate jwt token = " + refreshToken);
}
User findUser = userRepository.findById(Long.valueOf(userId))
.orElseThrow(() -> new NotExistUserException("유저 고유 번호 : " + userId + "는 없는 유저입니다."));
// access token 생성
Authentication authentication = getAuthentication(findUser.getEmail());
List<String> roles = authentication.getAuthorities()
.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
String newAccessToken = jwtTokenProvider.createJwtAccessToken(userId, "/refreshToken", roles);
return JwtTokenDto.builder()
.accessToken(newAccessToken)
.refreshToken(refreshToken)
.build();
}
@Override
public void logoutToken(String accessToken) {
if (!jwtTokenProvider.validateJwtToken(accessToken)) {
// 예외 발생
throw new AccessTokenNotValidException("access token is not valid");
}
RefreshToken refreshToken = refreshTokenRedisRepository.findById(jwtTokenProvider.getUserId(accessToken))
.orElseThrow(() -> new RefreshTokenNotValidException("refresh Token is not exist"));
refreshTokenRedisRepository.delete(refreshToken);
}
public Authentication getAuthentication(String email) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
}

View File

@@ -0,0 +1,63 @@
package com.justpickup.userservice.domain.jwt.web;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
import com.justpickup.userservice.global.dto.Result;
import com.justpickup.userservice.global.utils.CookieProvider;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.ws.rs.core.HttpHeaders;
@RestController
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
private final CookieProvider cookieProvider;
@GetMapping("/refreshToken")
public ResponseEntity<Result> refreshToken(@RequestHeader("X-AUTH-TOKEN") String accessToken,
@CookieValue("refresh-token") String refreshToken) {
JwtTokenDto jwtTokenDto = refreshTokenServiceImpl.refreshJwtToken(accessToken, refreshToken);
ResponseCookie responseCookie = cookieProvider.createRefreshTokenCookie(refreshToken);
return ResponseEntity.status(HttpStatus.OK)
.header(HttpHeaders.SET_COOKIE, responseCookie.toString())
.body(Result.createSuccessResult(new RefreshTokenResponse(jwtTokenDto)));
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class RefreshTokenResponse {
private String accessToken;
public RefreshTokenResponse(JwtTokenDto jwtTokenDto) {
this.accessToken = jwtTokenDto.getAccessToken();
}
}
@PostMapping("/logout")
public ResponseEntity<Result> logout(@RequestHeader("X-AUTH-TOKEN") String accessToken,
@RequestHeader("REFRESH-TOKEN") String refreshToken) {
refreshTokenServiceImpl.logoutToken(accessToken);
ResponseCookie refreshCookie = cookieProvider.removeRefreshTokenCookie();
return ResponseEntity.status(HttpStatus.OK)
.header(HttpHeaders.SET_COOKIE, refreshCookie.toString())
.body(Result.createErrorResult(""));
}
}

View File

@@ -1,5 +1,6 @@
package com.justpickup.userservice.domain.user.dto; package com.justpickup.userservice.domain.user.dto;
import com.justpickup.userservice.domain.user.entity.AuthType;
import com.justpickup.userservice.domain.user.entity.Customer; import com.justpickup.userservice.domain.user.entity.Customer;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
@@ -7,12 +8,17 @@ import lombok.Getter;
@Getter @Getter
public class CustomerDto extends UserDto { public class CustomerDto extends UserDto {
private AuthType authType;
public CustomerDto(Customer customer) { public CustomerDto(Customer customer) {
super(customer); super(customer);
this.authType = customer.getOauthType();
} }
@Builder @Builder
public CustomerDto(Long id, String password, String name, String phoneNumber) { public CustomerDto(Long id, String email, String password, String name,
super(id, password, name, phoneNumber); String phoneNumber, String dtype, String refreshTokenId) {
super(id, email, password, name, phoneNumber, dtype, refreshTokenId);
} }
} }

View File

@@ -0,0 +1,16 @@
package com.justpickup.userservice.domain.user.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class JwtTokenDto {
private String accessToken;
private String refreshToken;
@Builder
public JwtTokenDto(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}

View File

@@ -0,0 +1,66 @@
package com.justpickup.userservice.domain.user.dto;
import com.justpickup.userservice.domain.user.entity.AuthType;
import com.justpickup.userservice.domain.user.entity.Customer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OAuthAttributeDto {
private Map<String, Object> attributes; // OAuth2 반환하는 유저정보 MAP
private String nameAttributeKey;
private String name;
private String email;
private AuthType authType;
public OAuthAttributeDto(Map<String, Object> attributes, String nameAttributeKey, String name, String email) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
}
public static OAuthAttributeDto of(String registrationId, String userNameAttributeName, Map<String, Object> attributes){
// 여기서 네이버와 카카오 등 구분 (ofNaver, ofKakao)
if("naver".equals(registrationId))
return ofNaver(userNameAttributeName , attributes);
return ofGoogle(userNameAttributeName, attributes);
}
private static OAuthAttributeDto ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuthAttributeDto.builder()
.name((String) response.get("name"))
.email((String) response.get("email"))
.nameAttributeKey("id")
.attributes(response)
.authType(AuthType.NAVER)
.build();
}
private static OAuthAttributeDto ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributeDto.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.nameAttributeKey(userNameAttributeName)
.attributes(attributes)
.authType(AuthType.GOOGLE)
.build();
}
public Customer toEntity(OAuthAttributeDto attributeDto){
return new Customer(email,"temp",name,null, attributeDto.getAuthType());
}
}

View File

@@ -0,0 +1,16 @@
package com.justpickup.userservice.domain.user.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class StoreOwnerDto extends UserDto {
private String businessNumber;
@Builder
public StoreOwnerDto(Long id, String email, String password, String name,
String phoneNumber, String dtype, String businessNumber, String refreshTokenId) {
super(id, email, password, name, phoneNumber, dtype, refreshTokenId);
this.businessNumber = businessNumber;
}
}

View File

@@ -1,15 +1,17 @@
package com.justpickup.userservice.domain.user.dto; package com.justpickup.userservice.domain.user.dto;
import com.justpickup.userservice.domain.user.entity.Customer; import com.justpickup.userservice.domain.user.entity.Customer;
import lombok.Builder;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public class UserDto { public abstract class UserDto {
private Long id; private Long id;
private String email;
private String password; private String password;
private String name; private String name;
private String phoneNumber; private String phoneNumber;
private String dtype;
private String refreshTokenId;
// == 생성 메소드 == // // == 생성 메소드 == //
public UserDto(Customer customer) { public UserDto(Customer customer) {
@@ -19,10 +21,14 @@ public class UserDto {
this.phoneNumber = customer.getPhoneNumber(); this.phoneNumber = customer.getPhoneNumber();
} }
public UserDto(Long id, String password, String name, String phoneNumber) { public UserDto(Long id, String email, String password, String name, String phoneNumber,
String dtype, String refreshTokenId) {
this.id = id; this.id = id;
this.email = email;
this.password = password; this.password = password;
this.name = name; this.name = name;
this.phoneNumber = phoneNumber; this.phoneNumber = phoneNumber;
this.dtype = dtype;
this.refreshTokenId = refreshTokenId;
} }
} }

View File

@@ -0,0 +1,7 @@
package com.justpickup.userservice.domain.user.entity;
public enum AuthType {
GITHUB,
NAVER,
GOOGLE
}

View File

@@ -5,11 +5,20 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table; import javax.persistence.Table;
@Entity @Entity
@Table(name = "customer") @Table(name = "customer")
@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Customer extends User { public class Customer extends User {
@Enumerated(EnumType.STRING)
private AuthType oauthType;
public Customer(String email, String password, String name, String phoneNumber, AuthType oauthType) {
super(email, password, name, phoneNumber);
this.dtype = Customer.class.getSimpleName();
this.oauthType = oauthType;
}
} }

View File

@@ -12,4 +12,10 @@ import javax.persistence.Table;
@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class StoreOwner extends User { public class StoreOwner extends User {
private String businessNumber; private String businessNumber;
public StoreOwner(String email, String password, String name, String phoneNumber,
String businessNumber) {
super(email, password, name, phoneNumber);
this.businessNumber = businessNumber;
}
} }

View File

@@ -12,15 +12,28 @@ import javax.persistence.*;
@Inheritance(strategy = InheritanceType.JOINED) @Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE") @DiscriminatorColumn(name = "DTYPE")
@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity { public abstract class User extends BaseEntity {
@Id @GeneratedValue @Id @GeneratedValue
@Column(name = "user_id") @Column(name = "user_id")
private Long id; private Long id;
private String email;
private String password; private String password;
private String name; private String name;
private String phoneNumber; private String phoneNumber;
@Column(insertable = false,updatable = false)
protected String dtype;
public User(String email, String password, String name, String phoneNumber) {
this.email = email;
this.password = password;
this.name = name;
this.phoneNumber = phoneNumber;
}
} }

View File

@@ -8,4 +8,5 @@ public class NotExistUserException extends CustomException {
public NotExistUserException(String message) { public NotExistUserException(String message) {
super(HttpStatus.CONFLICT, message); super(HttpStatus.CONFLICT, message);
} }
} }

View File

@@ -1,7 +1,12 @@
package com.justpickup.userservice.domain.user.repository; package com.justpickup.userservice.domain.user.repository;
import com.justpickup.userservice.domain.user.entity.AuthType;
import com.justpickup.userservice.domain.user.entity.Customer; import com.justpickup.userservice.domain.user.entity.Customer;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CustomerRepository extends JpaRepository<Customer, Long> { public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByEmail(String email);
} }

View File

@@ -0,0 +1,10 @@
package com.justpickup.userservice.domain.user.repository;
import com.justpickup.userservice.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String username);
}

View File

@@ -1,7 +1,13 @@
package com.justpickup.userservice.domain.user.service; package com.justpickup.userservice.domain.user.service;
import com.justpickup.userservice.domain.user.dto.CustomerDto; import com.justpickup.userservice.domain.user.dto.CustomerDto;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import com.justpickup.userservice.domain.user.entity.StoreOwner;
public interface UserService { public interface UserService {
CustomerDto findCustomerByUserId(Long userId); CustomerDto findCustomerByUserId(Long userId);
StoreOwner saveStoreOwner(StoreOwnerDto storeOwnerDto);
} }

View File

@@ -1,21 +1,48 @@
package com.justpickup.userservice.domain.user.service; package com.justpickup.userservice.domain.user.service;
import com.justpickup.userservice.domain.user.dto.CustomerDto; import com.justpickup.userservice.domain.user.dto.CustomerDto;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import com.justpickup.userservice.domain.user.entity.Customer; import com.justpickup.userservice.domain.user.entity.Customer;
import com.justpickup.userservice.domain.user.entity.StoreOwner;
import com.justpickup.userservice.domain.user.entity.User;
import com.justpickup.userservice.domain.user.exception.NotExistUserException; import com.justpickup.userservice.domain.user.exception.NotExistUserException;
import com.justpickup.userservice.domain.user.repository.CustomerRepository; import com.justpickup.userservice.domain.user.repository.CustomerRepository;
import com.justpickup.userservice.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional(readOnly = true) @Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
@Slf4j @Slf4j
public class UserServiceImpl implements UserService { public class UserServiceImpl implements UserService, UserDetailsService {
private final CustomerRepository customerRepository; private final CustomerRepository customerRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found in the database"));
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(user.getDtype()));
return new org.springframework.security.core.userdetails.User(user.getId().toString(), user.getPassword(), authorities);
}
@Override @Override
public CustomerDto findCustomerByUserId(Long userId) { public CustomerDto findCustomerByUserId(Long userId) {
@@ -24,4 +51,15 @@ public class UserServiceImpl implements UserService {
return new CustomerDto(customer); return new CustomerDto(customer);
} }
@Override
@Transactional
public StoreOwner saveStoreOwner(StoreOwnerDto storeOwnerDto) {
String encode = passwordEncoder.encode(storeOwnerDto.getPassword());
StoreOwner storeOwner = new StoreOwner(storeOwnerDto.getEmail(), encode, storeOwnerDto.getName(),
storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber());
return userRepository.save(storeOwner);
}
} }

View File

@@ -1,20 +1,24 @@
package com.justpickup.userservice.domain.user.web; package com.justpickup.userservice.domain.user.web;
import com.justpickup.userservice.domain.user.dto.CustomerDto; import com.justpickup.userservice.domain.user.dto.CustomerDto;
import com.justpickup.userservice.domain.user.entity.Customer;
import com.justpickup.userservice.domain.user.service.UserService; import com.justpickup.userservice.domain.user.service.UserService;
import com.justpickup.userservice.global.dto.Result; import com.justpickup.userservice.global.dto.Result;
import lombok.AllArgsConstructor; import lombok.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.Objects;
import java.util.Optional;
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -47,4 +51,6 @@ public class UserController {
} }
} }
} }

View File

@@ -0,0 +1,15 @@
package com.justpickup.userservice.global.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,31 @@
package com.justpickup.userservice.global.config;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
private final RedisProperties redisProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}

View File

@@ -0,0 +1,10 @@
package com.justpickup.userservice.global.dto;
import lombok.Data;
@Data
public class LoginRequest {
private String name;
private String email;
private String password;
}

View File

@@ -1,8 +1,13 @@
package com.justpickup.userservice.global.exception; package com.justpickup.userservice.global.exception;
import com.justpickup.userservice.domain.jwt.exception.RefreshTokenNotValidException;
import com.justpickup.userservice.global.dto.Result; import com.justpickup.userservice.global.dto.Result;
import com.justpickup.userservice.global.utils.CookieProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
@@ -11,9 +16,12 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice @RestControllerAdvice
@RequiredArgsConstructor
@Slf4j @Slf4j
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
private final CookieProvider cookieProvider;
@ExceptionHandler(CustomException.class) @ExceptionHandler(CustomException.class)
public ResponseEntity customExceptionHandler(CustomException ce) { public ResponseEntity customExceptionHandler(CustomException ce) {
HttpStatus status = ce.getStatus(); HttpStatus status = ce.getStatus();
@@ -23,6 +31,16 @@ public class GlobalExceptionHandler {
.body(errorResult); .body(errorResult);
} }
@ExceptionHandler(RefreshTokenNotValidException.class)
public ResponseEntity customJwtExceptionHandler(RefreshTokenNotValidException e) {
// 쿠키 삭제
ResponseCookie responseCookie = cookieProvider.removeRefreshTokenCookie();
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.header(HttpHeaders.SET_COOKIE, responseCookie.toString())
.body(e.getResult());
}
@ExceptionHandler(BindException.class) @ExceptionHandler(BindException.class)
public ResponseEntity bindExceptionHandler(BindException exception) { public ResponseEntity bindExceptionHandler(BindException exception) {
return getValidationErrorBody(exception); return getValidationErrorBody(exception);

View File

@@ -0,0 +1,28 @@
package com.justpickup.userservice.global.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class HeaderAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getServletPath().equals("/login")) {
filterChain.doFilter(request, response);
return;
}
String email = request.getHeader("jwt-sub");
log.info("email jwt-sub = {}", email);
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,99 @@
package com.justpickup.userservice.global.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.global.utils.JwtTokenProvider;
import com.justpickup.userservice.global.dto.LoginRequest;
import com.justpickup.userservice.global.dto.Result;
import com.justpickup.userservice.global.utils.CookieProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequiredArgsConstructor
@Slf4j
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
private final CookieProvider cookieProvider;
// login 리퀘스트 패스로 오는 요청을 판단
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
Authentication authentication;
try {
LoginRequest credential = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(credential.getEmail(), credential.getPassword())
);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return authentication;
}
// 로그인 성공 이후 토큰 생성
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
org.springframework.security.core.userdetails.User user = (User) authResult.getPrincipal();
List<String> roles = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
String userId = user.getUsername();
String accessToken = jwtTokenProvider.createJwtAccessToken(userId, request.getRequestURI(), roles);
String refreshToken = jwtTokenProvider.createJwtRefreshToken();
refreshTokenServiceImpl.updateRefreshToken(Long.valueOf(userId), jwtTokenProvider.getRefreshTokenId(refreshToken));
// 쿠키 설정
ResponseCookie refreshTokenCookie = cookieProvider.createRefreshTokenCookie(refreshToken);
Cookie cookie = cookieProvider.of(refreshTokenCookie);
response.setContentType(APPLICATION_JSON_VALUE);
response.addCookie(cookie);
// body 설정
Map<String, String> tokens = Map.of(
"access_token", accessToken
);
new ObjectMapper().writeValue(response.getOutputStream(), Result.createSuccessResult(tokens));
}
@Override
protected void unsuccessfulAuthentication
(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
log.warn("로그인 실패!!");
}
}

View File

@@ -0,0 +1,67 @@
package com.justpickup.userservice.global.security;
import com.justpickup.userservice.domain.jwt.service.OAuthService;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.global.utils.JwtTokenProvider;
import com.justpickup.userservice.domain.user.service.UserService;
import com.justpickup.userservice.global.utils.CookieProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
private final CookieProvider cookieProvider;
private final OAuthService oAuthService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
LoginAuthenticationFilter loginAuthenticationFilter =
new LoginAuthenticationFilter(authenticationManagerBean(), jwtTokenProvider, refreshTokenServiceImpl, cookieProvider);
loginAuthenticationFilter.setFilterProcessesUrl("/login");
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().anyRequest().permitAll();
http.logout()
.logoutUrl("/logout")
.deleteCookies("refresh-token");
http.oauth2Login()
.defaultSuccessUrl("http://just-pickup.com:8000/customer-frontend-service/")
.userInfoEndpoint()
.userService(oAuthService);
http.addFilter(loginAuthenticationFilter);
http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}

View File

@@ -0,0 +1,36 @@
package com.justpickup.userservice.global.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
@Component
public class CookieProvider {
@Value("${token.refresh-expired-time}")
private String refreshTokenExpiredTime;
public ResponseCookie createRefreshTokenCookie(String refreshToken) {
return ResponseCookie.from("refresh-token", refreshToken)
.httpOnly(true)
.secure(false)
.path("/")
.maxAge(Long.parseLong(refreshTokenExpiredTime)).build();
}
public ResponseCookie removeRefreshTokenCookie() {
return ResponseCookie.from("refresh-token", null)
.build();
}
public Cookie of(ResponseCookie responseCookie) {
Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue());
cookie.setPath(responseCookie.getPath());
cookie.setSecure(responseCookie.isSecure());
cookie.setHttpOnly(responseCookie.isHttpOnly());
cookie.setMaxAge((int) responseCookie.getMaxAge().getSeconds());
return cookie;
}
}

View File

@@ -0,0 +1,109 @@
package com.justpickup.userservice.global.utils;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Component
@Slf4j
public class JwtTokenProvider {
@Value("${token.access-expired-time}")
private long ACCESS_EXPIRED_TIME;
@Value("${token.refresh-expired-time}")
private long REFRESH_EXPIRED_TIME;
@Value("${token.secret}")
private String SECRET;
public String createJwtAccessToken(String userId, String uri, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userId);
claims.put("roles", roles);
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(uri)
.compact();
}
public String createJwtRefreshToken() {
Claims claims = Jwts.claims();
claims.put("value", UUID.randomUUID());
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + REFRESH_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public String getUserId(String token) {
return getClaimsFromJwtToken(token).getSubject();
}
private Claims getClaimsFromJwtToken(String token) {
try {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
public String getRefreshTokenId(String token) {
return getClaimsFromJwtToken(token).get("value").toString();
}
public List<String> getRoles(String token) {
return (List<String>) getClaimsFromJwtToken(token).get("roles");
}
public boolean validateJwtToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
return false;
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
return false;
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
return false;
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
return false;
}
}
public boolean equalRefreshTokenId(String refreshTokenId, String refreshToken) {
String compareToken = this.getRefreshTokenId(refreshToken);
return refreshTokenId.equals(compareToken);
}
}

View File

@@ -1,9 +1,16 @@
server.port: 0 server.port: 60000
spring: spring:
application: application:
name: user-service name: user-service
config:
import: optional:configserver:http://127.0.0.1:8888
cloud:
config:
name: user-service
profile: dev
datasource: datasource:
url: jdbc:postgresql://localhost:5432/userdb url: jdbc:postgresql://localhost:5432/userdb
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
@@ -12,13 +19,17 @@ spring:
jpa: jpa:
hibernate: hibernate:
ddl-auto: validate ddl-auto: create
generate-ddl: true generate-ddl: true
open-in-view: false open-in-view: false
properties: properties:
hibernate: hibernate:
default_batch_fetch_size: 1000 default_batch_fetch_size: 1000
redis:
host: 127.0.0.1
port: 6379
eureka: eureka:
client: client:
service-url: service-url:
@@ -34,4 +45,11 @@ logging:
# jpa query, parameter 로그 (p6spy) # jpa query, parameter 로그 (p6spy)
decorator.datasource.p6spy: decorator.datasource.p6spy:
enable-logging: true enable-logging: true
token:
access-expired-time: 3600000
refresh-expired-time: 604800000
secret: my-secret
refresh-token-name: refresh-token
access-token-name: access-token