diff --git a/README.md b/README.md index 705e90d..757a5de 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ -# oauth-server +# Overview -OAuth 인증서버를 구축합니다. - -1. JWT Token을 사용합니다. -1. 해당 앱을 사용하기 위해서는 kakao 개발자 센터에서 애플리케이션을 발급받아야 합니다. 발급 후 설정방법은 노션을 참고하도록 합니다. -1. API Spec는 `{url}/swagger/index.html` 으로 요청하여`swagger` 를 참고해주세요. +1. JWT Token을 기반으로 구축 된 인증서버 입니다. +1. API Spec는 `{url}/swagger/index.html` 의 `swagger` 를 참고해주세요. 1. kakao login의 경우 콜백 때문에 테스트를 위해서는 웹앱이 필요합니다. [test/kakao-login](./test/kakao-login) 를 참고해주세요. +1. 환경 변수 범례의 경우 [configs](./configs) 를 참고해주세요. +1. 이외 컨테이너 실행과 같은 명령어는 vscode `tasks.json` 에도 정의되어 있음으로 편하게 task로 실행하세요! 😎 # Stack @@ -13,21 +12,48 @@ OAuth 인증서버를 구축합니다. 1. vscode 1. gin 1. redis:6.2.4 +1. mysql:5.7.16 1. docker -# 시퀀스 다이어그램 +# Quick Start + +## 개발 환경 + +1. [config.dev.json](./configs/config.dev.json) 에서 필요한 환경 변수를 설정합니다. +1. `docker-compose up -d` 으로 db server와 같은 개발에 필요한 컨테이너를 올립니다. +1. vscode의 `실행 및 디버그` 에서 `Server` 으로 실행 혹은 터미널에 `export GO_ENV=development && go run main.go` 으로 시작합니다. + +## 제품 시작 + +1. [config.prod.json](./configs/config.prod.json) 에서 필요한 환경 변수를 설정합니다. +1. `docker-compose up -d` docker image 생성 후 컨테이너를 실행합니다. + +> db 서비스가 초기화 후 웹 서버가 실행되어야하는데 `scratch` image라 [wait-for-it.sh](https://github.com/vishnubob/wait-for-it/) 를 실행하기가 쉽지 않았습니다. 근본적인 해결방법은 아니지만 local 환경에서 build 된 Dockerfile를 테스트하고 싶은 경우 재시작으로 해결하도록 합니다. local 이외 Prod는 k8s가 알아서 핸들링하기 때문에 상관이 없습니다. + +# Other + +## swagger 확인 및 업데이트 방법 + +swagger가 탑재되어 있음으로 [http://localhost:8080/swagger/index.html](http://localhost:8080/swagger/index.html) 에서 swagger 문서를 확인하실 수 있습니다. +이외, swagger 업데이트 명령어가 vscode `tasks.json` 에 정의되어 있음으로 편하게 task로 실행하세요! 😎 -> 추가 예정 +> 직접 명령어로 수행 하시려면 아래의 명령어를 사용하세요. +> +> ```shell +> export PATH=$(go env GOPATH)/bin:$PATH +> swag i +> ``` -# 빠른 시작 +## live reloading로 실행 방법 -1. vscode에서 `F5` 혹은 `go run main.go` 으로 시작합니다. -1. kakao developers에서 kakao application 정보 및 환경변수를 입력해주세요. +javascript, python, ruby와 같은 인터프리터 언어로 작업하는 데 익숙한 사람들은 특히 golang에서 live reloading을 갈망합니다. +이를 위해 `tasks.json` 에 live reloading를 위한 명령어를 정의해 놓았습니다. 해당 기능을 사용하기 위해서 아래의 패키지를 다운받아주세요. -# 의존성 +```shell +go get github.com/codegangsta/gin +``` -| 이름 | 명령어 | -| ------------------------------------------ | ------------------------------------- | -| [gin](https://github.com/gin-gonic/gin) | `go get -u github.com/gin-gonic/gin` | -| [redis](https://github.com/go-redis/redis) | `go get github.com/go-redis/redis/v8` | -| [GORM](https://github.com/go-gorm/gorm) | `go get -u gorm.io/gorm` | +> live reloading로 속도감 있게 개발하는 것은 좋지만 아쉬운 점은 breakpoint을 사용하기 위해서는 필요할 때마다 process를 attach 해줘야합니다. +> 이유는 reloading 마다 빌드되어 실행되는 파일이 달라서 process를 계속 추적할 수 없습니다. (아직 debug tool에서 지원되지 않는 것으로 확인됨) +> 만약, live reloading에서 breakpoint가 필요하면 `launch.json` 에 정의된 `Attach to Gin` 를 실행하세요! (이럴 줄 알고 미리 정의해 놓았습니다) +> reloading시 연결이 끈어지지만 빠른 개발을 위해 live reloading 진행하다가 필요할 떄 debug를 해나가는 전략이 더 유리할 것으로 기대합니다. diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..931da1d --- /dev/null +++ b/configs/README.md @@ -0,0 +1,40 @@ +# 환경 변수 + +1. 모두 대문자 key인 환경변수의 경우 `config` 에도 지정되어 있고 `env` 로도 넣어준다면 `env` 값을 사용하게 됩니다. +1. kakao 로그인을 사용하기 위해서는 [kakao developers](https://developers.kakao.com/) 에서 kakao application 정보를 입력해야합니다. +1. JWT 관련 secret 유출 시 token을 복호화 할 수 있음으로 주의하도록 합니다! + +## 표 범례 + +| 구성 요소 | 설명 | +| ------------- | ----------------------------------------------------------------------------- | +| Variable | 환경 변수 이름 | +| dev | 환경 변수가 개발 환경에서 사용되는지 여부 | +| qa/prod | 환경 변수가 qa, production 환경에서 사용되는지 여부 | +| Default value | 시스템 환경 변수를 사용해 환경 변수를 정하지 않았을 때 기본적으로 적용되는 값 | +| Example | 환경 변수 값으로 들어갈 수 있는 예시의 나열 | +| Explanation | 환경 변수에 대한 설명 | + +## 표 + +| Variable | dev | qa/prod | Default value | Example | Explanation | +| --------------------- | :-: | :-----: | :-----------: | ----------------------- | ------------------------------------------------------------------------------------- | +| GO_ENV | ✅ | ✅ | 🤷‍♂️ | development, production | `Go 실행 환경` 을 설정하는 값이며 프로그램 시작 전 값이 있어야 합니다. | +| STAGES | ✅ | ✅ | 🤷‍♂️ | local, qa, prod | 배포 환경을 구분하기 위함이며, 해당 값에 따라서 swagger 문서가 달라집니다. | +| SERVER_PORT | ✅ | ✅ | 🤷‍♂️ | 8080 | 웹 서버 HTTP listen port 입니다. | +| SWAGGER_HOSTNAME | ✅ | ✅ | 🤷‍♂️ | localhost, test.com | CORS 문제를 해결하기 위하여 사용되며 swagger 문서를 요청하는 도메인네임 입니다. | +| SWAGGER_PORT | ✅ | ✅ | 🤷‍♂️ | 8080, 443 | swagger 문서를 요청하는 port 입니다. | +| KAKAO_REST_API_KEY | ✅ | ✅ | 🤷‍♂️ | | kakao 로그인 API를 사용하기 위해서 필요하며 kakao API Key 입니다. | +| KAKAO_REDIRECT_URI | ✅ | ✅ | 🤷‍♂️ | | kakao 로그인 API를 사용하기 위해서 필요하며 kakao 로그인 완료 시 콜백되는 주소입니다. | +| JWT_ACCESS_SECRET | ✅ | ✅ | 🤷‍♂️ | | jwt access token 발급 시 암호화 하기 위한 key 입니다. | +| JWT_REFRESH_SECRET | ✅ | ✅ | 🤷‍♂️ | | jwt refresh token 발급 시 암호화 하기 위한 key 입니다. | +| AUTH_REDIRECT_URL | ✅ | ✅ | 🤷‍♂️ | | kakao 로그인 완료 시 서명된 jwt token을 queryString으로 전달 할 웹앱 주소입니다. | +| MYSQL_MASTER_HOST | ✅ | ✅ | 🤷‍♂️ | | `DB 주소` 로 `MASTER 환경` 에서 사용되는 값입니다. | +| MYSQL_MASTER_PORT | ✅ | ✅ | 🤷‍♂️ | | `DB port` 로 `MASTER 환경` 에서 사용되는 값입니다. | +| MYSQL_MASTER_USERNAME | ✅ | ✅ | 🤷‍♂️ | | `DB 계정명` 으로 `MASTER 환경` 에서 사용되는 값입니다. | +| MYSQL_MASTER_PASSWORD | ✅ | ✅ | 🤷‍♂️ | | `DB 계정의 비밀번호` 로 `MASTER 환경` 에서 사용되는 값입니다. | +| MYSQL_MASTER_DATABASE | ✅ | ✅ | 🤷‍♂️ | | `DB명` 으로 `MASTER 환경` 에서 사용되는 값입니다. | +| REDIS_HOST | ✅ | ✅ | 🤷‍♂️ | | `DB 주소` 입니다. | +| REDIS_PORT | ✅ | ✅ | 🤷‍♂️ | | `DB port` 입니다. | +| REDIS_PASSWORD | ✅ | ✅ | 🤷‍♂️ | | `DB 계정의 비밀번호` 입니다. | +| REDIS_DATABASE | ✅ | ✅ | 🤷‍♂️ | | `DB명` 입니다. | diff --git a/configs/config.dev.json b/configs/config.dev.json index 1be48a9..7048f92 100644 --- a/configs/config.dev.json +++ b/configs/config.dev.json @@ -7,7 +7,7 @@ "SWAGGER_PORT": "8080", "KAKAO_REST_API_KEY": "", - "KAKAO_REDIRECT_URI": "", + "KAKAO_REDIRECT_URI": "http://localhost:8080/api/users/login/kakao", "JWT_ACCESS_SECRET": "", "JWT_REFRESH_SECRET": "", diff --git a/configs/config.prod.json b/configs/config.prod.json index 2772f62..a22a125 100644 --- a/configs/config.prod.json +++ b/configs/config.prod.json @@ -7,11 +7,11 @@ "SWAGGER_PORT": "8080", "KAKAO_REST_API_KEY": "", - "KAKAO_REDIRECT_URI": "", + "KAKAO_REDIRECT_URI": "http://localhost:3001/api/users/login/kakao", "JWT_ACCESS_SECRET": "", "JWT_REFRESH_SECRET": "", - "AUTH_REDIRECT_URL": "http://localhost:3000/auth-end", + "AUTH_REDIRECT_URL": "http://localhost:2999/auth-end", "MYSQL_MASTER_HOST": "host.docker.internal", "MYSQL_MASTER_PORT": "3306", diff --git a/docker-compose.yaml b/docker-compose.yaml index 60e7d1d..15835ac 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,3 +19,15 @@ services: container_name: redis-dev ports: - 6379:6379 + oauth-server: + build: + context: ./ + dockerfile: Dockerfile + image: belf-oauth-server + depends_on: + - mysql + container_name: oauth-server-dev + ports: + - 3001:8080 + environment: + - SWAGGER_PORT=3001 diff --git a/docs/docs.go b/docs/docs.go index 55a6386..d0dc6b1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -548,9 +548,9 @@ var doc = `{ } } }, - "/users/{userEmail}": { + "/users/{userKey}": { "get": { - "description": "사용자 이메일으로 사용자를 조회합니다.", + "description": "id 혹은 email로 사용자를 조회합니다.", "consumes": [ "application/json" ], @@ -560,12 +560,12 @@ var doc = `{ "tags": [ "User" ], - "summary": "Email로 사용자 정보 조회", + "summary": "사용자 조회", "parameters": [ { "type": "string", - "description": "사용자 Email", - "name": "userEmail", + "description": "id or email", + "name": "userKey", "in": "path", "required": true } diff --git a/docs/swagger.json b/docs/swagger.json index 6d86c40..59bd995 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -528,9 +528,9 @@ } } }, - "/users/{userEmail}": { + "/users/{userKey}": { "get": { - "description": "사용자 이메일으로 사용자를 조회합니다.", + "description": "id 혹은 email로 사용자를 조회합니다.", "consumes": [ "application/json" ], @@ -540,12 +540,12 @@ "tags": [ "User" ], - "summary": "Email로 사용자 정보 조회", + "summary": "사용자 조회", "parameters": [ { "type": "string", - "description": "사용자 Email", - "name": "userEmail", + "description": "id or email", + "name": "userKey", "in": "path", "required": true } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2ca442a..5a61606 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -259,15 +259,15 @@ paths: summary: 사용자 생성 (회원가입) tags: - User - /users/{userEmail}: + /users/{userKey}: get: consumes: - application/json - description: 사용자 이메일으로 사용자를 조회합니다. + description: id 혹은 email로 사용자를 조회합니다. parameters: - - description: 사용자 Email + - description: id or email in: path - name: userEmail + name: userKey required: true type: string produces: @@ -285,7 +285,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/models.ErrResponse' - summary: Email로 사용자 정보 조회 + summary: 사용자 조회 tags: - User /users/avatar: diff --git a/internal/app/handlers/users.go b/internal/app/handlers/users.go index bdc3691..f7bf555 100644 --- a/internal/app/handlers/users.go +++ b/internal/app/handlers/users.go @@ -291,32 +291,33 @@ func UserInfoTokenQuey(c *gin.Context) { c.JSON(http.StatusOK, res) } -// @Summary Email로 사용자 정보 조회 -// @Description 사용자 이메일으로 사용자를 조회합니다. +// @Summary 사용자 조회 +// @Description id 혹은 email로 사용자를 조회합니다. // @Tags User // @Accept json // @Produce json -// @Param userEmail path string true "사용자 Email" +// @Param userKey path string true "id or email" // @Success 200 {object} models.UserInfo // @Failure 400 // @Failure 404 // @Failure 500 {object} models.ErrResponse -// @Router /users/{userEmail} [get] -func UserInfoEmailQuey(c *gin.Context) { - userEmail := c.Param("userEmail") - if userEmail == "" { +// @Router /users/{userKey} [get] +func UserInfoQuey(c *gin.Context) { + userKey := c.Param("userKey") + if userKey == "" { c.Status(http.StatusBadRequest) return } user := entitys.User{} - if err := orm.Client.Where("email = ?", userEmail).Find(&user).Error; err != nil { + result := orm.Client.Where("id = ?", userKey).Or("email = ?", userKey).Find(&user) + if result.Error != nil { c.JSON(http.StatusInternalServerError, models.ErrResponse{ - Message: err.Error(), + Message: result.Error.Error(), }) return } - if userEmail != user.Email { + if result.RowsAffected == 0 { c.Status(http.StatusNotFound) return } diff --git a/internal/app/routers/router.go b/internal/app/routers/router.go index f69cf88..e4a7108 100644 --- a/internal/app/routers/router.go +++ b/internal/app/routers/router.go @@ -23,7 +23,7 @@ func Use(api *gin.RouterGroup) { users.POST("", handlers.UserSignup) users.GET("", middlewares.TokenAuthMiddleware(), handlers.UserInfoTokenQuey) users.DELETE("", middlewares.TokenAuthMiddleware(), handlers.UserWithdrawal) - users.GET(":userEmail", handlers.UserInfoEmailQuey) + users.GET(":userKey", handlers.UserInfoQuey) users.POST("/avatar", middlewares.TokenAuthMiddleware(), handlers.UploadAvatar) users.DELETE("/avatar", middlewares.TokenAuthMiddleware(), handlers.DeleteAvatar) } diff --git a/internal/pkg/project/project.go b/internal/pkg/project/project.go index 2f9220f..d8404a4 100644 --- a/internal/pkg/project/project.go +++ b/internal/pkg/project/project.go @@ -2,5 +2,5 @@ package project const ( AppName string = "oauth-server" - AppVersion string = "1.1.0" + AppVersion string = "1.2.0" ) diff --git a/test/kakao-login/README.md b/test/kakao-login/README.md index 4877280..cffc651 100644 --- a/test/kakao-login/README.md +++ b/test/kakao-login/README.md @@ -1,11 +1,13 @@ # Overview 1. kakao login 기능을 테스트 하기 위한 웹앱 입니다. -1. `node@14` 의 CRA project 입니다. +1. `node@v14.16.1` 의 CRA project 입니다. # Quick start -1. 아래의 명령어로 웹 앱을 실행해주세요. +> 사전에 인증 서버가 실행 중 이여야 합니다. + +1. 해당 디렉터리에서 아래의 명령어로 웹 앱을 실행해주세요. ```shell npm i && npm run start diff --git a/test/kakao-login/src/pages/Home.tsx b/test/kakao-login/src/pages/Home.tsx index 1b31b21..cfad931 100644 --- a/test/kakao-login/src/pages/Home.tsx +++ b/test/kakao-login/src/pages/Home.tsx @@ -5,6 +5,7 @@ import { delLocalStorageAccessToken, delLocalStorageRefreshToken, getLocalStorageAccessToken, + getLocalStorageRefreshToken, } from "libs/local-storage"; import { useEffect, useState } from "react"; @@ -14,6 +15,7 @@ export default function Home() { const [error, setError] = useState(); const accessToken = getLocalStorageAccessToken(); + const refreshToken = getLocalStorageRefreshToken(); function handleKakaoLogin() { if (!config) { @@ -25,10 +27,16 @@ export default function Home() { window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code`; } + function handleTokenClear() { + delLocalStorageAccessToken(); + delLocalStorageRefreshToken(); + window.location.reload(); + } + useEffect(() => { (async () => { try { - if (!accessToken) { + if (!accessToken && !refreshToken) { return; } const res = await GetUserInfo(); @@ -72,6 +80,7 @@ export default function Home() { if (error) { return ( <> +

에러 발생

{error} @@ -88,9 +97,9 @@ export default function Home() { return ( <> - {userInfo ? ( <> +

카카오 로그인 완료

) : ( <> +

로그인되지 않았거나 token이 손상됨

)}