├── backend └── sample │ ├── settings.gradle │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── sample │ │ │ │ ├── domain │ │ │ │ ├── entity │ │ │ │ │ ├── user │ │ │ │ │ │ ├── Provider.java │ │ │ │ │ │ ├── Role.java │ │ │ │ │ │ ├── Token.java │ │ │ │ │ │ └── User.java │ │ │ │ │ └── time │ │ │ │ │ │ └── DefaultTime.java │ │ │ │ └── mapping │ │ │ │ │ ├── BoardMapping.java │ │ │ │ │ └── TokenMapping.java │ │ │ │ ├── config │ │ │ │ ├── security │ │ │ │ │ ├── token │ │ │ │ │ │ ├── CurrentUser.java │ │ │ │ │ │ ├── CustomAuthenticationEntryPoint.java │ │ │ │ │ │ ├── CustomOncePerRequestFilter.java │ │ │ │ │ │ └── UserPrincipal.java │ │ │ │ │ ├── auth │ │ │ │ │ │ ├── OAuth2UserInfo.java │ │ │ │ │ │ ├── company │ │ │ │ │ │ │ ├── Google.java │ │ │ │ │ │ │ ├── Github.java │ │ │ │ │ │ │ ├── Facebook.java │ │ │ │ │ │ │ ├── Kakao.java │ │ │ │ │ │ │ └── Naver.java │ │ │ │ │ │ └── OAuth2UserInfoFactory.java │ │ │ │ │ ├── WebMvcConfig.java │ │ │ │ │ ├── OAuth2Config.java │ │ │ │ │ ├── handler │ │ │ │ │ │ ├── CustomSimpleUrlAuthenticationFailureHandler.java │ │ │ │ │ │ └── CustomSimpleUrlAuthenticationSuccessHandler.java │ │ │ │ │ ├── util │ │ │ │ │ │ └── CustomCookie.java │ │ │ │ │ └── SecurityConfig.java │ │ │ │ └── docs │ │ │ │ │ └── OpenApiConfig.java │ │ │ │ ├── advice │ │ │ │ ├── error │ │ │ │ │ ├── DefaultNullPointerException.java │ │ │ │ │ ├── DefaultException.java │ │ │ │ │ ├── InvalidParameterException.java │ │ │ │ │ └── DefaultAuthenticationException.java │ │ │ │ ├── payload │ │ │ │ │ ├── ErrorCode.java │ │ │ │ │ └── ErrorResponse.java │ │ │ │ ├── assertThat │ │ │ │ │ └── DefaultAssert.java │ │ │ │ └── ApiControllerAdvice.java │ │ │ │ ├── repository │ │ │ │ ├── user │ │ │ │ │ └── UserRepository.java │ │ │ │ └── auth │ │ │ │ │ ├── TokenRepository.java │ │ │ │ │ └── CustomAuthorizationRequestRepository.java │ │ │ │ ├── payload │ │ │ │ ├── response │ │ │ │ │ ├── Message.java │ │ │ │ │ ├── ApiResponse.java │ │ │ │ │ └── AuthResponse.java │ │ │ │ └── request │ │ │ │ │ └── auth │ │ │ │ │ ├── SignInRequest.java │ │ │ │ │ ├── SignUpRequest.java │ │ │ │ │ ├── ChangePasswordRequest.java │ │ │ │ │ └── RefreshTokenRequest.java │ │ │ │ ├── SampleApplication.java │ │ │ │ ├── aop │ │ │ │ ├── AdviceAspect.java │ │ │ │ ├── LogAspect.java │ │ │ │ └── TimeAspect.java │ │ │ │ ├── service │ │ │ │ ├── user │ │ │ │ │ └── UserService.java │ │ │ │ └── auth │ │ │ │ │ ├── CustomUserDetailsService.java │ │ │ │ │ ├── CustomDefaultOAuth2UserService.java │ │ │ │ │ ├── CustomTokenProviderService.java │ │ │ │ │ └── AuthService.java │ │ │ │ └── controller │ │ │ │ └── auth │ │ │ │ └── AuthController.java │ │ └── resources │ │ │ ├── application.properties │ │ │ ├── database │ │ │ └── database-sample.properties │ │ │ ├── swagger │ │ │ └── springdoc.properties │ │ │ └── oauth2 │ │ │ └── oauth2-sample.properties │ └── test │ │ └── java │ │ └── com │ │ └── sample │ │ ├── SampleApplicationTests.java │ │ ├── lib │ │ └── JsonUtils.java │ │ └── controller │ │ └── auth │ │ └── AuthControllerTest.java │ ├── .gitignore │ ├── build.gradle │ ├── gradlew.bat │ └── gradlew ├── frontend └── sample │ ├── README.md │ ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html │ ├── src │ ├── img │ │ ├── fb-logo.png │ │ ├── kakao-logo.png │ │ ├── naver-logo.png │ │ ├── github-logo.png │ │ ├── google-logo.png │ │ └── spring-boot-and-react-js.png │ ├── app │ │ ├── App.css │ │ ├── App.test.js │ │ └── App.js │ ├── common │ │ ├── LoadingIndicator.js │ │ ├── NotFound.css │ │ ├── PrivateRoute.js │ │ ├── NotFound.js │ │ ├── AppHeader.css │ │ └── AppHeader.js │ ├── index.js │ ├── home │ │ ├── Home.js │ │ └── Home.css │ ├── constants │ │ └── index.js │ ├── user │ │ ├── signup │ │ │ ├── Signup.css │ │ │ └── Signup.js │ │ ├── oauth2 │ │ │ └── OAuth2RedirectHandler.js │ │ ├── profile │ │ │ ├── Profile.css │ │ │ └── Profile.js │ │ └── login │ │ │ ├── Login.css │ │ │ └── Login.js │ ├── util │ │ └── APIUtils.js │ ├── logo.svg │ ├── registerServiceWorker.js │ └── index.css │ ├── package.json │ └── .gitignore ├── README ├── main.png ├── register.png ├── google_login.png ├── local_login.png ├── register_success.png ├── google_login_success.png └── local_login_success.png ├── .gitignore └── README.md /backend/sample/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sample' 2 | -------------------------------------------------------------------------------- /frontend/sample/README.md: -------------------------------------------------------------------------------- 1 | # Spring-Boot-Security-Sample-Restful 2 | -------------------------------------------------------------------------------- /README/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/README/main.png -------------------------------------------------------------------------------- /README/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/README/register.png -------------------------------------------------------------------------------- /README/google_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/README/google_login.png -------------------------------------------------------------------------------- /README/local_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/README/local_login.png -------------------------------------------------------------------------------- /README/register_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/README/register_success.png -------------------------------------------------------------------------------- /README/google_login_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/README/google_login_success.png -------------------------------------------------------------------------------- /README/local_login_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/README/local_login_success.png -------------------------------------------------------------------------------- /frontend/sample/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/frontend/sample/public/favicon.ico -------------------------------------------------------------------------------- /frontend/sample/src/img/fb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/frontend/sample/src/img/fb-logo.png -------------------------------------------------------------------------------- /frontend/sample/src/img/kakao-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/frontend/sample/src/img/kakao-logo.png -------------------------------------------------------------------------------- /frontend/sample/src/img/naver-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/frontend/sample/src/img/naver-logo.png -------------------------------------------------------------------------------- /frontend/sample/src/img/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/frontend/sample/src/img/github-logo.png -------------------------------------------------------------------------------- /frontend/sample/src/img/google-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/frontend/sample/src/img/google-logo.png -------------------------------------------------------------------------------- /frontend/sample/src/app/App.css: -------------------------------------------------------------------------------- 1 | .s-alert-box { 2 | min-width: 250px; 3 | } 4 | 5 | .s-alert-close::before, .s-alert-close::after { 6 | width: 2px; 7 | } -------------------------------------------------------------------------------- /backend/sample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/backend/sample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /frontend/sample/src/img/spring-boot-and-react-js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MizzleAa/Spring-Boot-Security-OAuth2-JWT/HEAD/frontend/sample/src/img/spring-boot-and-react-js.png -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/domain/entity/user/Provider.java: -------------------------------------------------------------------------------- 1 | package com.sample.domain.entity.user; 2 | 3 | public enum Provider { 4 | local, 5 | facebook, 6 | google, 7 | github, 8 | kakao, 9 | naver 10 | } 11 | -------------------------------------------------------------------------------- /backend/sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /backend/sample/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # logging 설정 2 | logging.level.org.hibernate.type.descriptor.sql=debug 3 | # console 색상 4 | spring.output.ansi.enabled=always 5 | #오류처리 6 | server.error.include-exception=true 7 | server.error.include-stacktrace=always 8 | # port 설정 9 | server.port=8080 10 | -------------------------------------------------------------------------------- /frontend/sample/src/common/LoadingIndicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function LoadingIndicator(props) { 4 | return ( 5 |
6 | Loading ... 7 |
8 | ); 9 | } -------------------------------------------------------------------------------- /frontend/sample/src/app/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /backend/sample/src/test/java/com/sample/SampleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SampleApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/domain/entity/user/Role.java: -------------------------------------------------------------------------------- 1 | package com.sample.domain.entity.user; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | @Getter 8 | public enum Role { 9 | ADMIN("ROLE_ADMIN"), 10 | USER("ROLE_USER"); 11 | 12 | private String value; 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /frontend/sample/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/domain/mapping/BoardMapping.java: -------------------------------------------------------------------------------- 1 | package com.sample.domain.mapping; 2 | 3 | import java.time.LocalDateTime; 4 | /** 5 | * Mapping 예제 6 | */ 7 | public interface BoardMapping { 8 | Long getId(); 9 | LocalDateTime getCreatedDate(); 10 | String getTitle(); 11 | String getSubtitle(); 12 | String getUsername(); 13 | String getMarkdown(); 14 | String getHtml(); 15 | } 16 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/token/CurrentUser.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.token; 2 | 3 | import java.lang.annotation.*; 4 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 5 | 6 | @Target({ElementType.PARAMETER, ElementType.TYPE}) 7 | @Retention(RetentionPolicy.RUNTIME) 8 | @Documented 9 | @AuthenticationPrincipal 10 | public @interface CurrentUser { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /frontend/sample/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './app/App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | import { BrowserRouter as Router } from 'react-router-dom'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | registerServiceWorker(); 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring-Boot-OAuth2-JWT 2 | 3 | 4 | ### 메인화면 5 | ![main](README/main.png) 6 | 7 | ### 사용자 등록(local) 8 | ![register](README/register.png) 9 | ![register_success](README/register_success.png) 10 | 11 | ### 사용자 로그인(local) 12 | ![local_login](README/local_login.png) 13 | ![local_login_success](README/local_login_success.png) 14 | 15 | ### 외부 사이트 로그인(google, etc) 16 | ![google_login](README/google_login.png) 17 | ![google_login_success](README/google_login_success.png) -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/error/DefaultNullPointerException.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice.error; 2 | 3 | import com.sample.advice.payload.ErrorCode; 4 | 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class DefaultNullPointerException extends NullPointerException{ 9 | 10 | private ErrorCode errorCode; 11 | 12 | public DefaultNullPointerException(ErrorCode errorCode) { 13 | super(errorCode.getMessage()); 14 | this.errorCode = errorCode; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-social", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.5.2", 7 | "react-dom": "^16.5.2", 8 | "react-router-dom": "^4.3.1", 9 | "react-s-alert": "^1.4.1", 10 | "react-scripts": "1.1.5" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/repository/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.sample.repository.user; 2 | 3 | import java.util.Optional; 4 | 5 | import com.sample.domain.entity.user.User; 6 | 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.stereotype.Repository; 9 | 10 | @Repository 11 | public interface UserRepository extends JpaRepository{ 12 | 13 | Optional findByEmail(String email); 14 | Boolean existsByEmail(String email); 15 | } 16 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/repository/auth/TokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.sample.repository.auth; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import java.util.Optional; 7 | 8 | import com.sample.domain.entity.user.Token; 9 | 10 | @Repository 11 | public interface TokenRepository extends JpaRepository { 12 | Optional findByUserEmail(String userEmail); 13 | Optional findByRefreshToken(String refreshToken); 14 | } -------------------------------------------------------------------------------- /frontend/sample/src/common/NotFound.css: -------------------------------------------------------------------------------- 1 | .page-not-found { 2 | background-color: rgb(31, 31, 31); 3 | max-width: 500px; 4 | margin: 0 auto; 5 | margin-top: 50px; 6 | padding: 40px; 7 | border: 1px solid #c8c8c8; 8 | text-align: center; 9 | } 10 | 11 | .page-not-found .title { 12 | font-size: 50px; 13 | letter-spacing: 10px; 14 | margin-bottom: 10px; 15 | } 16 | 17 | .page-not-found .desc { 18 | font-size: 20px; 19 | margin-bottom: 20px; 20 | } 21 | 22 | .go-back-btn { 23 | min-width: 160px; 24 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/payload/response/Message.java: -------------------------------------------------------------------------------- 1 | package com.sample.payload.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.ToString; 7 | 8 | @ToString 9 | @Data 10 | public class Message { 11 | 12 | @Schema( type = "string", example = "메시지 문구를 출력합니다.", description="메시지 입니다.") 13 | private String message; 14 | 15 | public Message(){}; 16 | 17 | @Builder 18 | public Message(String message) { 19 | this.message = message; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/domain/mapping/TokenMapping.java: -------------------------------------------------------------------------------- 1 | package com.sample.domain.mapping; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class TokenMapping { 8 | private String userEmail; 9 | private String accessToken; 10 | private String refreshToken; 11 | 12 | public TokenMapping(){} 13 | 14 | @Builder 15 | public TokenMapping(String userEmail, String accessToken, String refreshToken){ 16 | this.userEmail = userEmail; 17 | this.accessToken = accessToken; 18 | this.refreshToken = refreshToken; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /backend/sample/src/main/resources/database/database-sample.properties: -------------------------------------------------------------------------------- 1 | # db 설정 2 | spring.datasource.driver-class-name={"Database Driver"} 3 | spring.datasource.url={"url"} 4 | spring.datasource.username={"username"} 5 | spring.datasource.password={"password"} 6 | # jpa 설정 7 | spring.jpa.database-platform={"Database Platform"} 8 | spring.jpa.hibernate.ddl-auto=update 9 | #spring.jpa.hibernate.ddl-auto=create-drop 10 | #spring.jpa.hibernate.ddl-auto=none 11 | 12 | # 배표할때는 open-in-view false 설정 13 | spring.jpa.open-in-view=false 14 | spring.jpa.properties.hibernate.show_sql=true 15 | spring.jpa.properties.hibernate.format_sql=true 16 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/error/DefaultException.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice.error; 2 | 3 | 4 | import com.sample.advice.payload.ErrorCode; 5 | 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class DefaultException extends RuntimeException{ 10 | 11 | private ErrorCode errorCode; 12 | 13 | public DefaultException(ErrorCode errorCode) { 14 | super(errorCode.getMessage()); 15 | this.errorCode = errorCode; 16 | } 17 | 18 | public DefaultException(ErrorCode errorCode, String message) { 19 | super(message); 20 | this.errorCode = errorCode; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /backend/sample/src/main/resources/swagger/springdoc.properties: -------------------------------------------------------------------------------- 1 | #sping doc 설정 2 | springdoc.swagger-ui.path=swagger 3 | #"사용해 보기" 섹션이 기본적으로 활성화되어야 하는지 여부를 제어 4 | springdoc.swagger-ui.tryItOutEnabled=true 5 | # filter 검색 6 | springdoc.swagger-ui.filter=true 7 | springdoc.swagger-ui.operationsSorter=method 8 | # ms단위 표시 9 | springdoc.swagger-ui.displayRequestDuration=true 10 | springdoc.swagger-ui.supportedSubmitMethods="get", "put", "post", "delete", "options", "head", "patch", "trace" 11 | 12 | #api-doc 비활성화 13 | #springdoc.api-docs.enabled=false 14 | #swagger-ui 비활성화 15 | #springdoc.swagger-ui.enabled=false 16 | # springdoc.swagger-ui.queryConfigEnabled=false 17 | -------------------------------------------------------------------------------- /frontend/sample/src/common/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Route, 4 | Redirect 5 | } from "react-router-dom"; 6 | 7 | 8 | const PrivateRoute = ({ component: Component, authenticated, ...rest }) => ( 9 | 12 | authenticated ? ( 13 | 14 | ) : ( 15 | 21 | ) 22 | } 23 | /> 24 | ); 25 | 26 | export default PrivateRoute -------------------------------------------------------------------------------- /backend/sample/.gitignore: -------------------------------------------------------------------------------- 1 | database.properties 2 | oauth2.properties 3 | 4 | HELP.md 5 | .gradle 6 | build/ 7 | !gradle/wrapper/gradle-wrapper.jar 8 | !**/src/main/**/build/ 9 | !**/src/test/**/build/ 10 | 11 | ### STS ### 12 | .apt_generated 13 | .classpath 14 | .factorypath 15 | .project 16 | .settings 17 | .springBeans 18 | .sts4-cache 19 | bin/ 20 | !**/src/main/**/bin/ 21 | !**/src/test/**/bin/ 22 | 23 | ### IntelliJ IDEA ### 24 | .idea 25 | *.iws 26 | *.iml 27 | *.ipr 28 | out/ 29 | !**/src/main/**/out/ 30 | !**/src/test/**/out/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | -------------------------------------------------------------------------------- /frontend/sample/src/common/NotFound.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './NotFound.css'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | class NotFound extends Component { 6 | render() { 7 | return ( 8 |
9 |

10 | 404 11 |

12 |
13 | 해당 페이지는 찾을 수 없습니다.. 14 |
15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default NotFound; -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/auth/OAuth2UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.auth; 2 | 3 | import java.util.Map; 4 | 5 | public abstract class OAuth2UserInfo { 6 | protected Map attributes; 7 | 8 | public OAuth2UserInfo(Map attributes) { 9 | this.attributes = attributes; 10 | } 11 | 12 | public Map getAttributes() { 13 | return attributes; 14 | } 15 | 16 | public abstract String getProvider(); 17 | 18 | public abstract String getId(); 19 | 20 | public abstract String getName(); 21 | 22 | public abstract String getEmail(); 23 | 24 | public abstract String getImageUrl(); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/sample/src/home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Home.css'; 3 | import springReactImage from '../img/spring-boot-and-react-js.png'; 4 | 5 | class Home extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 | Naver 11 |

Sample OAuth2 & JWT

12 | Copyright © 2022 Mizzle Inc. Policy Edit page on GitHub 13 |
14 |
15 | ) 16 | } 17 | } 18 | 19 | export default Home; -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/payload/request/auth/SignInRequest.java: -------------------------------------------------------------------------------- 1 | package com.sample.payload.request.auth; 2 | 3 | import javax.validation.constraints.Email; 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.NotNull; 6 | 7 | import io.swagger.v3.oas.annotations.media.Schema; 8 | import lombok.Data; 9 | 10 | @Data 11 | public class SignInRequest { 12 | 13 | @Schema( type = "string", example = "string@aa.bb", description="계정 이메일 입니다.") 14 | @NotBlank 15 | @NotNull 16 | @Email 17 | private String email; 18 | 19 | @Schema( type = "string", example = "string", description="계정 비밀번호 입니다.") 20 | @NotBlank 21 | @NotNull 22 | private String password; 23 | } 24 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/error/InvalidParameterException.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice.error; 2 | 3 | import java.util.List; 4 | 5 | import com.sample.advice.payload.ErrorCode; 6 | 7 | import org.springframework.validation.Errors; 8 | import org.springframework.validation.FieldError; 9 | 10 | import lombok.Getter; 11 | 12 | @Getter 13 | public class InvalidParameterException extends DefaultException{ 14 | 15 | private final Errors errors; 16 | 17 | public InvalidParameterException(Errors errors) { 18 | super(ErrorCode.INVALID_PARAMETER); 19 | this.errors = errors; 20 | } 21 | 22 | public List getFieldErrors() { 23 | return errors.getFieldErrors(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/payload/request/auth/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.sample.payload.request.auth; 2 | 3 | import javax.validation.constraints.Email; 4 | import javax.validation.constraints.NotBlank; 5 | 6 | import io.swagger.v3.oas.annotations.media.Schema; 7 | import lombok.Data; 8 | 9 | @Data 10 | public class SignUpRequest { 11 | 12 | @Schema( type = "string", example = "string", description="계정 명 입니다.") 13 | @NotBlank 14 | private String name; 15 | 16 | @Schema( type = "string", example = "string@aa.bb", description="계정 이메일 입니다.") 17 | @NotBlank 18 | @Email 19 | private String email; 20 | 21 | @Schema( type = "string", example = "string", description="계정 비밀번호 입니다.") 22 | @NotBlank 23 | private String password; 24 | } 25 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/SampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.sample; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.PropertySource; 6 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 7 | 8 | @EnableJpaAuditing 9 | @SpringBootApplication 10 | @PropertySource(value = { "classpath:database/database.properties" }) 11 | @PropertySource(value = { "classpath:oauth2/oauth2.properties" }) 12 | @PropertySource(value = { "classpath:swagger/springdoc.properties" }) 13 | public class SampleApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(SampleApplication.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/aop/AdviceAspect.java: -------------------------------------------------------------------------------- 1 | package com.sample.aop; 2 | 3 | import org.aspectj.lang.ProceedingJoinPoint; 4 | import org.aspectj.lang.annotation.Around; 5 | import org.aspectj.lang.annotation.Aspect; 6 | import org.springframework.stereotype.Component; 7 | 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | @Slf4j 11 | @Aspect 12 | @Component 13 | public class AdviceAspect { 14 | 15 | @Around("execution(* com.sample.advice.*.*(..))") 16 | public Object adviceController(ProceedingJoinPoint proceedingJoinPoint) throws Throwable, Exception { 17 | log.error("Adivce Error = {}.{}", proceedingJoinPoint.getSignature().getDeclaringTypeName(), proceedingJoinPoint.getSignature().getName()); 18 | Object result = proceedingJoinPoint.proceed(); 19 | return result; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/payload/response/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.sample.payload.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.ToString; 7 | 8 | @ToString 9 | @Data 10 | public class ApiResponse { 11 | 12 | @Schema( type = "boolean", example = "true", description="올바르게 로직을 처리했으면 True, 아니면 False를 반환합니다.") 13 | private boolean check; 14 | 15 | @Schema( type = "object", example = "information", description="restful의 정보를 감싸 표현합니다. object형식으로 표현합니다.") 16 | private Object information; 17 | 18 | public ApiResponse(){}; 19 | 20 | @Builder 21 | public ApiResponse(boolean check, Object information) { 22 | this.check = check; 23 | this.information = information; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/sample/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const API_BASE_URL = 'http://localhost:8080'; 2 | export const ACCESS_TOKEN = 'accessToken'; 3 | export const REFRESH_TOKEN = 'refreshToken'; 4 | 5 | export const OAUTH2_REDIRECT_URI = 'http://localhost:3000/oauth2/redirect' 6 | 7 | export const GOOGLE_AUTH_URL = API_BASE_URL + '/oauth2/authorize/google?redirect_uri=' + OAUTH2_REDIRECT_URI; 8 | export const FACEBOOK_AUTH_URL = API_BASE_URL + '/oauth2/authorize/facebook?redirect_uri=' + OAUTH2_REDIRECT_URI; 9 | export const GITHUB_AUTH_URL = API_BASE_URL + '/oauth2/authorize/github?redirect_uri=' + OAUTH2_REDIRECT_URI; 10 | export const KAKAO_AUTH_URL = API_BASE_URL + '/oauth2/authorize/kakao?redirect_uri=' + OAUTH2_REDIRECT_URI; 11 | export const NAVER_AUTH_URL = API_BASE_URL + '/oauth2/authorize/naver?redirect_uri=' + OAUTH2_REDIRECT_URI; 12 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/payload/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice.payload; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum ErrorCode { 7 | INVALID_PARAMETER(400, null, "잘못된 요청 데이터 입니다."), 8 | INVALID_REPRESENTATION(400, null, "잘못된 표현 입니다."), 9 | INVALID_FILE_PATH(400, null, "잘못된 파일 경로 입니다."), 10 | INVALID_OPTIONAL_ISPRESENT(400, null, "해당 값이 존재하지 않습니다."), 11 | INVALID_CHECK(400, null, "해당 값이 유효하지 않습니다."), 12 | INVALID_AUTHENTICATION(400, null, "잘못된 인증입니다."); 13 | 14 | private final String code; 15 | private final String message; 16 | private final int status; 17 | 18 | ErrorCode(final int status, final String code, final String message) { 19 | this.status = status; 20 | this.message = message; 21 | this.code = code; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/domain/entity/time/DefaultTime.java: -------------------------------------------------------------------------------- 1 | package com.sample.domain.entity.time; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.EntityListeners; 7 | import javax.persistence.MappedSuperclass; 8 | 9 | import org.springframework.data.annotation.CreatedDate; 10 | import org.springframework.data.annotation.LastModifiedDate; 11 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 12 | 13 | import lombok.Getter; 14 | 15 | @Getter 16 | @MappedSuperclass 17 | @EntityListeners(AuditingEntityListener.class) 18 | public abstract class DefaultTime { 19 | 20 | @CreatedDate 21 | @Column(updatable=false) 22 | private LocalDateTime createdDate; 23 | 24 | @LastModifiedDate 25 | private LocalDateTime modifiedDate; 26 | 27 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/payload/request/auth/ChangePasswordRequest.java: -------------------------------------------------------------------------------- 1 | package com.sample.payload.request.auth; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | 6 | import io.swagger.v3.oas.annotations.media.Schema; 7 | import lombok.Data; 8 | 9 | @Data 10 | public class ChangePasswordRequest { 11 | 12 | @Schema( type = "string", example = "string", description="기존 비밀번호 입니다.") 13 | @NotBlank 14 | @NotNull 15 | private String oldPassword; 16 | 17 | @Schema( type = "string", example = "string123", description="신규 비밀번호 입니다.") 18 | @NotBlank 19 | @NotNull 20 | private String newPassword; 21 | 22 | @Schema( type = "string", example = "string123", description="신규 비밀번호 확인란 입니다.") 23 | @NotBlank 24 | @NotNull 25 | private String reNewPassword; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/payload/request/auth/RefreshTokenRequest.java: -------------------------------------------------------------------------------- 1 | package com.sample.payload.request.auth; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | 6 | import io.swagger.v3.oas.annotations.media.Schema; 7 | 8 | import lombok.Builder; 9 | import lombok.Data; 10 | 11 | @Data 12 | public class RefreshTokenRequest { 13 | 14 | @Schema( type = "string", example = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NTI3OTgxOTh9.6CoxHB_siOuz6PxsxHYQCgUT1_QbdyKTUwStQDutEd1-cIIARbQ0cyrnAmpIgi3IBoLRaqK7N1vXO42nYy4g5g", description="refresh token 입니다." ) 15 | @NotBlank 16 | @NotNull 17 | private String refreshToken; 18 | 19 | public RefreshTokenRequest(){} 20 | 21 | @Builder 22 | public RefreshTokenRequest(String refreshToken){ 23 | this.refreshToken = refreshToken; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/error/DefaultAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice.error; 2 | 3 | import com.sample.advice.payload.ErrorCode; 4 | 5 | import org.springframework.security.core.AuthenticationException; 6 | 7 | import lombok.Getter; 8 | 9 | 10 | @Getter 11 | public class DefaultAuthenticationException extends AuthenticationException{ 12 | 13 | private ErrorCode errorCode; 14 | 15 | public DefaultAuthenticationException(String msg, Throwable t) { 16 | super(msg, t); 17 | this.errorCode = ErrorCode.INVALID_REPRESENTATION; 18 | } 19 | 20 | public DefaultAuthenticationException(String msg) { 21 | super(msg); 22 | } 23 | 24 | public DefaultAuthenticationException(ErrorCode errorCode) { 25 | super(errorCode.getMessage()); 26 | this.errorCode = errorCode; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/token/CustomAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.token; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.security.web.AuthenticationEntryPoint; 11 | 12 | public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint{ 13 | 14 | @Override 15 | public void commence(HttpServletRequest request, HttpServletResponse response, 16 | AuthenticationException authException) throws IOException, ServletException { 17 | 18 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getLocalizedMessage()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/aop/LogAspect.java: -------------------------------------------------------------------------------- 1 | package com.sample.aop; 2 | 3 | import org.aspectj.lang.ProceedingJoinPoint; 4 | import org.aspectj.lang.annotation.Around; 5 | import org.aspectj.lang.annotation.Aspect; 6 | 7 | import org.springframework.stereotype.Component; 8 | 9 | 10 | @Aspect 11 | @Component 12 | public class LogAspect { 13 | 14 | @Around("execution(* com.sample.controller.*.*(..))") 15 | public Object ControllerLogger(ProceedingJoinPoint proceedingJoinPoint) throws Throwable, Exception { 16 | //log.info("start = {} / {}", proceedingJoinPoint.getSignature().getDeclaringTypeName(), proceedingJoinPoint.getSignature().getName()); 17 | Object result = proceedingJoinPoint.proceed(); 18 | //log.info("end = {} / {}", proceedingJoinPoint.getSignature().getDeclaringTypeName(), proceedingJoinPoint.getSignature().getName()); 19 | return result; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/sample/src/test/java/com/sample/lib/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.sample.lib; 2 | 3 | import org.json.simple.JSONObject; 4 | import org.json.simple.parser.JSONParser; 5 | import org.json.simple.parser.ParseException; 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | 9 | public class JsonUtils { 10 | 11 | //dto를 object mapper로 통해 json 으로 저장 12 | public static String asJsonToString(Object object) { 13 | try { 14 | return new ObjectMapper().writeValueAsString(object); 15 | } catch (Exception e) { 16 | throw new RuntimeException(e); 17 | } 18 | } 19 | 20 | //string 값을 json 형식으로 변경 21 | public static JSONObject asStringToJson(String string) throws ParseException{ 22 | JSONParser jsonParser = new JSONParser(); 23 | Object object = jsonParser.parse( string ); 24 | JSONObject jsonObject = (JSONObject) object; 25 | return jsonObject; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/aop/TimeAspect.java: -------------------------------------------------------------------------------- 1 | package com.sample.aop; 2 | 3 | 4 | import org.aspectj.lang.ProceedingJoinPoint; 5 | import org.aspectj.lang.annotation.Around; 6 | import org.aspectj.lang.annotation.Aspect; 7 | import org.springframework.stereotype.Component; 8 | 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | @Aspect 13 | @Component 14 | public class TimeAspect { 15 | 16 | @Around("execution(* com.sample.controller.*.*(..))") 17 | public Object timerController(ProceedingJoinPoint proceedingJoinPoint) throws Throwable, Exception { 18 | 19 | long startTime = System.currentTimeMillis(); 20 | Object result = proceedingJoinPoint.proceed(); 21 | long endTime = System.currentTimeMillis(); 22 | long totalTime = endTime - startTime; 23 | 24 | log.info("{}.{} = {}ms", proceedingJoinPoint.getSignature().getDeclaringTypeName(),proceedingJoinPoint.getSignature().getName() , totalTime); 25 | return result; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /frontend/sample/src/common/AppHeader.css: -------------------------------------------------------------------------------- 1 | .app-header { 2 | background-color: rgb(25, 25, 25); 3 | height: 60px; 4 | position: relative; 5 | z-index: 10; 6 | } 7 | 8 | .app-title { 9 | color: antiquewhite; 10 | line-height: 60px; 11 | vertical-align: middle; 12 | font-size: 1.35em; 13 | } 14 | 15 | .app-title:hover { 16 | color:azure; 17 | } 18 | 19 | .app-branding { 20 | float: left; 21 | } 22 | 23 | .app-options { 24 | float: right; 25 | } 26 | 27 | .app-nav ul { 28 | list-style-position: none; 29 | margin: 0; 30 | padding: 0; 31 | } 32 | 33 | .app-nav ul li { 34 | list-style-type: none; 35 | display: inline-block; 36 | } 37 | 38 | .app-nav ul li a { 39 | color: antiquewhite; 40 | display: inline-block; 41 | line-height: 60px; 42 | vertical-align: middle; 43 | padding-left: 15px; 44 | padding-right: 15px; 45 | } 46 | 47 | .app-nav ul li a:hover { 48 | color: azure; 49 | } 50 | 51 | .app-nav ul li a.active { 52 | color: azure; 53 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | public class WebMvcConfig implements WebMvcConfigurer{ 10 | 11 | private final long MAX_AGE_SECS = 3600; 12 | 13 | @Value("${app.cors.allowedOrigins}") 14 | private String[] allowedOrigins; 15 | 16 | @Override 17 | public void addCorsMappings(CorsRegistry registry) { 18 | registry.addMapping("/**") 19 | .allowedOrigins(allowedOrigins) 20 | .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") 21 | .allowedHeaders("*") 22 | .allowCredentials(true) 23 | .maxAge(MAX_AGE_SECS); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/service/user/UserService.java: -------------------------------------------------------------------------------- 1 | package com.sample.service.user; 2 | 3 | import java.util.Optional; 4 | 5 | import com.sample.advice.assertThat.DefaultAssert; 6 | import com.sample.config.security.token.UserPrincipal; 7 | import com.sample.domain.entity.user.User; 8 | import com.sample.payload.response.ApiResponse; 9 | import com.sample.repository.user.UserRepository; 10 | 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.stereotype.Service; 13 | 14 | import lombok.RequiredArgsConstructor; 15 | 16 | @RequiredArgsConstructor 17 | @Service 18 | public class UserService { 19 | private final UserRepository userRepository; 20 | 21 | public ResponseEntity readByUser(UserPrincipal userPrincipal){ 22 | Optional user = userRepository.findById(userPrincipal.getId()); 23 | DefaultAssert.isOptionalPresent(user); 24 | ApiResponse apiResponse = ApiResponse.builder().check(true).information(user.get()).build(); 25 | return ResponseEntity.ok(apiResponse); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/domain/entity/user/Token.java: -------------------------------------------------------------------------------- 1 | package com.sample.domain.entity.user; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import javax.persistence.Table; 7 | 8 | import com.sample.domain.entity.time.DefaultTime; 9 | 10 | import lombok.Builder; 11 | import lombok.Getter; 12 | 13 | @Getter 14 | @Table(name="token") 15 | @Entity 16 | public class Token extends DefaultTime{ 17 | 18 | @Id 19 | @Column(name = "user_email", length = 1024 , nullable = false) 20 | private String userEmail; 21 | 22 | @Column(name = "refresh_token", length = 1024 , nullable = false) 23 | private String refreshToken; 24 | 25 | public Token(){} 26 | 27 | public Token updateRefreshToken(String refreshToken) { 28 | this.refreshToken = refreshToken; 29 | return this; 30 | } 31 | 32 | @Builder 33 | public Token(String userEmail, String refreshToken) { 34 | this.userEmail = userEmail; 35 | this.refreshToken = refreshToken; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/auth/company/Google.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.auth.company; 2 | 3 | import java.util.Map; 4 | 5 | import com.sample.config.security.auth.OAuth2UserInfo; 6 | import com.sample.domain.entity.user.Provider; 7 | 8 | public class Google extends OAuth2UserInfo{ 9 | 10 | public Google(Map attributes) { 11 | super(attributes); 12 | } 13 | 14 | @Override 15 | public String getId() { 16 | 17 | return (String) attributes.get("sub"); 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | 23 | return (String) attributes.get("name"); 24 | } 25 | 26 | @Override 27 | public String getEmail() { 28 | 29 | return (String) attributes.get("email"); 30 | } 31 | 32 | @Override 33 | public String getImageUrl() { 34 | 35 | return (String) attributes.get("picture"); 36 | } 37 | 38 | @Override 39 | public String getProvider(){ 40 | return Provider.google.toString(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/auth/company/Github.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.auth.company; 2 | 3 | import java.util.Map; 4 | 5 | import com.sample.config.security.auth.OAuth2UserInfo; 6 | import com.sample.domain.entity.user.Provider; 7 | 8 | public class Github extends OAuth2UserInfo{ 9 | 10 | public Github(Map attributes) { 11 | super(attributes); 12 | } 13 | 14 | @Override 15 | public String getId() { 16 | 17 | return ((Integer) attributes.get("id")).toString(); 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | 23 | return (String) attributes.get("name"); 24 | } 25 | 26 | @Override 27 | public String getEmail() { 28 | 29 | return (String) attributes.get("email"); 30 | } 31 | 32 | @Override 33 | public String getImageUrl() { 34 | 35 | return (String) attributes.get("avatar_url"); 36 | } 37 | 38 | @Override 39 | public String getProvider(){ 40 | return Provider.github.toString(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/payload/response/AuthResponse.java: -------------------------------------------------------------------------------- 1 | package com.sample.payload.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | import lombok.Builder; 6 | import lombok.Data; 7 | 8 | @Data 9 | public class AuthResponse { 10 | 11 | @Schema( type = "string", example = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NTI3OTgxOTh9.6CoxHB_siOuz6PxsxHYQCgUT1_QbdyKTUwStQDutEd1-cIIARbQ0cyrnAmpIgi3IBoLRaqK7N1vXO42nYy4g5g" , description="access token 을 출력합니다.") 12 | private String accessToken; 13 | 14 | @Schema( type = "string", example = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NTI3OTgxOTh9.asdf8as4df865as4dfasdf65_asdfweioufsdoiuf_432jdsaFEWFSDV_sadf" , description="refresh token 을 출력합니다.") 15 | private String refreshToken; 16 | 17 | @Schema( type = "string", example ="Bearer", description="권한(Authorization) 값 해더의 명칭을 지정합니다.") 18 | private String tokenType = "Bearer"; 19 | 20 | public AuthResponse(){}; 21 | 22 | @Builder 23 | public AuthResponse(String accessToken, String refreshToken) { 24 | this.accessToken = accessToken; 25 | this.refreshToken = refreshToken; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/sample/src/user/signup/Signup.css: -------------------------------------------------------------------------------- 1 | .signup-container { 2 | background-color: rgb(31, 31, 31); 3 | min-height: calc(100vh - 60px); 4 | text-align: center; 5 | } 6 | 7 | 8 | .signup-content { 9 | background: rgb(25, 25, 25); 10 | box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); 11 | border-radius: 2px; 12 | width: 500px; 13 | display: inline-block; 14 | margin-top: 30px; 15 | vertical-align: middle; 16 | position: relative; 17 | padding: 35px; 18 | } 19 | 20 | .social-btn { 21 | color: azure; 22 | margin-bottom: 15px; 23 | font-weight: 400; 24 | font-size: 16px; 25 | } 26 | 27 | .social-btn:hover { 28 | color:aliceblue; 29 | } 30 | 31 | .social-btn img { 32 | height: 32px; 33 | float: left; 34 | margin-top: 10px; 35 | } 36 | 37 | .social-btn.google { 38 | margin-top: 7px; 39 | } 40 | 41 | .social-btn.facebook img { 42 | height: 24px; 43 | margin-left: 3px; 44 | } 45 | 46 | .social-btn.github img { 47 | height: 24px; 48 | margin-left: 3px; 49 | } 50 | 51 | .login-link { 52 | color: azure; 53 | font-size: 14px; 54 | } 55 | 56 | .signup-title { 57 | font-size: 1.5em; 58 | font-weight: 500; 59 | margin-top: 0; 60 | margin-bottom: 30px; 61 | color: azure; 62 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/OAuth2Config.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import lombok.Data; 10 | 11 | @Configuration 12 | @ConfigurationProperties(prefix = "app") 13 | public class OAuth2Config { 14 | private final Auth auth = new Auth(); 15 | private final OAuth2 oauth2 = new OAuth2(); 16 | 17 | @Data 18 | public static class Auth { 19 | private String tokenSecret; 20 | private long accessTokenExpirationMsec; 21 | private long refreshTokenExpirationMsec; 22 | } 23 | 24 | public static final class OAuth2 { 25 | private List authorizedRedirectUris = new ArrayList<>(); 26 | 27 | public List getAuthorizedRedirectUris() { 28 | return authorizedRedirectUris; 29 | } 30 | 31 | public OAuth2 authorizedRedirectUris(List authorizedRedirectUris) { 32 | this.authorizedRedirectUris = authorizedRedirectUris; 33 | return this; 34 | } 35 | } 36 | 37 | public Auth getAuth() { 38 | return auth; 39 | } 40 | 41 | public OAuth2 getOauth2() { 42 | return oauth2; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/sample/src/user/oauth2/OAuth2RedirectHandler.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ACCESS_TOKEN, REFRESH_TOKEN } from '../../constants'; 3 | import { Redirect } from 'react-router-dom' 4 | 5 | class OAuth2RedirectHandler extends Component { 6 | getUrlParameter(name) { 7 | name = name.replace(/[\\[]/, '\\[').replace(/[\]]/, '\\]'); 8 | var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); 9 | 10 | var results = regex.exec(this.props.location.search); 11 | return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); 12 | }; 13 | 14 | render() { 15 | const token = this.getUrlParameter('token'); 16 | const error = this.getUrlParameter('error'); 17 | 18 | if(token) { 19 | localStorage.setItem(ACCESS_TOKEN, token); 20 | localStorage.setItem(REFRESH_TOKEN, null); 21 | return ; 25 | } else { 26 | return ; 33 | } 34 | } 35 | } 36 | 37 | export default OAuth2RedirectHandler; -------------------------------------------------------------------------------- /frontend/sample/src/user/profile/Profile.css: -------------------------------------------------------------------------------- 1 | .profile-container { 2 | background: rgb(25, 25, 25); 3 | min-height: calc(100vh - 60px); 4 | padding-top: 200px; 5 | } 6 | 7 | .profile-info { 8 | text-align: center; 9 | } 10 | 11 | .profile-info .profile-avatar img { 12 | border-radius: 50%; 13 | max-width: 250px; 14 | } 15 | 16 | .profile-info .profile-name { 17 | color:azure; 18 | font-weight: 500; 19 | font-size: 18px; 20 | } 21 | 22 | .profile-info .profile-email { 23 | color:azure; 24 | font-weight: 400; 25 | } 26 | 27 | .text-avatar { 28 | width: 200px; 29 | height: 200px; 30 | margin: 0 auto; 31 | vertical-align: middle; 32 | text-align: center; 33 | border-radius: 50%; 34 | background: rgb(82, 82, 82); 35 | /* background: linear-gradient(45deg,#46b5e5 1%,#1e88e5 64%,#40baf5 97%); 36 | background-image: -ms-linear-gradient(45deg,#46b5e5 1%,#1e88e5 64%,#40baf5 97%); 37 | background-image: -moz-linear-gradient(45deg,#46b5e5 1%,#1e88e5 64%,#40baf5 97%); 38 | background-image: -o-linear-gradient(45deg,#46b5e5 1%,#1e88e5 64%,#40baf5 97%); 39 | background-image: -webkit-linear-gradient(45deg,#46b5e5 1%,#1e88e5 64%,#40baf5 97%); 40 | background-image: linear-gradient(45deg,#46b5e5 1%,#1e88e5 64%,#40baf5 97%); */ 41 | } 42 | 43 | .text-avatar span { 44 | line-height: 200px; 45 | color: #fff; 46 | font-size: 3em; 47 | } -------------------------------------------------------------------------------- /frontend/sample/src/util/APIUtils.js: -------------------------------------------------------------------------------- 1 | import { API_BASE_URL, ACCESS_TOKEN } from '../constants'; 2 | 3 | const request = (options) => { 4 | const headers = new Headers({ 5 | 'Content-Type': 'application/json', 6 | }) 7 | 8 | if(localStorage.getItem(ACCESS_TOKEN)) { 9 | headers.append('Authorization', 'Bearer ' + localStorage.getItem(ACCESS_TOKEN)) 10 | } 11 | 12 | const defaults = {headers: headers}; 13 | options = Object.assign({}, defaults, options); 14 | 15 | return fetch(options.url, options) 16 | .then(response => 17 | response.json().then(json => { 18 | if(!response.ok) { 19 | return Promise.reject(json); 20 | } 21 | return json; 22 | }) 23 | ); 24 | }; 25 | 26 | export function getCurrentUser() { 27 | if(!localStorage.getItem(ACCESS_TOKEN)) { 28 | return Promise.reject("No access token set."); 29 | } 30 | 31 | return request({ 32 | url: API_BASE_URL + "/auth/", 33 | method: 'GET' 34 | }); 35 | } 36 | 37 | export function login(loginRequest) { 38 | return request({ 39 | url: API_BASE_URL + "/auth/signin", 40 | method: 'POST', 41 | body: JSON.stringify(loginRequest) 42 | }); 43 | } 44 | 45 | export function signup(signupRequest) { 46 | return request({ 47 | url: API_BASE_URL + "/auth/signup", 48 | method: 'POST', 49 | body: JSON.stringify(signupRequest) 50 | }); 51 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/auth/OAuth2UserInfoFactory.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.auth; 2 | 3 | import java.util.Map; 4 | 5 | import com.sample.advice.assertThat.DefaultAssert; 6 | import com.sample.config.security.auth.company.Facebook; 7 | import com.sample.config.security.auth.company.Github; 8 | import com.sample.config.security.auth.company.Google; 9 | import com.sample.config.security.auth.company.Kakao; 10 | import com.sample.config.security.auth.company.Naver; 11 | import com.sample.domain.entity.user.Provider; 12 | 13 | public class OAuth2UserInfoFactory { 14 | public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map attributes) { 15 | if(registrationId.equalsIgnoreCase(Provider.google.toString())) { 16 | return new Google(attributes); 17 | } else if (registrationId.equalsIgnoreCase(Provider.facebook.toString())) { 18 | return new Facebook(attributes); 19 | } else if (registrationId.equalsIgnoreCase(Provider.github.toString())) { 20 | return new Github(attributes); 21 | } else if (registrationId.equalsIgnoreCase(Provider.naver.toString())) { 22 | return new Naver(attributes); 23 | } else if (registrationId.equalsIgnoreCase(Provider.kakao.toString())) { 24 | return new Kakao(attributes); 25 | } else { 26 | DefaultAssert.isAuthentication("해당 oauth2 기능은 지원하지 않습니다."); 27 | } 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/auth/company/Facebook.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.auth.company; 2 | 3 | import java.util.Map; 4 | 5 | import com.sample.config.security.auth.OAuth2UserInfo; 6 | import com.sample.domain.entity.user.Provider; 7 | 8 | public class Facebook extends OAuth2UserInfo{ 9 | 10 | public Facebook(Map attributes) { 11 | super(attributes); 12 | } 13 | 14 | @Override 15 | public String getId() { 16 | return (String) attributes.get("id"); 17 | } 18 | 19 | @Override 20 | public String getName() { 21 | return (String) attributes.get("name"); 22 | } 23 | 24 | @Override 25 | public String getEmail() { 26 | 27 | return (String) attributes.get("email"); 28 | } 29 | 30 | @Override 31 | public String getImageUrl() { 32 | 33 | if(attributes.containsKey("picture")) { 34 | 35 | Map pictureObj = (Map) attributes.get("picture"); 36 | if(pictureObj.containsKey("data")) { 37 | Map dataObj = (Map) pictureObj.get("data"); 38 | if(dataObj.containsKey("url")) { 39 | return (String) dataObj.get("url"); 40 | } 41 | } 42 | } 43 | return null; 44 | } 45 | 46 | @Override 47 | public String getProvider(){ 48 | return Provider.facebook.toString(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /frontend/sample/src/user/login/Login.css: -------------------------------------------------------------------------------- 1 | .login-container { 2 | background-color: rgb(31, 31, 31); 3 | min-height: calc(100vh - 60px); 4 | text-align: center; 5 | } 6 | 7 | .login-content { 8 | background: rgb(25, 25, 25); 9 | box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); 10 | border-radius: 2px; 11 | width: 500px; 12 | display: inline-block; 13 | margin-top: 30px; 14 | vertical-align: middle; 15 | position: relative; 16 | padding: 35px; 17 | 18 | } 19 | 20 | .social-btn { 21 | color: azure; 22 | margin-bottom: 15px; 23 | font-weight: 400; 24 | font-size: 16px; 25 | border-color: rgb(62, 62, 62); 26 | background-color: rgb(82, 82, 82); 27 | } 28 | 29 | 30 | .social-btn:hover { 31 | color:aliceblue; 32 | } 33 | 34 | .social-btn img { 35 | height: 32px; 36 | float: left; 37 | margin-top: 10px; 38 | } 39 | 40 | .social-btn.google { 41 | margin-top: 7px; 42 | } 43 | 44 | .social-btn.facebook img { 45 | height: 24px; 46 | margin-left: 3px; 47 | } 48 | 49 | .social-btn.github img { 50 | height: 24px; 51 | margin-left: 3px; 52 | } 53 | 54 | .social-btn.kakao img { 55 | height: 24px; 56 | margin-left: 3px; 57 | } 58 | 59 | .social-btn.naver img { 60 | height: 24px; 61 | margin-left: 3px; 62 | } 63 | 64 | .signup-link { 65 | color: azure; 66 | font-size: 14px; 67 | } 68 | 69 | .login-title { 70 | font-size: 1.5em; 71 | font-weight: 500; 72 | margin-top: 0; 73 | margin-bottom: 30px; 74 | color: azure; 75 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/service/auth/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.sample.service.auth; 2 | 3 | import java.util.Optional; 4 | 5 | import javax.transaction.Transactional; 6 | 7 | import com.sample.advice.assertThat.DefaultAssert; 8 | import com.sample.config.security.token.UserPrincipal; 9 | import com.sample.domain.entity.user.User; 10 | import com.sample.repository.user.UserRepository; 11 | 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | import org.springframework.security.core.userdetails.UserDetailsService; 14 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 15 | import org.springframework.stereotype.Service; 16 | 17 | import lombok.RequiredArgsConstructor; 18 | 19 | @RequiredArgsConstructor 20 | @Service 21 | public class CustomUserDetailsService implements UserDetailsService{ 22 | 23 | private final UserRepository userRepository; 24 | 25 | @Override 26 | public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { 27 | 28 | User user = userRepository.findByEmail(email) 29 | .orElseThrow(() -> 30 | new UsernameNotFoundException("유저 정보를 찾을 수 없습니다.") 31 | ); 32 | 33 | return UserPrincipal.create(user); 34 | } 35 | 36 | @Transactional 37 | public UserDetails loadUserById(Long id) { 38 | Optional user = userRepository.findById(id); 39 | DefaultAssert.isOptionalPresent(user); 40 | 41 | return UserPrincipal.create(user.get()); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /frontend/sample/src/user/profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Profile.css'; 3 | 4 | class Profile extends Component { 5 | constructor(props) { 6 | super(props); 7 | console.log(props); 8 | } 9 | render() { 10 | return ( 11 |
12 |
13 |
14 |
15 | { 16 | this.props.currentUser.information.imageUrl ? ( 17 | {this.props.currentUser.information.name}/ 18 | ) : ( 19 |
20 | {this.props.currentUser.information.name && this.props.currentUser.information.name[0]} 21 |
22 | ) 23 | } 24 |
25 |
26 |

{this.props.currentUser.information.name}

27 |

{this.props.currentUser.information.email}

28 |
29 |
30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default Profile -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/auth/company/Kakao.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.auth.company; 2 | 3 | import java.util.Map; 4 | 5 | import com.sample.config.security.auth.OAuth2UserInfo; 6 | import com.sample.domain.entity.user.Provider; 7 | 8 | public class Kakao extends OAuth2UserInfo{ 9 | 10 | public Kakao(Map attributes) { 11 | super(attributes); 12 | } 13 | 14 | @Override 15 | public String getId() { 16 | return attributes.get("id").toString(); 17 | } 18 | 19 | @Override 20 | public String getName() { 21 | Map properties = (Map) attributes.get("properties"); 22 | 23 | if (properties == null) { 24 | return null; 25 | } 26 | 27 | return (String) properties.get("nickname"); 28 | } 29 | 30 | @Override 31 | public String getEmail() { 32 | Map properties = (Map) attributes.get("kakao_account"); 33 | if (properties == null) { 34 | return null; 35 | } 36 | return (String) properties.get("email"); 37 | } 38 | 39 | @Override 40 | public String getImageUrl() { 41 | Map properties = (Map) attributes.get("properties"); 42 | 43 | if (properties == null) { 44 | return null; 45 | } 46 | 47 | return (String) properties.get("thumbnail_image"); 48 | } 49 | 50 | @Override 51 | public String getProvider(){ 52 | return Provider.kakao.toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/auth/company/Naver.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.auth.company; 2 | 3 | import java.util.Map; 4 | 5 | import com.sample.config.security.auth.OAuth2UserInfo; 6 | import com.sample.domain.entity.user.Provider; 7 | 8 | public class Naver extends OAuth2UserInfo{ 9 | 10 | public Naver(Map attributes) { 11 | super(attributes); 12 | } 13 | 14 | @Override 15 | public String getId() { 16 | Map response = (Map) attributes.get("response"); 17 | 18 | if (response == null) { 19 | return null; 20 | } 21 | 22 | return (String) response.get("id"); 23 | } 24 | 25 | @Override 26 | public String getName() { 27 | Map response = (Map) attributes.get("response"); 28 | 29 | if (response == null) { 30 | return null; 31 | } 32 | 33 | return (String) response.get("nickname"); 34 | } 35 | 36 | @Override 37 | public String getEmail() { 38 | Map response = (Map) attributes.get("response"); 39 | 40 | if (response == null) { 41 | return null; 42 | } 43 | 44 | return (String) response.get("email"); 45 | } 46 | 47 | @Override 48 | public String getImageUrl() { 49 | Map response = (Map) attributes.get("response"); 50 | 51 | if (response == null) { 52 | return null; 53 | } 54 | 55 | return (String) response.get("profile_image"); 56 | } 57 | 58 | @Override 59 | public String getProvider(){ 60 | return Provider.naver.toString(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /frontend/sample/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /backend/sample/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.6.7' 3 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'com' 8 | version = '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '11' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 17 | implementation 'org.springframework.boot:spring-boot-starter-security' 18 | implementation 'org.springframework.boot:spring-boot-starter-validation' 19 | implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' 20 | implementation 'org.springframework.boot:spring-boot-starter-web' 21 | 22 | implementation 'org.apache.poi:poi:5.2.0' 23 | 24 | implementation 'com.googlecode.json-simple:json-simple:1.1.1' 25 | 26 | implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.4' 27 | implementation group: 'org.springdoc', name: 'springdoc-openapi-security', version: '1.6.4' 28 | testImplementation group: 'org.springdoc', name: 'springdoc-openapi-webmvc-core', version: '1.6.4' 29 | 30 | implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' 31 | runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' 32 | runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' 33 | 34 | compileOnly 'org.projectlombok:lombok' 35 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 36 | runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' 37 | annotationProcessor 'org.projectlombok:lombok' 38 | 39 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 40 | testImplementation 'org.springframework.security:spring-security-test' 41 | } 42 | 43 | tasks.named('test') { 44 | useJUnitPlatform() 45 | } 46 | -------------------------------------------------------------------------------- /frontend/sample/src/common/AppHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, NavLink } from 'react-router-dom'; 3 | import './AppHeader.css'; 4 | 5 | class AppHeader extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 |
11 | Sample OAuth2 & JWT 12 |
13 |
14 | 35 |
36 |
37 |
38 | ) 39 | } 40 | } 41 | 42 | export default AppHeader; -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.handler; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.util.UriComponentsBuilder; 7 | 8 | import lombok.RequiredArgsConstructor; 9 | 10 | import static com.sample.repository.auth.CustomAuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; 11 | 12 | import java.io.IOException; 13 | 14 | import javax.servlet.ServletException; 15 | import javax.servlet.http.Cookie; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | 19 | import com.sample.config.security.util.CustomCookie; 20 | import com.sample.repository.auth.CustomAuthorizationRequestRepository; 21 | 22 | @RequiredArgsConstructor 23 | @Component 24 | public class CustomSimpleUrlAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler{ 25 | private final CustomAuthorizationRequestRepository customAuthorizationRequestRepository; 26 | 27 | @Override 28 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { 29 | String targetUrl = CustomCookie.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) 30 | .map(Cookie::getValue) 31 | .orElse(("/")); 32 | 33 | targetUrl = UriComponentsBuilder.fromUriString(targetUrl) 34 | .queryParam("error", exception.getLocalizedMessage()) 35 | .build().toUriString(); 36 | 37 | customAuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); 38 | 39 | getRedirectStrategy().sendRedirect(request, response, targetUrl); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/docs/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.docs; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import io.swagger.v3.oas.models.Components; 8 | import io.swagger.v3.oas.models.OpenAPI; 9 | import io.swagger.v3.oas.models.info.Contact; 10 | import io.swagger.v3.oas.models.info.Info; 11 | import io.swagger.v3.oas.models.info.License; 12 | import io.swagger.v3.oas.models.security.SecurityRequirement; 13 | import io.swagger.v3.oas.models.security.SecurityScheme; 14 | 15 | @Configuration 16 | public class OpenApiConfig { 17 | 18 | private final String securitySchemeName = "bearerAuth"; 19 | 20 | @Bean 21 | public OpenAPI openAPI(@Value("OpenAPI") String appVersion) { 22 | Info info = new Info().title("Demo API").version(appVersion) 23 | .description("Spring Boot를 이용한 Demo 웹 애플리케이션 API입니다.") 24 | .termsOfService("http://swagger.io/terms/") 25 | .contact(new Contact().name("name").url("https://name.name.name/").email("name@name.name")) 26 | .license(new License().name("Apache License Version 2.0") 27 | .url("http://www.apache.org/licenses/LICENSE-2.0")); 28 | 29 | return new OpenAPI() 30 | .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) 31 | .components( 32 | new Components() 33 | .addSecuritySchemes(securitySchemeName, 34 | new SecurityScheme() 35 | .name(securitySchemeName) 36 | .type(SecurityScheme.Type.HTTP) 37 | .scheme("bearer") 38 | .bearerFormat("JWT") 39 | ) 40 | ) 41 | .info(info); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/domain/entity/user/User.java: -------------------------------------------------------------------------------- 1 | package com.sample.domain.entity.user; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.EnumType; 6 | import javax.persistence.Enumerated; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import javax.validation.constraints.Email; 12 | import javax.validation.constraints.NotNull; 13 | 14 | import com.fasterxml.jackson.annotation.JsonIgnore; 15 | import com.sample.domain.entity.time.DefaultTime; 16 | 17 | import org.hibernate.annotations.DynamicUpdate; 18 | 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | 22 | @DynamicUpdate 23 | @Entity 24 | @Getter 25 | @Table(name = "user") 26 | public class User extends DefaultTime{ 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | private Long id; 30 | 31 | @Column(nullable = false) 32 | private String name; 33 | 34 | @Email 35 | @Column(nullable = false) 36 | private String email; 37 | 38 | private String imageUrl; 39 | 40 | @Column(nullable = false) 41 | private Boolean emailVerified = false; 42 | 43 | @JsonIgnore 44 | private String password; 45 | 46 | @NotNull 47 | @Enumerated(EnumType.STRING) 48 | private Provider provider; 49 | 50 | @Enumerated(EnumType.STRING) 51 | private Role role; 52 | 53 | private String providerId; 54 | 55 | public User(){} 56 | 57 | @Builder 58 | public User(String name, String email, String password, Role role, Provider provider, String providerId, String imageUrl){ 59 | this.email = email; 60 | this.password = password; 61 | this.name = name; 62 | this.provider = provider; 63 | this.role = role; 64 | } 65 | 66 | public void updateName(String name){ 67 | this.name = name; 68 | } 69 | 70 | public void updateImageUrl(String imageUrl){ 71 | this.imageUrl = imageUrl; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/sample/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/util/CustomCookie.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.util; 2 | 3 | import java.util.Base64; 4 | import java.util.Optional; 5 | 6 | import javax.servlet.http.Cookie; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | import org.springframework.util.SerializationUtils; 11 | 12 | public class CustomCookie { 13 | 14 | public static Optional getCookie(HttpServletRequest request, String name){ 15 | Cookie[] cookies = request.getCookies(); 16 | 17 | if (cookies != null && cookies.length > 0) { 18 | for (Cookie cookie : cookies) { 19 | if (cookie.getName().equals(name)) { 20 | return Optional.of(cookie); 21 | } 22 | } 23 | } 24 | 25 | return Optional.empty(); 26 | } 27 | 28 | public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { 29 | Cookie cookie = new Cookie(name, value); 30 | 31 | cookie.setPath("/"); 32 | cookie.setHttpOnly(true); 33 | cookie.setMaxAge(maxAge); 34 | response.addCookie(cookie); 35 | } 36 | 37 | public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { 38 | Cookie[] cookies = request.getCookies(); 39 | if (cookies != null && cookies.length > 0) { 40 | for (Cookie cookie: cookies) { 41 | if (cookie.getName().equals(name)) { 42 | cookie.setValue(""); 43 | cookie.setPath("/"); 44 | cookie.setMaxAge(0); 45 | response.addCookie(cookie); 46 | } 47 | } 48 | } 49 | } 50 | 51 | 52 | public static String serialize(Object object) { 53 | return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object)); 54 | } 55 | 56 | public static T deserialize(Cookie cookie, Class cls) { 57 | return cls.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue()))); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/token/CustomOncePerRequestFilter.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.token; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.FilterChain; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | import com.sample.service.auth.CustomTokenProviderService; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 14 | import org.springframework.security.core.context.SecurityContextHolder; 15 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 16 | import org.springframework.util.StringUtils; 17 | import org.springframework.web.filter.OncePerRequestFilter; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | @Slf4j 22 | public class CustomOncePerRequestFilter extends OncePerRequestFilter{ 23 | 24 | @Autowired 25 | private CustomTokenProviderService customTokenProviderService; 26 | 27 | @Override 28 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 29 | String jwt = getJwtFromRequest(request); 30 | 31 | if (StringUtils.hasText(jwt) && customTokenProviderService.validateToken(jwt)) { 32 | UsernamePasswordAuthenticationToken authentication = customTokenProviderService.getAuthenticationById(jwt); 33 | authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 34 | SecurityContextHolder.getContext().setAuthentication(authentication); 35 | } 36 | 37 | filterChain.doFilter(request, response); 38 | } 39 | 40 | private String getJwtFromRequest(HttpServletRequest request) { 41 | String bearerToken = request.getHeader("Authorization"); 42 | if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { 43 | log.info("bearerToken = {}", bearerToken.substring(7, bearerToken.length())); 44 | return bearerToken.substring(7, bearerToken.length()); 45 | } 46 | return null; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/assertThat/DefaultAssert.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice.assertThat; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import com.sample.advice.error.DefaultAuthenticationException; 7 | import com.sample.advice.error.DefaultException; 8 | import com.sample.advice.error.DefaultNullPointerException; 9 | import com.sample.advice.error.InvalidParameterException; 10 | import com.sample.advice.payload.ErrorCode; 11 | 12 | import org.springframework.util.Assert; 13 | import org.springframework.validation.Errors; 14 | 15 | public class DefaultAssert extends Assert{ 16 | 17 | public static void isTrue(boolean value){ 18 | if(!value){ 19 | throw new DefaultException(ErrorCode.INVALID_CHECK); 20 | } 21 | } 22 | 23 | public static void isTrue(boolean value, String message){ 24 | if(!value){ 25 | throw new DefaultException(ErrorCode.INVALID_CHECK, message); 26 | } 27 | } 28 | 29 | public static void isValidParameter(Errors errors){ 30 | if(errors.hasErrors()){ 31 | throw new InvalidParameterException(errors); 32 | } 33 | } 34 | 35 | public static void isObjectNull(Object object){ 36 | if(object == null){ 37 | throw new DefaultNullPointerException(ErrorCode.INVALID_CHECK); 38 | } 39 | } 40 | 41 | public static void isListNull(List values){ 42 | if(values.isEmpty()){ 43 | throw new DefaultException(ErrorCode.INVALID_FILE_PATH); 44 | } 45 | } 46 | 47 | public static void isListNull(Object[] values){ 48 | if(values == null){ 49 | throw new DefaultException(ErrorCode.INVALID_FILE_PATH); 50 | } 51 | } 52 | 53 | public static void isOptionalPresent(Optional value){ 54 | if(!value.isPresent()){ 55 | throw new DefaultException(ErrorCode.INVALID_PARAMETER); 56 | } 57 | } 58 | 59 | public static void isAuthentication(String message){ 60 | throw new DefaultAuthenticationException(message); 61 | } 62 | 63 | public static void isAuthentication(boolean value){ 64 | if(!value){ 65 | throw new DefaultAuthenticationException(ErrorCode.INVALID_AUTHENTICATION); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/payload/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice.payload; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import com.fasterxml.jackson.annotation.JsonInclude; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | 10 | import org.springframework.validation.FieldError; 11 | 12 | import lombok.Builder; 13 | import lombok.Data; 14 | 15 | @Data 16 | public class ErrorResponse { 17 | private LocalDateTime timestamp = LocalDateTime.now(); 18 | 19 | private String message; 20 | 21 | private String code; 22 | 23 | @JsonProperty("class") 24 | private String clazz; 25 | 26 | private int status; 27 | 28 | @JsonInclude(JsonInclude.Include.NON_NULL) 29 | @JsonProperty("errors") 30 | private List customFieldErrors = new ArrayList<>(); 31 | 32 | public ErrorResponse() {} 33 | 34 | @Builder 35 | public ErrorResponse(String code, int status, String message, String clazz, List fieldErrors){ 36 | this.code = code; 37 | this.status = status; 38 | this.message = message; 39 | this.clazz = clazz; 40 | //setFieldErrors(fieldErrors); 41 | } 42 | 43 | public void setFieldErrors(List fieldErrors) { 44 | if(fieldErrors != null){ 45 | fieldErrors.forEach(error -> { 46 | customFieldErrors.add(new CustomFieldError( 47 | error.getField(), 48 | error.getRejectedValue(), 49 | error.getDefaultMessage() 50 | )); 51 | }); 52 | } 53 | } 54 | 55 | public static class CustomFieldError { 56 | 57 | private String field; 58 | private Object value; 59 | private String reason; 60 | 61 | public CustomFieldError(String field, Object value, String reason) { 62 | this.field = field; 63 | this.value = value; 64 | this.reason = reason; 65 | } 66 | 67 | public String getField() { 68 | return field; 69 | } 70 | 71 | public Object getValue() { 72 | return value; 73 | } 74 | 75 | public String getReason() { 76 | return reason; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /frontend/sample/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/repository/auth/CustomAuthorizationRequestRepository.java: -------------------------------------------------------------------------------- 1 | package com.sample.repository.auth; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import com.sample.config.security.util.CustomCookie; 7 | import com.nimbusds.oauth2.sdk.util.StringUtils; 8 | 9 | import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; 10 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; 11 | import org.springframework.stereotype.Repository; 12 | 13 | @Repository 14 | public class CustomAuthorizationRequestRepository implements AuthorizationRequestRepository{ 15 | public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; 16 | public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; 17 | 18 | private static final int cookieExpireSeconds = 60*60; 19 | 20 | @Override 21 | public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { 22 | return CustomCookie.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) 23 | .map(cookie -> CustomCookie.deserialize(cookie, OAuth2AuthorizationRequest.class)) 24 | .orElse(null); 25 | } 26 | 27 | @Override 28 | public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { 29 | if (authorizationRequest == null) { 30 | CustomCookie.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); 31 | CustomCookie.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); 32 | return; 33 | } 34 | 35 | CustomCookie.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CustomCookie.serialize(authorizationRequest), cookieExpireSeconds); 36 | String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME); 37 | if (StringUtils.isNotBlank(redirectUriAfterLogin)) { 38 | CustomCookie.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds); 39 | } 40 | } 41 | 42 | @Override 43 | public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { 44 | return this.loadAuthorizationRequest(request); 45 | } 46 | 47 | public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) { 48 | CustomCookie.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); 49 | CustomCookie.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /backend/sample/src/main/resources/oauth2/oauth2-sample.properties: -------------------------------------------------------------------------------- 1 | 2 | #security 3 | spring.security.oauth2.client.registration.google.client-id={"client-id"} 4 | spring.security.oauth2.client.registration.google.client-secret={"client-secret"} 5 | spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/oauth2/callback/google 6 | spring.security.oauth2.client.registration.google.scope=profile,email 7 | 8 | spring.security.oauth2.client.registration.naver.client-id={"client-id"} 9 | spring.security.oauth2.client.registration.naver.client-secret={"client-secret"} 10 | spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/oauth2/callback/naver 11 | spring.security.oauth2.client.registration.naver.client-authentication-method=post 12 | spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code 13 | spring.security.oauth2.client.registration.naver.scope=nickname,email,profile_image 14 | spring.security.oauth2.client.registration.naver.client-name=Naver 15 | 16 | spring.security.oauth2.client.registration.kakao.client-id={"client-id"} 17 | spring.security.oauth2.client.registration.kakao.client-secret={"client-secret"} 18 | spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/oauth2/callback/kakao 19 | spring.security.oauth2.client.registration.kakao.client-authentication-method=post 20 | spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code 21 | spring.security.oauth2.client.registration.kakao.scope=profile_nickname,profile_image,account_email 22 | spring.security.oauth2.client.registration.kakao.client-name=Kakao 23 | 24 | spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize 25 | spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token 26 | spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me 27 | spring.security.oauth2.client.provider.naver.user-name-attribute=response 28 | 29 | spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize 30 | spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token 31 | spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me 32 | spring.security.oauth2.client.provider.kakao.user-name-attribute=id 33 | 34 | #app 35 | app.auth.tokenSecret={"tokenSecret"} 36 | app.auth.accessTokenExpirationMsec=1800000 37 | app.auth.refreshTokenExpirationMsec=604800000 38 | 39 | app.cors.allowedOrigins=http://localhost:3000, http://localhost:8080 40 | 41 | app.oauth2.authorizedRedirectUris=http://localhost:3000/oauth2/redirect, myandroidapp://oauth2/redirect, myiosapp://oauth2/redirect 42 | -------------------------------------------------------------------------------- /backend/sample/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/token/UserPrincipal.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.token; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import com.sample.domain.entity.user.User; 9 | 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | import org.springframework.security.oauth2.core.user.OAuth2User; 14 | 15 | public class UserPrincipal implements OAuth2User, UserDetails{ 16 | 17 | private Long id; 18 | private String email; 19 | private String password; 20 | private Collection authorities; 21 | private Map attributes; 22 | 23 | public UserPrincipal(Long id, String email, String password, Collection authorities) { 24 | this.id = id; 25 | this.email = email; 26 | this.password = password; 27 | this.authorities = authorities; 28 | } 29 | 30 | public static UserPrincipal create(User user) { 31 | List authorities = Collections.singletonList(new SimpleGrantedAuthority(user.getRole().getValue())); 32 | return new UserPrincipal( 33 | user.getId(), 34 | user.getEmail(), 35 | user.getPassword(), 36 | authorities 37 | ); 38 | } 39 | 40 | public static UserPrincipal create(User user, Map attributes) { 41 | UserPrincipal userPrincipal = UserPrincipal.create(user); 42 | userPrincipal.setAttributes(attributes); 43 | return userPrincipal; 44 | } 45 | 46 | public void setAttributes(Map attributes) { 47 | this.attributes = attributes; 48 | } 49 | 50 | public Long getId() { 51 | return id; 52 | } 53 | 54 | public String getEmail() { 55 | return email; 56 | } 57 | 58 | @Override 59 | public Map getAttributes() { 60 | return attributes; 61 | } 62 | 63 | @Override 64 | public Collection getAuthorities() { 65 | return authorities; 66 | } 67 | 68 | @Override 69 | public String getName() { 70 | return String.valueOf(id); 71 | } 72 | 73 | @Override 74 | public String getPassword() { 75 | return password; 76 | } 77 | 78 | @Override 79 | public String getUsername() { 80 | return email; 81 | } 82 | 83 | @Override 84 | public boolean isAccountNonExpired() { 85 | return true; 86 | } 87 | 88 | @Override 89 | public boolean isAccountNonLocked() { 90 | return true; 91 | } 92 | 93 | @Override 94 | public boolean isCredentialsNonExpired() { 95 | return true; 96 | } 97 | 98 | @Override 99 | public boolean isEnabled() { 100 | return true; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /frontend/sample/src/app/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Route, 4 | Switch 5 | } from 'react-router-dom'; 6 | import AppHeader from '../common/AppHeader'; 7 | import Home from '../home/Home'; 8 | import Login from '../user/login/Login'; 9 | import Signup from '../user/signup/Signup'; 10 | import Profile from '../user/profile/Profile'; 11 | import OAuth2RedirectHandler from '../user/oauth2/OAuth2RedirectHandler'; 12 | import NotFound from '../common/NotFound'; 13 | import LoadingIndicator from '../common/LoadingIndicator'; 14 | import { getCurrentUser } from '../util/APIUtils'; 15 | import { ACCESS_TOKEN, REFRESH_TOKEN } from '../constants'; 16 | import PrivateRoute from '../common/PrivateRoute'; 17 | import Alert from 'react-s-alert'; 18 | import 'react-s-alert/dist/s-alert-default.css'; 19 | import 'react-s-alert/dist/s-alert-css-effects/slide.css'; 20 | import './App.css'; 21 | 22 | class App extends Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | authenticated: false, 27 | currentUser: null, 28 | loading: true 29 | } 30 | 31 | this.loadCurrentlyLoggedInUser = this.loadCurrentlyLoggedInUser.bind(this); 32 | this.handleLogout = this.handleLogout.bind(this); 33 | } 34 | 35 | loadCurrentlyLoggedInUser() { 36 | getCurrentUser() 37 | .then(response => { 38 | this.setState({ 39 | currentUser: response, 40 | authenticated: true, 41 | loading: false 42 | }); 43 | }).catch(error => { 44 | this.setState({ 45 | loading: false 46 | }); 47 | }); 48 | } 49 | 50 | handleLogout() { 51 | localStorage.removeItem(ACCESS_TOKEN); 52 | localStorage.removeItem(REFRESH_TOKEN); 53 | this.setState({ 54 | authenticated: false, 55 | currentUser: null 56 | }); 57 | Alert.success("로그아웃 했습니다."); 58 | } 59 | 60 | componentDidMount() { 61 | this.loadCurrentlyLoggedInUser(); 62 | } 63 | 64 | render() { 65 | if(this.state.loading) { 66 | return 67 | } 68 | 69 | return ( 70 |
71 |
72 | 73 |
74 |
75 | 76 | 77 | 79 | }> 81 | }> 83 | 84 | 85 | 86 |
87 | 90 |
91 | ); 92 | } 93 | } 94 | 95 | export default App; 96 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/service/auth/CustomDefaultOAuth2UserService.java: -------------------------------------------------------------------------------- 1 | package com.sample.service.auth; 2 | 3 | import java.util.Optional; 4 | 5 | import com.sample.advice.assertThat.DefaultAssert; 6 | import com.sample.config.security.auth.OAuth2UserInfo; 7 | import com.sample.config.security.auth.OAuth2UserInfoFactory; 8 | import com.sample.config.security.token.UserPrincipal; 9 | import com.sample.domain.entity.user.Provider; 10 | import com.sample.domain.entity.user.Role; 11 | import com.sample.domain.entity.user.User; 12 | import com.sample.repository.user.UserRepository; 13 | 14 | import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; 15 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 16 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 17 | import org.springframework.security.oauth2.core.user.OAuth2User; 18 | import org.springframework.stereotype.Service; 19 | 20 | import lombok.RequiredArgsConstructor; 21 | 22 | @RequiredArgsConstructor 23 | @Service 24 | public class CustomDefaultOAuth2UserService extends DefaultOAuth2UserService{ 25 | 26 | private final UserRepository userRepository; 27 | 28 | @Override 29 | public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { 30 | OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest); 31 | try { 32 | return processOAuth2User(oAuth2UserRequest, oAuth2User); 33 | } catch (Exception e) { 34 | DefaultAssert.isAuthentication(e.getMessage()); 35 | } 36 | return null; 37 | } 38 | 39 | private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) { 40 | OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes()); 41 | DefaultAssert.isAuthentication(!oAuth2UserInfo.getEmail().isEmpty()); 42 | 43 | Optional userOptional = userRepository.findByEmail(oAuth2UserInfo.getEmail()); 44 | User user; 45 | if(userOptional.isPresent()) { 46 | user = userOptional.get(); 47 | DefaultAssert.isAuthentication(user.getProvider().equals(Provider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))); 48 | user = updateExistingUser(user, oAuth2UserInfo); 49 | } else { 50 | user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo); 51 | } 52 | 53 | return UserPrincipal.create(user, oAuth2User.getAttributes()); 54 | } 55 | 56 | private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) { 57 | User user = User.builder() 58 | .provider(Provider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId())) 59 | .providerId(oAuth2UserInfo.getId()) 60 | .name(oAuth2UserInfo.getName()) 61 | .email(oAuth2UserInfo.getEmail()) 62 | .imageUrl(oAuth2UserInfo.getImageUrl()) 63 | .role(Role.USER) 64 | .build(); 65 | 66 | return userRepository.save(user); 67 | } 68 | 69 | private User updateExistingUser(User user, OAuth2UserInfo oAuth2UserInfo) { 70 | 71 | user.updateName(oAuth2UserInfo.getName()); 72 | user.updateImageUrl(oAuth2UserInfo.getImageUrl()); 73 | 74 | return userRepository.save(user); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/handler/CustomSimpleUrlAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security.handler; 2 | 3 | import com.sample.advice.assertThat.DefaultAssert; 4 | import com.sample.config.security.OAuth2Config; 5 | import com.sample.config.security.util.CustomCookie; 6 | import com.sample.domain.entity.user.Token; 7 | import com.sample.domain.mapping.TokenMapping; 8 | import com.sample.repository.auth.CustomAuthorizationRequestRepository; 9 | import com.sample.repository.auth.TokenRepository; 10 | import com.sample.service.auth.CustomTokenProviderService; 11 | 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.web.util.UriComponentsBuilder; 16 | 17 | import lombok.RequiredArgsConstructor; 18 | 19 | import static com.sample.repository.auth.CustomAuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; 20 | 21 | import java.io.IOException; 22 | import java.net.URI; 23 | import java.util.Optional; 24 | 25 | import javax.servlet.ServletException; 26 | import javax.servlet.http.Cookie; 27 | import javax.servlet.http.HttpServletRequest; 28 | import javax.servlet.http.HttpServletResponse; 29 | 30 | @RequiredArgsConstructor 31 | @Component 32 | public class CustomSimpleUrlAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler{ 33 | 34 | private final CustomTokenProviderService customTokenProviderService; 35 | private final OAuth2Config oAuth2Config; 36 | private final TokenRepository tokenRepository; 37 | private final CustomAuthorizationRequestRepository customAuthorizationRequestRepository; 38 | 39 | @Override 40 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 41 | DefaultAssert.isAuthentication(!response.isCommitted()); 42 | 43 | String targetUrl = determineTargetUrl(request, response, authentication); 44 | 45 | clearAuthenticationAttributes(request, response); 46 | getRedirectStrategy().sendRedirect(request, response, targetUrl); 47 | } 48 | 49 | protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { 50 | Optional redirectUri = CustomCookie.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue); 51 | 52 | DefaultAssert.isAuthentication( !(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) ); 53 | 54 | String targetUrl = redirectUri.orElse(getDefaultTargetUrl()); 55 | 56 | TokenMapping tokenMapping = customTokenProviderService.createToken(authentication); 57 | Token token = Token.builder() 58 | .userEmail(tokenMapping.getUserEmail()) 59 | .refreshToken(tokenMapping.getRefreshToken()) 60 | .build(); 61 | tokenRepository.save(token); 62 | 63 | return UriComponentsBuilder.fromUriString(targetUrl) 64 | .queryParam("token", tokenMapping.getAccessToken()) 65 | .build().toUriString(); 66 | } 67 | 68 | protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) { 69 | super.clearAuthenticationAttributes(request); 70 | customAuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); 71 | } 72 | 73 | private boolean isAuthorizedRedirectUri(String uri) { 74 | URI clientRedirectUri = URI.create(uri); 75 | 76 | return oAuth2Config.getOauth2().getAuthorizedRedirectUris() 77 | .stream() 78 | .anyMatch(authorizedRedirectUri -> { 79 | URI authorizedURI = URI.create(authorizedRedirectUri); 80 | if(authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) 81 | && authorizedURI.getPort() == clientRedirectUri.getPort()) { 82 | return true; 83 | } 84 | return false; 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /frontend/sample/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /frontend/sample/src/user/signup/Signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Signup.css'; 3 | import { Link, Redirect } from 'react-router-dom' 4 | import { NAVER_AUTH_URL ,KAKAO_AUTH_URL, GOOGLE_AUTH_URL, FACEBOOK_AUTH_URL, GITHUB_AUTH_URL } from '../../constants'; 5 | import { signup } from '../../util/APIUtils'; 6 | import fbLogo from '../../img/fb-logo.png'; 7 | import googleLogo from '../../img/google-logo.png'; 8 | import githubLogo from '../../img/github-logo.png'; 9 | import kakaoLogo from '../../img/kakao-logo.png'; 10 | import naverLogo from '../../img/naver-logo.png'; 11 | 12 | import Alert from 'react-s-alert'; 13 | 14 | class Signup extends Component { 15 | render() { 16 | if(this.props.authenticated) { 17 | return ; 22 | } 23 | 24 | return ( 25 |
26 |
27 |

Sign Up

28 | 29 |
30 | OR 31 |
32 | 33 | Already have an account? Login! 34 |
35 |
36 | ); 37 | } 38 | } 39 | 40 | 41 | class SocialSignup extends Component { 42 | render() { 43 | return ( 44 | 57 | ); 58 | } 59 | } 60 | 61 | class SignupForm extends Component { 62 | constructor(props) { 63 | super(props); 64 | this.state = { 65 | name: '', 66 | email: '', 67 | password: '' 68 | } 69 | this.handleInputChange = this.handleInputChange.bind(this); 70 | this.handleSubmit = this.handleSubmit.bind(this); 71 | } 72 | 73 | handleInputChange(event) { 74 | const target = event.target; 75 | const inputName = target.name; 76 | const inputValue = target.value; 77 | 78 | this.setState({ 79 | [inputName] : inputValue 80 | }); 81 | } 82 | 83 | handleSubmit(event) { 84 | event.preventDefault(); 85 | 86 | const signUpRequest = Object.assign({}, this.state); 87 | 88 | signup(signUpRequest) 89 | .then(response => { 90 | Alert.success("회원가입에 성공하셨습니다."); 91 | this.props.history.push("/login"); 92 | }).catch(error => { 93 | Alert.error((error && error.message) || '예기치 않은 문제가 발생하였습니다.'); 94 | }); 95 | } 96 | 97 | render() { 98 | return ( 99 |
100 |
101 | 104 |
105 |
106 | 109 |
110 |
111 | 114 |
115 |
116 | 117 |
118 |
119 | 120 | ); 121 | } 122 | } 123 | 124 | export default Signup -------------------------------------------------------------------------------- /frontend/sample/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | *:before, 8 | *:after { 9 | -webkit-box-sizing: border-box; 10 | -moz-box-sizing: border-box; 11 | box-sizing: border-box; 12 | } 13 | 14 | input[type="checkbox"], 15 | input[type="radio"] { 16 | -webkit-box-sizing: border-box; 17 | -moz-box-sizing: border-box; 18 | box-sizing: border-box; 19 | padding: 0; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | padding: 0; 25 | font-size: 16px; 26 | font-weight: 400; 27 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 28 | color: rgba(0,0,0,.80); 29 | } 30 | 31 | .container { 32 | width: 100%; 33 | max-width: 1300px; 34 | margin-left: auto; 35 | margin-right: auto; 36 | padding-left: 15px; 37 | padding-right: 15px; 38 | } 39 | 40 | @media screen and (min-width: 1200px) { 41 | .container { 42 | padding-left: 30px; 43 | padding-right: 30px; 44 | } 45 | } 46 | 47 | /** 48 | Link 49 | **/ 50 | 51 | a { 52 | color: #2098f3; 53 | text-decoration: none; 54 | word-wrap: break-word; 55 | cursor: pointer; 56 | } 57 | 58 | a:hover { 59 | color: #40a9ff; 60 | text-decoration: none; 61 | } 62 | 63 | /** 64 | Button 65 | **/ 66 | 67 | 68 | .btn { 69 | display: inline-block; 70 | font-weight: 400; 71 | text-align: center; 72 | -ms-touch-action: manipulation; 73 | touch-action: manipulation; 74 | cursor: pointer; 75 | background-image: none; 76 | border: 1px solid transparent; 77 | white-space: nowrap; 78 | padding: 0 20px; 79 | font-size: 14px; 80 | border-radius: 4px; 81 | height: 45px; 82 | line-height: 45px; 83 | -webkit-user-select: none; 84 | -moz-user-select: none; 85 | -ms-user-select: none; 86 | user-select: none; 87 | -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1); 88 | transition: all .3s cubic-bezier(.645,.045,.355,1); 89 | position: relative; 90 | color: rgba(0,0,0,.65); 91 | background-color: transparent; 92 | border-color: #e8e8e8; 93 | outline: none; 94 | } 95 | 96 | .btn-block { 97 | display: block; 98 | width: 100%; 99 | } 100 | 101 | .btn-lg { 102 | padding: 0 30px; 103 | } 104 | 105 | .btn-primary { 106 | color: #fff; 107 | background-color: rgb(62, 62, 62); 108 | border-color: rgb(62, 62, 62); 109 | } 110 | 111 | .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active { 112 | background-color: rgb(42, 42, 42); 113 | border-color: rgb(42, 42, 42); 114 | } 115 | 116 | .btn-success { 117 | color: #fff; 118 | background-color: #52c41a; 119 | border-color: #52c41a; 120 | } 121 | 122 | .btn-success:hover, .btn-success:focus, .btn-success:active, .btn-success.active { 123 | background-color: #52c41a; 124 | border-color: #52c41a; 125 | } 126 | 127 | 128 | .btn-link { 129 | border: none; 130 | height: 34px; 131 | padding: 0 15px; 132 | } 133 | 134 | .btn-link:hover { 135 | background-color: rgba(158,158,158, 0.20); 136 | } 137 | 138 | 139 | /** 140 | Form 141 | **/ 142 | 143 | .form-item { 144 | 145 | margin-bottom: 18px; 146 | } 147 | 148 | .form-item .btn { 149 | cursor: pointer; 150 | } 151 | 152 | .form-item label { 153 | font-size: 0.85em; 154 | font-weight: 500; 155 | display: inline-block; 156 | margin-bottom: 5px; 157 | color: rgba(0,0,0,.65); 158 | } 159 | 160 | .form-control { 161 | margin: 0; 162 | padding: 0; 163 | list-style: none; 164 | position: relative; 165 | display: inline-block; 166 | padding: 4px 11px; 167 | width: 100%; 168 | height: 45px; 169 | font-size: 0.87em; 170 | line-height: 45px; 171 | color:azure; 172 | background-color: rgb(82, 82, 82); 173 | background-image: none; 174 | border: 1px solid rgb(62, 62, 62); 175 | border-radius: 4px; 176 | transition: all .3s; 177 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075); 178 | } 179 | 180 | 181 | .form-control:hover, .form-control:focus, .form-control:active { 182 | border-color: rgb(82, 82, 82); 183 | outline: 0; 184 | border-right-width: 1px!important; 185 | } 186 | 187 | .form-control:focus, .form-control:active { 188 | box-shadow: 0 0 0 2px rgba(24,144,255,.2); 189 | } 190 | 191 | .form-control.invalid { 192 | border-color: #f5222d; 193 | } 194 | 195 | .form-control.invalid:focus, .form-control.invalid:active { 196 | box-shadow: 0 0 0 2px rgba(245,34,45,.2); 197 | } 198 | 199 | .form-control[disabled], fieldset[disabled] .form-control { 200 | cursor: not-allowed; 201 | } 202 | 203 | .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { 204 | background-color: #f6f8fa; 205 | opacity: 1; 206 | } 207 | 208 | .form-label { 209 | margin-bottom: 10px; 210 | } 211 | 212 | /** 213 | Or separator 214 | **/ 215 | 216 | .or-separator { 217 | border-bottom: 1px solid rgb(82, 82, 82); 218 | padding: 10px 0; 219 | position: relative; 220 | display: block; 221 | margin-top: 20px; 222 | margin-bottom: 30px; 223 | font-size: 1em; 224 | } 225 | 226 | .or-text { 227 | position: absolute; 228 | left: 46%; 229 | top: 0; 230 | background: rgb(25, 25, 25); 231 | padding: 10px; 232 | color:azure; 233 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/config/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.sample.config.security; 2 | 3 | import com.sample.config.security.handler.CustomSimpleUrlAuthenticationFailureHandler; 4 | import com.sample.config.security.handler.CustomSimpleUrlAuthenticationSuccessHandler; 5 | import com.sample.config.security.token.CustomAuthenticationEntryPoint; 6 | import com.sample.config.security.token.CustomOncePerRequestFilter; 7 | import com.sample.repository.auth.CustomAuthorizationRequestRepository; 8 | import com.sample.service.auth.CustomDefaultOAuth2UserService; 9 | import com.sample.service.auth.CustomUserDetailsService; 10 | 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.security.authentication.AuthenticationManager; 14 | import org.springframework.security.config.BeanIds; 15 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 16 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 17 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 18 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 19 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 20 | import org.springframework.security.config.http.SessionCreationPolicy; 21 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 22 | import org.springframework.security.crypto.password.PasswordEncoder; 23 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 24 | 25 | import lombok.RequiredArgsConstructor; 26 | 27 | 28 | @RequiredArgsConstructor 29 | @Configuration 30 | @EnableWebSecurity 31 | @EnableGlobalMethodSecurity( 32 | securedEnabled = true, 33 | jsr250Enabled = true, 34 | prePostEnabled = true 35 | ) 36 | public class SecurityConfig extends WebSecurityConfigurerAdapter{ 37 | 38 | private final CustomUserDetailsService customUserDetailsService; 39 | private final CustomDefaultOAuth2UserService customOAuth2UserService; 40 | private final CustomSimpleUrlAuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; 41 | private final CustomSimpleUrlAuthenticationFailureHandler oAuth2AuthenticationFailureHandler; 42 | private final CustomAuthorizationRequestRepository customAuthorizationRequestRepository; 43 | 44 | @Bean 45 | public CustomOncePerRequestFilter customOncePerRequestFilter() { 46 | return new CustomOncePerRequestFilter(); 47 | } 48 | 49 | @Override 50 | public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { 51 | authenticationManagerBuilder 52 | .userDetailsService(customUserDetailsService) 53 | .passwordEncoder(passwordEncoder()); 54 | } 55 | 56 | @Bean 57 | public PasswordEncoder passwordEncoder() { 58 | return new BCryptPasswordEncoder(); 59 | } 60 | 61 | @Bean(BeanIds.AUTHENTICATION_MANAGER) 62 | @Override 63 | public AuthenticationManager authenticationManagerBean() throws Exception { 64 | return super.authenticationManagerBean(); 65 | } 66 | 67 | @Override 68 | protected void configure(HttpSecurity http) throws Exception { 69 | http 70 | .cors() 71 | .and() 72 | .sessionManagement() 73 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 74 | .and() 75 | .csrf() 76 | .disable() 77 | .formLogin() 78 | .disable() 79 | .httpBasic() 80 | .disable() 81 | .exceptionHandling() 82 | .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) 83 | .and() 84 | .authorizeRequests() 85 | .antMatchers("/", "/error","/favicon.ico", "/**/*.png", "/**/*.gif", "/**/*.svg", "/**/*.jpg", "/**/*.html", "/**/*.css", "/**/*.js") 86 | .permitAll() 87 | .antMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**") 88 | .permitAll() 89 | .antMatchers("/login/**","/auth/**", "/oauth2/**") 90 | .permitAll() 91 | .antMatchers("/blog/**") 92 | .permitAll() 93 | .anyRequest() 94 | .authenticated() 95 | .and() 96 | .oauth2Login() 97 | .authorizationEndpoint() 98 | .baseUri("/oauth2/authorize") 99 | .authorizationRequestRepository(customAuthorizationRequestRepository) 100 | .and() 101 | .redirectionEndpoint() 102 | .baseUri("/oauth2/callback/*") 103 | .and() 104 | .userInfoEndpoint() 105 | .userService(customOAuth2UserService) 106 | .and() 107 | .successHandler(oAuth2AuthenticationSuccessHandler) 108 | .failureHandler(oAuth2AuthenticationFailureHandler); 109 | 110 | http.addFilterBefore(customOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /frontend/sample/src/user/login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Login.css'; 3 | import { NAVER_AUTH_URL ,KAKAO_AUTH_URL ,GOOGLE_AUTH_URL, FACEBOOK_AUTH_URL, GITHUB_AUTH_URL, ACCESS_TOKEN, REFRESH_TOKEN } from '../../constants'; 4 | import { login } from '../../util/APIUtils'; 5 | import { Link, Redirect } from 'react-router-dom' 6 | import fbLogo from '../../img/fb-logo.png'; 7 | import googleLogo from '../../img/google-logo.png'; 8 | import githubLogo from '../../img/github-logo.png'; 9 | import kakaoLogo from '../../img/kakao-logo.png'; 10 | import naverLogo from '../../img/naver-logo.png'; 11 | 12 | import Alert from 'react-s-alert'; 13 | 14 | class Login extends Component { 15 | componentDidMount() { 16 | // If the OAuth2 login encounters an error, the user is redirected to the /login page with an error. 17 | // Here we display the error and then remove the error query parameter from the location. 18 | if(this.props.location.state && this.props.location.state.error) { 19 | setTimeout(() => { 20 | Alert.error(this.props.location.state.error, { 21 | timeout: 5000 22 | }); 23 | this.props.history.replace({ 24 | pathname: this.props.location.pathname, 25 | state: {} 26 | }); 27 | }, 100); 28 | } 29 | } 30 | 31 | render() { 32 | if(this.props.authenticated) { 33 | return ; 38 | } 39 | 40 | return ( 41 |
42 |
43 |

Sign In

44 | 45 |
46 | OR 47 |
48 | 49 | New user? Sign up! 50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | class SocialLogin extends Component { 57 | render() { 58 | return ( 59 | 72 | ); 73 | } 74 | } 75 | 76 | 77 | class LoginForm extends Component { 78 | constructor(props) { 79 | super(props); 80 | this.state = { 81 | email: '', 82 | password: '' 83 | }; 84 | this.handleInputChange = this.handleInputChange.bind(this); 85 | this.handleSubmit = this.handleSubmit.bind(this); 86 | } 87 | 88 | handleInputChange(event) { 89 | const target = event.target; 90 | const inputName = target.name; 91 | const inputValue = target.value; 92 | 93 | this.setState({ 94 | [inputName] : inputValue 95 | }); 96 | } 97 | 98 | handleSubmit(event) { 99 | event.preventDefault(); 100 | 101 | const loginRequest = Object.assign({}, this.state); 102 | 103 | login(loginRequest) 104 | .then(response => { 105 | localStorage.setItem(ACCESS_TOKEN, response.accessToken); 106 | localStorage.setItem(REFRESH_TOKEN, response.refreshToken); 107 | Alert.success("로그인에 성공하였습니다."); 108 | this.props.history.push("/"); 109 | }).catch(error => { 110 | Alert.error((error && error.message) || '로그인에 실패하였습니다.'); 111 | }); 112 | } 113 | 114 | render() { 115 | return ( 116 |
117 |
118 | 121 |
122 |
123 | 126 |
127 |
128 | 129 |
130 |
131 | ); 132 | } 133 | } 134 | 135 | export default Login 136 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/service/auth/CustomTokenProviderService.java: -------------------------------------------------------------------------------- 1 | package com.sample.service.auth; 2 | 3 | import java.security.Key; 4 | import java.util.Date; 5 | 6 | import com.sample.config.security.OAuth2Config; 7 | import com.sample.config.security.token.UserPrincipal; 8 | import com.sample.domain.mapping.TokenMapping; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.stereotype.Service; 15 | 16 | import io.jsonwebtoken.*; 17 | import io.jsonwebtoken.io.Decoders; 18 | import io.jsonwebtoken.security.Keys; 19 | 20 | import lombok.extern.slf4j.Slf4j; 21 | 22 | @Slf4j 23 | @Service 24 | public class CustomTokenProviderService { 25 | 26 | @Autowired 27 | private OAuth2Config oAuth2Config; 28 | 29 | @Autowired 30 | private CustomUserDetailsService customUserDetailsService; 31 | 32 | public TokenMapping refreshToken(Authentication authentication, String refreshToken) { 33 | UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); 34 | Date now = new Date(); 35 | 36 | Date accessTokenExpiresIn = new Date(now.getTime() + oAuth2Config.getAuth().getAccessTokenExpirationMsec()); 37 | 38 | String secretKey = oAuth2Config.getAuth().getTokenSecret(); 39 | byte[] keyBytes = Decoders.BASE64.decode(secretKey); 40 | Key key = Keys.hmacShaKeyFor(keyBytes); 41 | 42 | String accessToken = Jwts.builder() 43 | .setSubject(Long.toString(userPrincipal.getId())) 44 | .setIssuedAt(new Date()) 45 | .setExpiration(accessTokenExpiresIn) 46 | .signWith(key, SignatureAlgorithm.HS512) 47 | .compact(); 48 | 49 | return TokenMapping.builder() 50 | .userEmail(userPrincipal.getEmail()) 51 | .accessToken(accessToken) 52 | .refreshToken(refreshToken) 53 | .build(); 54 | } 55 | 56 | public TokenMapping createToken(Authentication authentication) { 57 | UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); 58 | 59 | Date now = new Date(); 60 | 61 | Date accessTokenExpiresIn = new Date(now.getTime() + oAuth2Config.getAuth().getAccessTokenExpirationMsec()); 62 | Date refreshTokenExpiresIn = new Date(now.getTime() + oAuth2Config.getAuth().getRefreshTokenExpirationMsec()); 63 | 64 | String secretKey = oAuth2Config.getAuth().getTokenSecret(); 65 | 66 | byte[] keyBytes = Decoders.BASE64.decode(secretKey); 67 | Key key = Keys.hmacShaKeyFor(keyBytes); 68 | 69 | String accessToken = Jwts.builder() 70 | .setSubject(Long.toString(userPrincipal.getId())) 71 | .setIssuedAt(new Date()) 72 | .setExpiration(accessTokenExpiresIn) 73 | .signWith(key, SignatureAlgorithm.HS512) 74 | .compact(); 75 | 76 | String refreshToken = Jwts.builder() 77 | .setExpiration(refreshTokenExpiresIn) 78 | .signWith(key, SignatureAlgorithm.HS512) 79 | .compact(); 80 | 81 | return TokenMapping.builder() 82 | .userEmail(userPrincipal.getEmail()) 83 | .accessToken(accessToken) 84 | .refreshToken(refreshToken) 85 | .build(); 86 | } 87 | 88 | public Long getUserIdFromToken(String token) { 89 | Claims claims = Jwts.parserBuilder() 90 | .setSigningKey(oAuth2Config.getAuth().getTokenSecret()) 91 | .build() 92 | .parseClaimsJws(token) 93 | .getBody(); 94 | 95 | return Long.parseLong(claims.getSubject()); 96 | } 97 | 98 | public UsernamePasswordAuthenticationToken getAuthenticationById(String token){ 99 | Long userId = getUserIdFromToken(token); 100 | UserDetails userDetails = customUserDetailsService.loadUserById(userId); 101 | UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); 102 | return authentication; 103 | } 104 | 105 | public UsernamePasswordAuthenticationToken getAuthenticationByEmail(String email){ 106 | UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); 107 | UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); 108 | return authentication; 109 | } 110 | 111 | public Long getExpiration(String token) { 112 | // accessToken 남은 유효시간 113 | Date expiration = Jwts.parserBuilder().setSigningKey(oAuth2Config.getAuth().getTokenSecret()).build().parseClaimsJws(token).getBody().getExpiration(); 114 | // 현재 시간 115 | Long now = new Date().getTime(); 116 | //시간 계산 117 | return (expiration.getTime() - now); 118 | } 119 | 120 | public boolean validateToken(String token) { 121 | try { 122 | //log.info("bearerToken = {} \n oAuth2Config.getAuth()={}", token, oAuth2Config.getAuth().getTokenSecret()); 123 | Jwts.parserBuilder().setSigningKey(oAuth2Config.getAuth().getTokenSecret()).build().parseClaimsJws(token); 124 | return true; 125 | } catch (io.jsonwebtoken.security.SecurityException ex) { 126 | log.error("잘못된 JWT 서명입니다."); 127 | } catch (MalformedJwtException ex) { 128 | log.error("잘못된 JWT 서명입니다."); 129 | } catch (ExpiredJwtException ex) { 130 | log.error("만료된 JWT 토큰입니다."); 131 | } catch (UnsupportedJwtException ex) { 132 | log.error("지원되지 않는 JWT 토큰입니다."); 133 | } catch (IllegalArgumentException ex) { 134 | log.error("JWT 토큰이 잘못되었습니다."); 135 | } 136 | return false; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/advice/ApiControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package com.sample.advice; 2 | 3 | import com.sample.advice.error.DefaultAuthenticationException; 4 | import com.sample.advice.error.DefaultException; 5 | import com.sample.advice.error.DefaultNullPointerException; 6 | import com.sample.advice.error.InvalidParameterException; 7 | import com.sample.advice.payload.ErrorCode; 8 | import com.sample.advice.payload.ErrorResponse; 9 | import com.sample.payload.response.ApiResponse; 10 | 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.security.core.AuthenticationException; 14 | import org.springframework.web.HttpRequestMethodNotSupportedException; 15 | import org.springframework.web.bind.MethodArgumentNotValidException; 16 | import org.springframework.web.bind.annotation.ExceptionHandler; 17 | import org.springframework.web.bind.annotation.RestController; 18 | import org.springframework.web.bind.annotation.RestControllerAdvice; 19 | 20 | @RestControllerAdvice(annotations = RestController.class) 21 | public class ApiControllerAdvice { 22 | 23 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 24 | protected ResponseEntity handleHttpRequestMethodNotSupportedException( 25 | HttpRequestMethodNotSupportedException e) { 26 | 27 | final ErrorResponse response = ErrorResponse 28 | .builder() 29 | .status(HttpStatus.METHOD_NOT_ALLOWED.value()) 30 | .code(e.getMessage()) 31 | .clazz(e.getMethod()) 32 | .message(e.getMessage()) 33 | .build(); 34 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 35 | return new ResponseEntity<>(apiResponse, HttpStatus.METHOD_NOT_ALLOWED); 36 | } 37 | 38 | @ExceptionHandler(MethodArgumentNotValidException.class) 39 | public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { 40 | 41 | ErrorResponse response = ErrorResponse 42 | .builder() 43 | .status(HttpStatus.METHOD_NOT_ALLOWED.value()) 44 | .code(e.getMessage()) 45 | .clazz(e.getBindingResult().getObjectName()) 46 | .message(e.toString()) 47 | .fieldErrors(e.getFieldErrors()) 48 | .build(); 49 | 50 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 51 | return new ResponseEntity<>(apiResponse, HttpStatus.OK); 52 | } 53 | 54 | @ExceptionHandler(InvalidParameterException.class) 55 | public ResponseEntity handleInvalidParameterException(InvalidParameterException e) { 56 | ErrorResponse response = ErrorResponse 57 | .builder() 58 | .status(HttpStatus.METHOD_NOT_ALLOWED.value()) 59 | .code(e.getMessage()) 60 | .clazz(e.getErrors().getObjectName()) 61 | .message(e.toString()) 62 | .fieldErrors(e.getFieldErrors()) 63 | .build(); 64 | 65 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 66 | return new ResponseEntity<>(apiResponse, HttpStatus.OK); 67 | } 68 | 69 | @ExceptionHandler(DefaultException.class) 70 | protected ResponseEntity handleDefaultException(DefaultException e) { 71 | 72 | ErrorCode errorCode = e.getErrorCode(); 73 | 74 | ErrorResponse response = ErrorResponse 75 | .builder() 76 | .status(errorCode.getStatus()) 77 | .code(errorCode.getCode()) 78 | .message(e.toString()) 79 | .build(); 80 | 81 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 82 | return new ResponseEntity<>(apiResponse, HttpStatus.resolve(errorCode.getStatus())); 83 | } 84 | 85 | @ExceptionHandler(Exception.class) 86 | protected ResponseEntity handleException(Exception e) { 87 | 88 | ErrorResponse response = ErrorResponse 89 | .builder() 90 | .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) 91 | .message(e.toString()) 92 | .build(); 93 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 94 | return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); 95 | } 96 | 97 | @ExceptionHandler(AuthenticationException.class) 98 | protected ResponseEntity handleAuthenticationException(AuthenticationException e) { 99 | 100 | ErrorResponse response = ErrorResponse 101 | .builder() 102 | .status(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value()) 103 | .message(e.getMessage()) 104 | .build(); 105 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 106 | return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); 107 | } 108 | 109 | @ExceptionHandler(DefaultAuthenticationException.class) 110 | protected ResponseEntity handleCustomAuthenticationException(DefaultAuthenticationException e) { 111 | ErrorResponse response = ErrorResponse 112 | .builder() 113 | .status(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value()) 114 | .message(e.getMessage()) 115 | .build(); 116 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 117 | return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); 118 | } 119 | 120 | 121 | @ExceptionHandler(DefaultNullPointerException.class) 122 | protected ResponseEntity handleNullPointerException(DefaultNullPointerException e) { 123 | ErrorResponse response = ErrorResponse 124 | .builder() 125 | .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) 126 | .message(e.getMessage()) 127 | .build(); 128 | 129 | ApiResponse apiResponse = ApiResponse.builder().check(false).information(response).build(); 130 | return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); 131 | } 132 | } -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/controller/auth/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.sample.controller.auth; 2 | 3 | 4 | import javax.validation.Valid; 5 | 6 | import com.sample.advice.payload.ErrorResponse; 7 | import com.sample.config.security.token.CurrentUser; 8 | import com.sample.config.security.token.UserPrincipal; 9 | import com.sample.domain.entity.user.User; 10 | import com.sample.payload.request.auth.ChangePasswordRequest; 11 | import com.sample.payload.request.auth.SignInRequest; 12 | import com.sample.payload.request.auth.SignUpRequest; 13 | import com.sample.payload.response.AuthResponse; 14 | import com.sample.payload.response.Message; 15 | import com.sample.service.auth.AuthService; 16 | import com.sample.payload.request.auth.RefreshTokenRequest; 17 | 18 | import org.springframework.http.ResponseEntity; 19 | import org.springframework.web.bind.annotation.DeleteMapping; 20 | import org.springframework.web.bind.annotation.GetMapping; 21 | import org.springframework.web.bind.annotation.PostMapping; 22 | import org.springframework.web.bind.annotation.PutMapping; 23 | import org.springframework.web.bind.annotation.RequestBody; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.RestController; 26 | 27 | import io.swagger.v3.oas.annotations.Operation; 28 | import io.swagger.v3.oas.annotations.Parameter; 29 | import io.swagger.v3.oas.annotations.media.Content; 30 | import io.swagger.v3.oas.annotations.media.Schema; 31 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 32 | import io.swagger.v3.oas.annotations.tags.Tag; 33 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 34 | 35 | import lombok.RequiredArgsConstructor; 36 | 37 | @Tag(name = "Authorization", description = "Authorization API") 38 | @RequiredArgsConstructor 39 | @RestController 40 | @RequestMapping("/auth") 41 | public class AuthController { 42 | 43 | private final AuthService authService; 44 | 45 | @Operation(summary = "유저 정보 확인", description = "현제 접속된 유저정보를 확인합니다.") 46 | @ApiResponses(value = { 47 | @ApiResponse(responseCode = "200", description = "유저 확인 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = User.class) ) } ), 48 | @ApiResponse(responseCode = "400", description = "유저 확인 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), 49 | }) 50 | @GetMapping(value = "/") 51 | public ResponseEntity whoAmI( 52 | @Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal 53 | ) { 54 | return authService.whoAmI(userPrincipal); 55 | } 56 | 57 | @Operation(summary = "유저 정보 삭제", description = "현제 접속된 유저정보를 삭제합니다.") 58 | @ApiResponses(value = { 59 | @ApiResponse(responseCode = "200", description = "유저 삭제 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class) ) } ), 60 | @ApiResponse(responseCode = "400", description = "유저 삭제 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), 61 | }) 62 | @DeleteMapping(value = "/") 63 | public ResponseEntity delete( 64 | @Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal 65 | ){ 66 | return authService.delete(userPrincipal); 67 | } 68 | 69 | @Operation(summary = "유저 정보 갱신", description = "현제 접속된 유저의 비밀번호를 새로 지정합니다.") 70 | @ApiResponses(value = { 71 | @ApiResponse(responseCode = "200", description = "유저 정보 갱신 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class) ) } ), 72 | @ApiResponse(responseCode = "400", description = "유저 정보 갱신 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), 73 | }) 74 | @PutMapping(value = "/") 75 | public ResponseEntity modify( 76 | @Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal, 77 | @Parameter(description = "Schemas의 ChangePasswordRequest를 참고해주세요.", required = true) @Valid @RequestBody ChangePasswordRequest passwordChangeRequest 78 | ){ 79 | return authService.modify(userPrincipal, passwordChangeRequest); 80 | } 81 | 82 | @Operation(summary = "유저 로그인", description = "유저 로그인을 수행합니다.") 83 | @ApiResponses(value = { 84 | @ApiResponse(responseCode = "200", description = "유저 로그인 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = AuthResponse.class) ) } ), 85 | @ApiResponse(responseCode = "400", description = "유저 로그인 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), 86 | }) 87 | @PostMapping(value = "/signin") 88 | public ResponseEntity signin( 89 | @Parameter(description = "Schemas의 SignInRequest를 참고해주세요.", required = true) @Valid @RequestBody SignInRequest signInRequest 90 | ) { 91 | return authService.signin(signInRequest); 92 | } 93 | 94 | @Operation(summary = "유저 회원가입", description = "유저 회원가입을 수행합니다.") 95 | @ApiResponses(value = { 96 | @ApiResponse(responseCode = "200", description = "회원가입 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class) ) } ), 97 | @ApiResponse(responseCode = "400", description = "회원가입 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), 98 | }) 99 | @PostMapping(value = "/signup") 100 | public ResponseEntity signup( 101 | @Parameter(description = "Schemas의 SignUpRequest를 참고해주세요.", required = true) @Valid @RequestBody SignUpRequest signUpRequest 102 | ) { 103 | return authService.signup(signUpRequest); 104 | } 105 | 106 | @Operation(summary = "토큰 갱신", description = "신규 토큰 갱신을 수행합니다.") 107 | @ApiResponses(value = { 108 | @ApiResponse(responseCode = "200", description = "토큰 갱신 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = AuthResponse.class) ) } ), 109 | @ApiResponse(responseCode = "400", description = "토큰 갱신 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), 110 | }) 111 | @PostMapping(value = "/refresh") 112 | public ResponseEntity refresh( 113 | @Parameter(description = "Schemas의 RefreshTokenRequest를 참고해주세요.", required = true) @Valid @RequestBody RefreshTokenRequest tokenRefreshRequest 114 | ) { 115 | return authService.refresh(tokenRefreshRequest); 116 | } 117 | 118 | 119 | @Operation(summary = "유저 로그아웃", description = "유저 로그아웃을 수행합니다.") 120 | @ApiResponses(value = { 121 | @ApiResponse(responseCode = "200", description = "로그아웃 성공", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class) ) } ), 122 | @ApiResponse(responseCode = "400", description = "로그아웃 실패", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class) ) } ), 123 | }) 124 | @PostMapping(value="/signout") 125 | public ResponseEntity signout( 126 | @Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal, 127 | @Parameter(description = "Schemas의 RefreshTokenRequest를 참고해주세요.", required = true) @Valid @RequestBody RefreshTokenRequest tokenRefreshRequest 128 | ) { 129 | return authService.signout(tokenRefreshRequest); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /frontend/sample/src/home/Home.css: -------------------------------------------------------------------------------- 1 | 2 | .home-container { 3 | background-color: rgb(31, 31, 31); 4 | text-align: center; 5 | min-height: calc(100vh - 60px); 6 | overflow: auto; 7 | position: relative; 8 | padding-top: 60px; 9 | } 10 | 11 | .home-image{ 12 | max-width: 100%; 13 | height: auto; 14 | display: block; 15 | } 16 | 17 | .home-title { 18 | font-size: 25px; 19 | font-weight: 400; 20 | color: azure; 21 | margin-top: 50px; 22 | } 23 | 24 | 25 | .home-footer { 26 | font-size: 8px; 27 | font-weight: 100; 28 | color: rgb(131,131,131); 29 | } 30 | 31 | 32 | .graf-bg-container { 33 | width: 100%; 34 | height: 310px; 35 | overflow: hidden; 36 | perspective: 2000px; 37 | opacity: .7 38 | } 39 | 40 | .graf-layout { 41 | height: 100%; 42 | margin: auto; 43 | position: relative; 44 | perspective: 2000px; 45 | } 46 | 47 | .graf-circle:first-child { 48 | position: absolute; 49 | top: 50%; 50 | left: 50%; 51 | transform: translate(-50%,-50%) rotateY(1000deg) rotateX(1000deg) rotate(0deg); 52 | width: 50px; 53 | height: 50px; 54 | border-radius: 50%; 55 | border: 5px solid #2098f3; 56 | animation: scaleOne 5.5s infinite alternate linear; 57 | opacity: 0 58 | } 59 | 60 | @keyframes scaleOne { 61 | to { 62 | border-radius: 50%; 63 | transform: translate(-50%,-50%) rotateY(180deg) rotateX(90deg) rotate(1000deg); 64 | opacity: 0 65 | } 66 | } 67 | 68 | .graf-circle:nth-child(2) { 69 | position: absolute; 70 | top: 50%; 71 | left: 50%; 72 | transform: translate(-50%,-50%) rotateX(135deg) rotateY(135deg) rotate(0deg); 73 | width: 310px; 74 | height: 310px; 75 | border-radius: 50%; 76 | border: 5px solid #ffa20a; 77 | border-left: none; 78 | border-top: none; 79 | animation: scaleTwo 5s infinite alternate linear 80 | } 81 | 82 | @keyframes scaleTwo { 83 | to { 84 | border-radius: 50%; 85 | transform: translate(-50%,-50%) rotateX(135deg) rotateY(135deg) rotate(1turn); 86 | opacity: 0 87 | } 88 | } 89 | 90 | .graf-circle:nth-child(3) { 91 | position: absolute; 92 | top: 50%; 93 | left: 50%; 94 | transform: translate(-50%,-50%) rotateX(45deg) rotateY(45deg) rotate(0deg); 95 | width: 310px; 96 | height: 310px; 97 | border-radius: 50%; 98 | border: 5px solid #ec412c; 99 | border-bottom: none; 100 | border-left: none; 101 | animation: scaleThree 4.5s infinite alternate linear 102 | } 103 | 104 | @keyframes scaleThree { 105 | to { 106 | border-radius: 50%; 107 | transform: translate(-50%,-50%) rotateX(45deg) rotateY(45deg) rotate(1turn); 108 | opacity: 0 109 | } 110 | } 111 | 112 | .graf-circle:nth-child(4) { 113 | position: absolute; 114 | top: 50%; 115 | left: 50%; 116 | transform: translate(-50%,-50%) rotateX(45deg) rotate(0deg); 117 | width: 310px; 118 | height: 310px; 119 | border-radius: 50%; 120 | border: 5px solid #fcbd00; 121 | border-top: none; 122 | border-right: none; 123 | animation: scaleFour 4s infinite alternate linear 124 | } 125 | 126 | @keyframes scaleFour { 127 | to { 128 | border-radius: 50%; 129 | transform: translate(-50%,-50%) rotateX(45deg) rotate(1turn); 130 | opacity: 0 131 | } 132 | } 133 | 134 | .graf-circle:nth-child(5) { 135 | position: absolute; 136 | top: 50%; 137 | left: 50%; 138 | transform: translate(-50%,-50%) rotateX(135deg) rotate(0deg); 139 | width: 310px; 140 | height: 310px; 141 | border-radius: 50%; 142 | border: 5px solid #2da94f; 143 | border-bottom: none; 144 | border-left: none; 145 | animation: scaleFive 3.5s infinite alternate linear 146 | } 147 | 148 | @keyframes scaleFive { 149 | to { 150 | border-radius: 50%; 151 | transform: translate(-50%,-50%) rotateX(135deg) rotate(1turn); 152 | opacity: 0 153 | } 154 | } 155 | 156 | .graf-circle:nth-child(6) { 157 | position: absolute; 158 | top: 50%; 159 | left: 50%; 160 | transform: translate(-50%,-50%) rotateX(100deg) rotate(0deg); 161 | width: 310px; 162 | height: 310px; 163 | border-radius: 50%; 164 | border: 15px solid #f57700; 165 | border-bottom: none; 166 | border-right: none; 167 | animation: scaleSix 3s infinite alternate linear 168 | } 169 | 170 | @keyframes scaleSix { 171 | to { 172 | border-radius: 50%; 173 | transform: translate(-50%,-50%) rotateX(100deg) rotate(1turn); 174 | opacity: 0 175 | } 176 | } 177 | 178 | .graf-circle:nth-child(7) { 179 | position: absolute; 180 | top: 50%; 181 | left: 50%; 182 | transform: translate(-50%,-50%) rotateY(-105deg) rotate(0deg); 183 | width: 310px; 184 | height: 310px; 185 | border-radius: 50%; 186 | border: 10px solid #2098f3; 187 | border-bottom: none; 188 | border-left: none; 189 | animation: scaleSeven 2.5s infinite alternate linear 190 | } 191 | 192 | @keyframes scaleSeven { 193 | to { 194 | border-radius: 50%; 195 | transform: translate(-50%,-50%) rotateY(-105deg) rotate(1turn); 196 | opacity: 0 197 | } 198 | } 199 | 200 | .graf-circle:nth-child(8) { 201 | position: absolute; 202 | top: 50%; 203 | left: 50%; 204 | transform: translate(-50%,-50%) rotateY(45deg) rotateX(45deg) rotate(0deg); 205 | width: 310px; 206 | height: 310px; 207 | border-radius: 50%; 208 | border: 5px solid #30bbb0; 209 | border-bottom: none; 210 | border-left: none; 211 | animation: scaleEight 2s infinite alternate linear 212 | } 213 | 214 | @keyframes scaleEight { 215 | to { 216 | border-radius: 50%; 217 | transform: translate(-50%,-50%) rotateY(45deg) rotateX(45deg) rotate(1turn); 218 | opacity: 0 219 | } 220 | } 221 | 222 | .graf-circle:nth-child(9) { 223 | position: absolute; 224 | top: 50%; 225 | left: 50%; 226 | transform: translate(-50%,-50%) rotateY(135deg) rotateX(135deg) rotate(0deg); 227 | width: 310px; 228 | height: 310px; 229 | border-radius: 50%; 230 | border: 5px solid #ff453c; 231 | border-bottom: none; 232 | border-right: none; 233 | animation: scaleNine 1.5s infinite alternate linear 234 | } 235 | 236 | @keyframes scaleNine { 237 | to { 238 | border-radius: 50%; 239 | transform: translate(-50%,-50%) rotateY(135deg) rotateX(135deg) rotate(1turn); 240 | opacity: 0 241 | } 242 | } 243 | 244 | .graf-circle:nth-child(10) { 245 | position: absolute; 246 | top: 50%; 247 | left: 50%; 248 | transform: translate(-50%,-50%) rotateY(113deg) rotateX(115deg) rotate(0deg); 249 | width: 310px; 250 | height: 310px; 251 | border-radius: 50%; 252 | border: 5px solid #2098f3; 253 | border-bottom: none; 254 | border-right: none; 255 | animation: scaleTen 3s infinite alternate linear 256 | } 257 | 258 | @keyframes scaleTen { 259 | to { 260 | border-radius: 50%; 261 | transform: translate(-50%,-50%) rotateY(113deg) rotateX(115deg) rotate(1turn); 262 | opacity: 0 263 | } 264 | } 265 | 266 | .graf-circle:nth-child(11) { 267 | position: absolute; 268 | top: 50%; 269 | left: 50%; 270 | transform: translate(-50%,-50%) rotateX(-45deg) rotateY(-45deg) rotate(0deg); 271 | width: 310px; 272 | height: 310px; 273 | border-radius: 50%; 274 | border: 5px solid #2098f3; 275 | border-bottom: none; 276 | border-right: none; 277 | animation: scaleEleven 2s infinite alternate linear 278 | } 279 | 280 | @keyframes scaleEleven { 281 | to { 282 | border-radius: 50%; 283 | transform: translate(-50%,-50%) rotateX(-45deg) rotateY(-45deg) rotate(1turn); 284 | opacity: 0 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /backend/sample/src/main/java/com/sample/service/auth/AuthService.java: -------------------------------------------------------------------------------- 1 | package com.sample.service.auth; 2 | 3 | import java.net.URI; 4 | import java.util.Optional; 5 | 6 | import com.sample.advice.assertThat.DefaultAssert; 7 | import com.sample.config.security.token.UserPrincipal; 8 | 9 | import com.sample.domain.entity.user.Provider; 10 | import com.sample.domain.entity.user.Role; 11 | import com.sample.domain.entity.user.Token; 12 | import com.sample.domain.entity.user.User; 13 | import com.sample.domain.mapping.TokenMapping; 14 | import com.sample.payload.request.auth.ChangePasswordRequest; 15 | import com.sample.payload.request.auth.SignInRequest; 16 | import com.sample.payload.request.auth.SignUpRequest; 17 | import com.sample.payload.request.auth.RefreshTokenRequest; 18 | import com.sample.payload.response.ApiResponse; 19 | import com.sample.payload.response.AuthResponse; 20 | import com.sample.payload.response.Message; 21 | import com.sample.repository.auth.TokenRepository; 22 | import com.sample.repository.user.UserRepository; 23 | 24 | import org.springframework.http.ResponseEntity; 25 | import org.springframework.security.authentication.AuthenticationManager; 26 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 27 | import org.springframework.security.core.Authentication; 28 | import org.springframework.security.core.context.SecurityContextHolder; 29 | import org.springframework.security.crypto.password.PasswordEncoder; 30 | import org.springframework.stereotype.Service; 31 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 32 | 33 | import lombok.RequiredArgsConstructor; 34 | 35 | @RequiredArgsConstructor 36 | @Service 37 | public class AuthService { 38 | 39 | private final AuthenticationManager authenticationManager; 40 | private final PasswordEncoder passwordEncoder; 41 | private final CustomTokenProviderService customTokenProviderService; 42 | 43 | private final UserRepository userRepository; 44 | private final TokenRepository tokenRepository; 45 | 46 | 47 | public ResponseEntity whoAmI(UserPrincipal userPrincipal){ 48 | Optional user = userRepository.findById(userPrincipal.getId()); 49 | DefaultAssert.isOptionalPresent(user); 50 | ApiResponse apiResponse = ApiResponse.builder().check(true).information(user.get()).build(); 51 | 52 | return ResponseEntity.ok(apiResponse); 53 | } 54 | 55 | public ResponseEntity delete(UserPrincipal userPrincipal){ 56 | Optional user = userRepository.findById(userPrincipal.getId()); 57 | DefaultAssert.isTrue(user.isPresent(), "유저가 올바르지 않습니다."); 58 | 59 | Optional token = tokenRepository.findByUserEmail(user.get().getEmail()); 60 | DefaultAssert.isTrue(token.isPresent(), "토큰이 유효하지 않습니다."); 61 | 62 | userRepository.delete(user.get()); 63 | tokenRepository.delete(token.get()); 64 | 65 | ApiResponse apiResponse = ApiResponse.builder().check(true).information(Message.builder().message("회원 탈퇴하셨습니다.").build()).build(); 66 | 67 | return ResponseEntity.ok(apiResponse); 68 | } 69 | 70 | public ResponseEntity modify(UserPrincipal userPrincipal, ChangePasswordRequest passwordChangeRequest){ 71 | Optional user = userRepository.findById(userPrincipal.getId()); 72 | boolean passwordCheck = passwordEncoder.matches(passwordChangeRequest.getOldPassword(),user.get().getPassword()); 73 | DefaultAssert.isTrue(passwordCheck, "잘못된 비밀번호 입니다."); 74 | 75 | boolean newPasswordCheck = passwordChangeRequest.getNewPassword().equals(passwordChangeRequest.getReNewPassword()); 76 | DefaultAssert.isTrue(newPasswordCheck, "신규 등록 비밀번호 값이 일치하지 않습니다."); 77 | 78 | 79 | passwordEncoder.encode(passwordChangeRequest.getNewPassword()); 80 | 81 | return ResponseEntity.ok(true); 82 | } 83 | 84 | public ResponseEntity signin(SignInRequest signInRequest){ 85 | Authentication authentication = authenticationManager.authenticate( 86 | new UsernamePasswordAuthenticationToken( 87 | signInRequest.getEmail(), 88 | signInRequest.getPassword() 89 | ) 90 | ); 91 | 92 | SecurityContextHolder.getContext().setAuthentication(authentication); 93 | 94 | TokenMapping tokenMapping = customTokenProviderService.createToken(authentication); 95 | Token token = Token.builder() 96 | .refreshToken(tokenMapping.getRefreshToken()) 97 | .userEmail(tokenMapping.getUserEmail()) 98 | .build(); 99 | tokenRepository.save(token); 100 | AuthResponse authResponse = AuthResponse.builder().accessToken(tokenMapping.getAccessToken()).refreshToken(token.getRefreshToken()).build(); 101 | 102 | return ResponseEntity.ok(authResponse); 103 | } 104 | 105 | public ResponseEntity signup(SignUpRequest signUpRequest){ 106 | DefaultAssert.isTrue(!userRepository.existsByEmail(signUpRequest.getEmail()), "해당 이메일이 존재하지 않습니다."); 107 | 108 | User user = User.builder() 109 | .name(signUpRequest.getName()) 110 | .email(signUpRequest.getEmail()) 111 | .password(passwordEncoder.encode(signUpRequest.getPassword())) 112 | .provider(Provider.local) 113 | .role(Role.ADMIN) 114 | .build(); 115 | 116 | userRepository.save(user); 117 | 118 | URI location = ServletUriComponentsBuilder 119 | .fromCurrentContextPath().path("/auth/") 120 | .buildAndExpand(user.getId()).toUri(); 121 | ApiResponse apiResponse = ApiResponse.builder().check(true).information(Message.builder().message("회원가입에 성공하였습니다.").build()).build(); 122 | 123 | return ResponseEntity.created(location).body(apiResponse); 124 | } 125 | 126 | public ResponseEntity refresh(RefreshTokenRequest tokenRefreshRequest){ 127 | //1차 검증 128 | boolean checkValid = valid(tokenRefreshRequest.getRefreshToken()); 129 | DefaultAssert.isAuthentication(checkValid); 130 | 131 | Optional token = tokenRepository.findByRefreshToken(tokenRefreshRequest.getRefreshToken()); 132 | Authentication authentication = customTokenProviderService.getAuthenticationByEmail(token.get().getUserEmail()); 133 | 134 | //4. refresh token 정보 값을 업데이트 한다. 135 | //시간 유효성 확인 136 | TokenMapping tokenMapping; 137 | 138 | Long expirationTime = customTokenProviderService.getExpiration(tokenRefreshRequest.getRefreshToken()); 139 | if(expirationTime > 0){ 140 | tokenMapping = customTokenProviderService.refreshToken(authentication, token.get().getRefreshToken()); 141 | }else{ 142 | tokenMapping = customTokenProviderService.createToken(authentication); 143 | } 144 | 145 | Token updateToken = token.get().updateRefreshToken(tokenMapping.getRefreshToken()); 146 | tokenRepository.save(updateToken); 147 | 148 | AuthResponse authResponse = AuthResponse.builder().accessToken(tokenMapping.getAccessToken()).refreshToken(updateToken.getRefreshToken()).build(); 149 | 150 | return ResponseEntity.ok(authResponse); 151 | } 152 | 153 | public ResponseEntity signout(RefreshTokenRequest tokenRefreshRequest){ 154 | boolean checkValid = valid(tokenRefreshRequest.getRefreshToken()); 155 | DefaultAssert.isAuthentication(checkValid); 156 | 157 | //4 token 정보를 삭제한다. 158 | Optional token = tokenRepository.findByRefreshToken(tokenRefreshRequest.getRefreshToken()); 159 | tokenRepository.delete(token.get()); 160 | ApiResponse apiResponse = ApiResponse.builder().check(true).information(Message.builder().message("로그아웃 하였습니다.").build()).build(); 161 | 162 | return ResponseEntity.ok(apiResponse); 163 | } 164 | 165 | private boolean valid(String refreshToken){ 166 | 167 | //1. 토큰 형식 물리적 검증 168 | boolean validateCheck = customTokenProviderService.validateToken(refreshToken); 169 | DefaultAssert.isTrue(validateCheck, "Token 검증에 실패하였습니다."); 170 | 171 | //2. refresh token 값을 불러온다. 172 | Optional token = tokenRepository.findByRefreshToken(refreshToken); 173 | DefaultAssert.isTrue(token.isPresent(), "탈퇴 처리된 회원입니다."); 174 | 175 | //3. email 값을 통해 인증값을 불러온다 176 | Authentication authentication = customTokenProviderService.getAuthenticationByEmail(token.get().getUserEmail()); 177 | DefaultAssert.isTrue(token.get().getUserEmail().equals(authentication.getName()), "사용자 인증에 실패하였습니다."); 178 | 179 | return true; 180 | } 181 | 182 | 183 | } 184 | -------------------------------------------------------------------------------- /backend/sample/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /backend/sample/src/test/java/com/sample/controller/auth/AuthControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.sample.controller.auth; 2 | 3 | import com.sample.lib.JsonUtils; 4 | import com.sample.payload.request.auth.ChangePasswordRequest; 5 | import com.sample.payload.request.auth.SignInRequest; 6 | import com.sample.payload.request.auth.SignUpRequest; 7 | 8 | import org.json.simple.JSONObject; 9 | import org.junit.Assert; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.context.ActiveProfiles; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | import org.springframework.test.web.servlet.ResultActions; 19 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 20 | import org.springframework.web.context.WebApplicationContext; 21 | import org.springframework.web.filter.CharacterEncodingFilter; 22 | 23 | import lombok.extern.slf4j.Slf4j; 24 | 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 27 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; 28 | 29 | @Slf4j 30 | @SpringBootTest 31 | @AutoConfigureMockMvc 32 | @ActiveProfiles(profiles = "local") 33 | public class AuthControllerTest { 34 | 35 | @Autowired 36 | private MockMvc mockMvc; 37 | 38 | @Autowired 39 | private WebApplicationContext context; 40 | 41 | @BeforeEach 42 | public void init(){ 43 | this.mockMvc = MockMvcBuilders.webAppContextSetup(context) 44 | .addFilter(new CharacterEncodingFilter("UTF-8", true)) 45 | .build(); 46 | } 47 | 48 | private void signup(String email) throws Exception{ 49 | SignUpRequest signUpRequest = new SignUpRequest(); 50 | signUpRequest.setEmail(email); 51 | signUpRequest.setPassword("string"); 52 | signUpRequest.setName("string"); 53 | 54 | ResultActions actions = this.mockMvc.perform( 55 | post("/auth/signup") 56 | .content( 57 | JsonUtils.asJsonToString(signUpRequest) 58 | ) 59 | .contentType(MediaType.APPLICATION_JSON) 60 | .accept(MediaType.APPLICATION_JSON) 61 | ) 62 | .andExpect(status().is2xxSuccessful()) 63 | .andDo(print()); 64 | 65 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 66 | log.info("jsonObject={}",jsonObject); 67 | 68 | Assert.assertEquals(jsonObject.get("check"), true); 69 | 70 | } 71 | 72 | private JSONObject signin(String email) throws Exception{ 73 | 74 | SignInRequest signInRequest = new SignInRequest(); 75 | signInRequest.setEmail(email); 76 | signInRequest.setPassword("string"); 77 | 78 | ResultActions actions = this.mockMvc.perform( 79 | post("/auth/signin") 80 | .content( 81 | JsonUtils.asJsonToString(signInRequest) 82 | ) 83 | .contentType(MediaType.APPLICATION_JSON) 84 | .accept(MediaType.APPLICATION_JSON) 85 | ) 86 | .andExpect(status().isOk()) 87 | .andDo(print()); 88 | 89 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 90 | return jsonObject; 91 | } 92 | 93 | private void remove(String email) throws Exception{ 94 | 95 | JSONObject token = signin(email); 96 | String accessToken = (String) token.get("accessToken"); 97 | 98 | this.mockMvc.perform( 99 | delete("/auth/") 100 | .header("Authorization", String.format("Bearer %s", accessToken)) 101 | .contentType(MediaType.APPLICATION_JSON) 102 | .accept(MediaType.APPLICATION_JSON) 103 | ) 104 | .andExpect(status().isOk()) 105 | .andDo(print()); 106 | } 107 | 108 | @Test 109 | void testModify() throws Exception { 110 | //give 111 | String oldPassword = "string"; 112 | String newPassword = "string"; 113 | 114 | JSONObject token = signin("string@aa.bb"); 115 | 116 | String accessToken = (String) token.get("accessToken"); 117 | 118 | ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); 119 | changePasswordRequest.setOldPassword(oldPassword); 120 | changePasswordRequest.setNewPassword(newPassword); 121 | changePasswordRequest.setReNewPassword(newPassword); 122 | 123 | //when 124 | ResultActions actions = this.mockMvc.perform( 125 | post("/auth/refresh") 126 | .content( 127 | JsonUtils.asJsonToString(changePasswordRequest) 128 | ) 129 | .header("Authorization", String.format("Bearer %s", accessToken)) 130 | .contentType(MediaType.APPLICATION_JSON) 131 | .accept(MediaType.APPLICATION_JSON) 132 | ) 133 | .andExpect(status().isOk()) 134 | .andDo(print()); 135 | 136 | //then 137 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 138 | log.info("jsonObject={}",jsonObject); 139 | 140 | } 141 | 142 | @Test 143 | void testRefresh() throws Exception { 144 | //give 145 | JSONObject token = signin("string@aa.bb"); 146 | 147 | String accessToken = (String) token.get("accessToken"); 148 | String refreshToken = (String) token.get("refreshToken"); 149 | 150 | //when 151 | ResultActions actions = this.mockMvc.perform( 152 | post("/auth/refresh") 153 | .content( 154 | JsonUtils.asJsonToString(refreshToken) 155 | ) 156 | .header("Authorization", String.format("Bearer %s", accessToken)) 157 | .contentType(MediaType.APPLICATION_JSON) 158 | .accept(MediaType.APPLICATION_JSON) 159 | ) 160 | .andExpect(status().isOk()) 161 | .andDo(print()); 162 | 163 | //then 164 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 165 | log.info("jsonObject={}",jsonObject); 166 | 167 | } 168 | 169 | 170 | @Test 171 | void testDelete() throws Exception { 172 | //give 173 | String email = "deleteString@aa.bb"; 174 | signup(email); 175 | 176 | JSONObject token = signin(email); 177 | 178 | String accessToken = (String) token.get("accessToken"); 179 | 180 | //when 181 | ResultActions actions = this.mockMvc.perform( 182 | delete("/auth/") 183 | .header("Authorization", String.format("Bearer %s", accessToken)) 184 | .contentType(MediaType.APPLICATION_JSON) 185 | .accept(MediaType.APPLICATION_JSON) 186 | ) 187 | .andExpect(status().isOk()) 188 | .andDo(print()); 189 | 190 | //then 191 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 192 | log.info("jsonObject={}",jsonObject); 193 | 194 | Assert.assertEquals(jsonObject.get("check"), true); 195 | } 196 | 197 | @Test 198 | void testSignin() throws Exception { 199 | //give 200 | SignInRequest signInRequest = new SignInRequest(); 201 | signInRequest.setEmail("string@aa.bb"); 202 | signInRequest.setPassword("string"); 203 | 204 | //when 205 | ResultActions actions = this.mockMvc.perform( 206 | post("/auth/signin") 207 | .content( 208 | JsonUtils.asJsonToString(signInRequest) 209 | ) 210 | .contentType(MediaType.APPLICATION_JSON) 211 | .accept(MediaType.APPLICATION_JSON) 212 | ) 213 | .andExpect(status().isOk()) 214 | .andDo(print()); 215 | 216 | //then 217 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 218 | log.info("jsonObject={}",jsonObject); 219 | } 220 | 221 | @Test 222 | void testSignout() throws Exception { 223 | //give 224 | JSONObject token = signin("string@aa.bb"); 225 | 226 | String accessToken = (String) token.get("accessToken"); 227 | String refreshToken = (String) token.get("refreshToken"); 228 | 229 | //when 230 | ResultActions actions = this.mockMvc.perform( 231 | post("/auth/signout") 232 | .content( 233 | JsonUtils.asJsonToString(refreshToken) 234 | ) 235 | .header("Authorization", String.format("Bearer %s", accessToken)) 236 | .contentType(MediaType.APPLICATION_JSON) 237 | .accept(MediaType.APPLICATION_JSON) 238 | ) 239 | .andExpect(status().isOk()) 240 | .andDo(print()); 241 | 242 | //then 243 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 244 | log.info("jsonObject={}",jsonObject); 245 | 246 | Assert.assertEquals(jsonObject.get("check"), true); 247 | 248 | } 249 | 250 | @Test 251 | void testSignup() throws Exception { 252 | 253 | //give 254 | String email = "createString@aa.bb"; 255 | 256 | SignUpRequest signUpRequest = new SignUpRequest(); 257 | signUpRequest.setEmail(email); 258 | signUpRequest.setPassword("string"); 259 | signUpRequest.setName("string"); 260 | 261 | //when 262 | ResultActions actions = this.mockMvc.perform( 263 | post("/auth/signup") 264 | .content( 265 | JsonUtils.asJsonToString(signUpRequest) 266 | ) 267 | .contentType(MediaType.APPLICATION_JSON) 268 | .accept(MediaType.APPLICATION_JSON) 269 | ) 270 | .andExpect(status().is2xxSuccessful()) 271 | .andDo(print()); 272 | 273 | //then 274 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 275 | log.info("jsonObject={}",jsonObject); 276 | 277 | Assert.assertEquals(jsonObject.get("check"), true); 278 | 279 | //sample delete 280 | remove(email); 281 | } 282 | 283 | @Test 284 | void testWhoAmI() throws Exception { 285 | //give 286 | JSONObject token = signin("string@aa.bb"); 287 | String accessToken = token.get("accessToken").toString(); 288 | 289 | //when 290 | ResultActions actions = this.mockMvc.perform( 291 | get("/auth/") 292 | .header("Authorization", String.format("Bearer %s", accessToken)) 293 | .contentType(MediaType.APPLICATION_JSON) 294 | .accept(MediaType.APPLICATION_JSON) 295 | ) 296 | .andExpect(status().isOk()) 297 | .andDo(print()); 298 | 299 | //then 300 | JSONObject jsonObject = JsonUtils.asStringToJson(actions.andReturn().getResponse().getContentAsString()); 301 | log.info("jsonObject={}",jsonObject); 302 | 303 | Assert.assertEquals(jsonObject.get("check"), true); 304 | 305 | } 306 | } 307 | --------------------------------------------------------------------------------