Skip to content

Commit

Permalink
Merge pull request #38 from CokeLee777/develop
Browse files Browse the repository at this point in the history
[RELEASE] v 1.0.0
  • Loading branch information
CokeLee777 authored Jun 19, 2023
2 parents 1283db1 + 01b77f3 commit 28e454c
Show file tree
Hide file tree
Showing 97 changed files with 1,131 additions and 1,038 deletions.
113 changes: 83 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Spring Boot, Spring security, jjwt 를 사용하여 jwt, auth를 지원하는 ap
## Contributor

| <p align="center"><img src="https://github.com/CokeLee777/spring-security-jwt-auth/assets/65009713/61bff8b2-f1bc-4387-b0fb-fc9d559b6552" width="120" height="120"/></p> CokeLee777 | <p align="center"><img src="https://github.com/CokeLee777/spring-security-jwt-auth/assets/65009713/281b2a6b-ae51-44f4-a557-858645bae0ba" width="120" height="120"/></p> JSY8869 |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <div align=center> [GitHub](https://github.com/CokeLee777) </div> | <div align=center> [GitHub](https://github.com/JSY8869) </div> |
| --- | --- |
| <div align=center> [github](https://github.com/CokeLee777) </div> | <div align=center> [github](https://github.com/JSY8869) </div> |

## 특징(Features)

Expand All @@ -34,6 +34,7 @@ Spring Boot, Spring security, jjwt 를 사용하여 jwt, auth를 지원하는 ap
- Access Token
- Refresh Token
- Cookie
- OOP

## 사용 기술(Technologies)

Expand All @@ -45,51 +46,74 @@ Spring Boot, Spring security, jjwt 를 사용하여 jwt, auth를 지원하는 ap

## 사용법(How To Use?)

### 1. 보안 전략 선택(Choose your security strategy)
### 1. Database 전략 선택(Choose your database strategy)

우선, 용도에 따라 어떤 소스를 사용할 것인지 선택하여야 합니다.

우리는 아래와 같은 소스 코드를 제공합니다.

| Database | Memory | Jpa |
| --- | --- | --- |
| Security | JWT | JWT |
| Security | OAUTH | OAUTH |
| Database | Security |
| --- | --- |
| Memory | JWT |
| Jpa | JWT |

사용할 데이터베이스와 Security 전략에 따라 선택하여 주십시오.
사용할 데이터베이스 전략에 따라 선택하여 주십시오.

### 2. 프로젝트 세팅(Set your project)

1. 우선, build.gradle 파일의 내용에 따라 spring boot 개발 환경을 세팅하세요.
2. [application.properties](http://application.properties) 내에 아래와 같은 코드를 작성합니다. (우리의 프로젝트에는 기본적으로 해당 코드가 포함되어 있습니다.)
2. [application.properties](src/main/resources/application.yml) 내에 아래와 같은 코드를 작성합니다. ( 프로젝트에는 기본적으로 해당 코드가 포함되어 있습니다.)

```yaml
```yml
# 커스텀 NOT FOUND ERROR를 생성하기 위해 기본 page not found exception 제거
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://"your domain or ip address":"your port number"/"your database name"
username: "database username"
password: "database user password"
jpa:
generate-ddl: true
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
```
1. 당신의 보안 전략에 맞는 폴더를 프로젝트에 적용시킵니다.
- 우리의 소스 코드의 구조는 폴더로 분리되어 있습니다.
- `**common` 폴더는 모든 전략에서 사용되는 코드를 포함하고 있기 때문에 정상적인 동작을 위해 해당 폴더의 파일들을 모두 프로젝트에 포함시켜야 합니다.**
- `Choose your security strategy` 단계에서 선택한 전략에 맞는 폴더의 파일들을 프로젝트에 저장합니다.
- Memory를 사용하는 경우 application.properties의 datasource 부분과 jpa부분, build.gradle의 ``implementation 'mysql:mysql-connector-java:8.0.33'``를 삭제하여야 합니다.
- 이 프로젝트의 소스 코드의 구조는 **어노테이션**으로 분리되어 있습니다.
- `Choose your security strategy` 단계에서 선택한 전략에 맞게 `SpringSecurityJwtAuthApplication` 파일에서 어노테이션을 선택하여야 합니다.
- Memory를 사용하는 경우

```java
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = DataBase.class))
```

- Database를 사용하는 경우

```java
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Memory.class))
```


### 3. 커스터마이징(Customize)

- 우리의 프로젝트튼 Lombok을 사용하지 않습니다. Lombok을 사용하고자 한다면 이를 수정하여 사용할 수 있습니다.
- User를 직접 만들고 싶은 경우에는`common.entity.User` interface를 구현하여 주세요.
- 우리의 프로젝트에서 JWT 토큰에는 유저의 Id(PK)와 Identifier, Password가 들어갑니다. 이에 주의하여 주세요.
- 프로젝트에서 JWT 토큰에는 유저의 Id(PK)와 Identifier, Password가 들어갑니다. 이에 주의하여 주세요.
- 아래와 같이 정의된 END_POINT를 커스터마이징 하세요.

| LOGOUT_END_POINT | PUBLIC_END_POINT | ANNONYMOUS_END_POINT |
| --- | --- | --- |
| “/user/sign-out” | “/” | "/", "/users/sign-in", "/users/sign-up" |
- JWT를 사용하는 경우 TokenProperties의 `SECRET_KEY`, `REFRESH_KEY`를 커스타미이징 해주세요. 이는 토큰 생성 시 비밀 키의 역할을 하며 보안을 위해 외부에 노출되지 않도록 주의하여 주세요.
- `UserRepository`나 `UserService`를 커스타미이징 할 때는 `common` 폴더 내의 인터페이스를 상속받아 구현하여야 합니다.
- `UserRepository`를 커스타미이징 할 때는 `UserRepository` 인터페이스를 구현하여야 합니다.

# 토큰 정보(Informations of Token)

Expand All @@ -105,58 +129,87 @@ spring:

**[대략적인 인증 흐름]**

![Authentication Flow](./asset/images/authentication_flow_diagram.png)
![authentication_flow_diagram](./asset/images/authentication_flow_diagram.png)

**[실제 인증 흐름]**

1. 사용자가 로그인 경로('/users/sign-in)로 로그인 요청을 시도하게 됩니다.
2. AntPathRequestMatcher는 요청한 URL이 로그인 요청인지 검증합니다.
- 요청한 URL이 로그인 요청이 아니라면 다음 필터로 넘어가게 됩니다.
- 요청한 URL이 로그인 요청이 아니라면 다음 필터로 넘어가게 됩니다.
3. 현재 요청한 사용자가 이미 인증되었는지 아닌지 확인하고, 이미 인증이 된 사용자라면 403 FORBIDDEN 응답을 내보내게 됩니다.
4. 사용자가 보낸 아이디, 비밀번호 정보를 검증하고 검증이 완료되었다면 가짜 인증객체를 만들어서 AuthenticationManager에게 실제 인증을 요청합니다.
5. AuthenticationManager는 JwtAuthenticationProvider에게 실제 인증을 위임하고, JwtAuthenticationProvider는 실제 DB에서 사용자 아이디와 비밀번호를 검증합니다.
- 사용자의 아이디가 존재하지 않거나 비밀번호가 다르다면 401 UNAUTHORIZED 응답을 내보내게 됩니다.
- 사용자의 아이디가 존재하지 않거나 비밀번호가 다르다면 401 UNAUTHORIZED 응답을 내보내게 됩니다.
6. 인증이 완료되었다면 실제 인증 객체를 만들어서 반환하고 스프링이 전역적으로 참조할 수 있도록 시큐리티 컨텍스트에 저장합니다.
7. 마지막으로 모든 과정이 성공적으로 마쳤다면 사용자에게 Access Token과 Refresh Token을 발급하여 응답합니다.

### 인가(Authorization) - 로그인 유지 또는 사용자 권한 검증

**[대략적인 인증 흐름]**

![Authorization Flow](./asset/images/authorization_flow_diagram.png)
![authorization_flow_diagram](./asset/images/authorization_flow_diagram.png)

**[실제 인증 흐름]**

1. 사용자가 원하는 아무 페이지나 요청을 시도합니다.
2. 사용자의 토큰 정보를 확인합니다.
- 사용자의 토큰이 없고, Anonymous 페이지 요청이라면 다음 필터로 이동합니다.
- 사용자의 토큰이 없고, 인증이 필요한 페이지 요청이라면 403 FORBIDDEN 응답을 내보내게 됩니다.
- 사용자의 토큰이 없고, Anonymous 페이지 요청이라면 다음 필터로 이동합니다.
- 사용자의 토큰이 없고, 인증이 필요한 페이지 요청이라면 403 FORBIDDEN 응답을 내보내게 됩니다.
3. 사용자의 Jwt Access Token을 검증한다. 단순히 토큰이 만료되었다면 Jwt Refresh Token 검증 단계로 넘어갑니다.
- 토큰이 변조되었다면 예외를 발생시키고 상황에 맞는 응답(400, 401, 403)을 내보내게 됩니다.
- 토큰이 변조되었다면 예외를 발생시키고 상황에 맞는 응답(400, 401, 403)을 내보내게 됩니다.
4. 사용자의 Jwt Refresh Token을 검증합니다.
- 토큰이 만료되었다면 304 MOVED PERMANENTLY(/users/sign-in) 응답을 내보내게 됩니다.
- 토큰이 변조되었다면 예외를 발생시키고 상황에 맞는 응답(400, 401, 403)을 내보내게 됩니다.
- 토큰이 만료되었다면 304 MOVED PERMANENTLY(/users/sign-in) 응답을 내보내게 됩니다.
- 토큰이 변조되었다면 예외를 발생시키고 상황에 맞는 응답(400, 401, 403)을 내보내게 됩니다.
5. 모든 과정이 정상적으로 성공했다면 다음 필터로 이동합니다.

### 로그아웃(Sign out) - 로그아웃 또는 사용자 권한 해제

**[대략적인 인증 흐름]**

![Sign out Flow](./asset/images/signout_flow_diagram.png)
![signout_flow_diagram](./asset/images/signout_flow_diagram.png)

**[실제 인증 흐름]**

1. 인증된 사용자가 로그아웃 요청을 시도합니다.
2. AntPathRequestMatcher는 요청한 URL이 로그아웃 요청인지 검증합니다.
- 요청한 URL이 로그아웃 요청이 아니라면 다음 필터로 넘어갑니다.
- 요청한 URL이 로그아웃 요청이 아니라면 다음 필터로 넘어갑니다.
3. 시큐리티 컨텍스트에서 인증 객체를 꺼내서 핸들러에게 넘겨줍니다.
4. 핸들러는 리프레쉬 토큰이 들어있는 쿠키를 삭제하고, 시큐리티 컨텍스트를 비우게 됩니다.
- 액세스 토큰이 있는 헤더는 클라이언트가 헤더에서 삭제하도록 합니다.
- 액세스 토큰이 있는 헤더는 클라이언트가 헤더에서 삭제하도록 합니다.
5. 모든 과정이 성공적으로 마쳤다면 304 MOVED PERMANENTLY(/users/sign-in) 응답을 내보내게 됩니다.

### OAUTH
## 객체 지향 프로그래밍

이 프로젝트는 OOP(객체 지향 프로그래밍)를 지향하며 설계 및 개발되었습니다.

객체를 중심으로 생각하고 판단하였으며 **[SOLID](https://en.wikipedia.org/wiki/SOLID)** 원칙에 따라 구현되었습니다.

### SRP(단일책임의 원칙)

- 단일책임의 원칙에 따라 한 클래스 or 한 객체가 하나의 책임만을 가지도록 설계 및 구현하였습니다.
- 토큰의 생성을 책임지는 `creator` 클래스와 생성된 토큰을 토큰 객체로 만들어서 반환하는 `provider` 클래스로 분리하는 등의 행동을 통해 단일책임의 원칙을 지킬 수 있도록 하였습니다.

### OCP(개방폐쇄의 원칙)

- 개방폐쇄의 원칙에 따라 확장에는 열려있지만 변경에는 닫혀있도록 구성요소를 설계하였습니다.
- 최상위 인터페이스인 `USER` 클래스를 통해 `Memory` 전략과 DB 전략에서 사용할 `USER` 클래스를 각각 구현하도록 설계하였습니다. 이에 따라 `Generic`을 활용하여 `repository` 인터페이스를 구현할 때 `User`의 구현체에 따라 반환값이 정해지도록 설계하였습니다.
- 또한, 각각의 클래스들은 인터페이스를 의존하도록 설계하여 OCP의 가장 중요한 메커니즘인 추상화와 다형성을 적극적으로 지향하였습니다.
- 위와 같은 설계를 통해 `USER`에 대한 확장성을 열어두고, `USER` 인터페이스가 변경될 수 없도록 설계하였습니다.

### LSP(리스코프 치환의 원칙)

- 리스코프 치환의 원칙에 따라 상위 인터페이스의 목적에 맞게 하위 클래스에서 구현하였습니다.
- 각각의 인터페이스에 대한 구현체는 인터페이스의 책임과 목적에 맞게 구현하였으며, 이를 통해 사용자는 인터페이스를 의존하여 해당 인터페이스의 구현체의 메서드를 사용하더라도 인터페이스의 목적에 맞는 결과를 반환받도록 설계하였습니다.

### ISP(인터페이스 분리의 원칙)

- 인터페이스 분리의 원칙에 따라 인터페이스로 책임과 역할을 분리하였습니다.
- `JwtTokenService` 클래스에서는 `Access Token`과 `Refresh Token`의 발급과 복호화 기능을 제공합니다. 하지만 발급과 복호화를 모든 곳에서 사용하진 않기 때문에 `Access Token`과 `Refresh Token` 각각의 발급에 대한 인터페이스와 복호화 기능을 제공하는 인터페이스로 분리하여 사용자에게 필요한 기능을 제공하는 인터페이스만을 의존하여 사용하도록 구현하였습니다.

### DIP(의존성역전의 원칙)

[추후 작성]
- 의존성역전의 원칙에 따라 상위 모듈을 가장 추상적인 인터페이스로 추상화 하여 하위 모듈에 의존하지 않도록 설계하였습니다.
- 이 프로젝트는 2가지의 Database 전략을 제공하기 때문에 2가지의 User 엔티티를 제공합니다. 이에 따라 2가지 User에 대한 Service를 구현하였습니다. 이 때 최상위 인터페이스인 `UserService` 인터페이스를 통해 추상화 하였고, 메서드의 반환값에 Generic을 사용하여 새로운 Database 전략을 추가 하더라도 User 인터페이스를 구현한 User 관련 엔티티 클래스를 반환하는 방식으로 `UserService`를 새롭게 구현할 수 있도록 설계하였습니다.

## [기능 정의서(**Functional Specification)**](https://github.com/CokeLee777/spring-security-jwt-auth/wiki/Functional-Specification)

Expand Down
Binary file modified asset/images/authentication_flow_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified asset/images/authorization_flow_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified asset/images/signout_flow_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5','io.jsonwebtoken:jjwt-jackson:0.11.5'
// compileOnly 'org.projectlombok:lombok'
// annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

implementation 'mysql:mysql-connector-java:8.0.33'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
package io.github.cokelee777.springsecurityjwtauth;

import io.github.cokelee777.springsecurityjwtauth.annotations.DataBase;
import io.github.cokelee777.springsecurityjwtauth.annotations.Memory;
import io.github.cokelee777.springsecurityjwtauth.entity.MemoryUser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@SpringBootApplication(scanBasePackages = {
"io.github.cokelee777.springsecurityjwtauth.common",
"io.github.cokelee777.springsecurityjwtauth.memory"}
)
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@SpringBootApplication
//@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Memory.class))
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = DataBase.class))
public class SpringSecurityJwtAuthApplication {

public static void main(String[] args) {
SpringApplication.run(SpringSecurityJwtAuthApplication.class, args);
}

@Bean
@Memory
public Map<String, MemoryUser> memoryStore() {
return new ConcurrentHashMap<>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.cokelee777.springsecurityjwtauth.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD})
public @interface DataBase {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.cokelee777.springsecurityjwtauth.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD})
public @interface Memory {
}

This file was deleted.

This file was deleted.

Loading

0 comments on commit 28e454c

Please sign in to comment.