- Spring Boot
- JWT ( JSON Web Token)
- Google SMTP
- JPA
- DB : MySQL, Redis
- 이메일을 입력하고 인증번호 받기 버튼을 누른다.
- 해당 이메일이 서비스에 존재한다면 이메일이 중복되었다는 응답메세지가 간다.
- 인증번호 받기 버튼을 누르면 해당 이메일로 인증번호가 전송된다. (전송받은 인증코드를 10분안에 입력해야 한다.)
- 인증번호가 잘 입력되었다면, 다음단계로 넘어간다.
- 인증번호가 틀렸다면 넘어가지 않는다.
- 인증번호전송을 여러 번 했다면, 맨 마지막으로 온 메일(최신 메일)의 인증번호로 입력해야한다.
- 닉네임과 패스워드를 입력하여 회원가입을 할 수 있다.
- 닉네임은 중복되지 않아야하며 중복확인 버튼으로 체크한다.
- 패스워드는 8~20자 사이의 문자로 영어, 숫자, 특수문자 조합으로 이루어져있다.
- 정상적으로 회원가입이 된다.
- 이메일 아이디, 비밀번호 입력 후 로그인 버튼을 누른다.
- 회원이라면 로그인이 되고 메인화면으로 넘어간다.
- 회원이 아니라면 로그인이 되지 않는다.
- 로그인시 AccessToken과 RefreshToken을 응답으로 넘겨준다.
- 임시비밀번호 발급
- 마이페이지에 들어오면 닉네임, 포인트, 내가 팔로우한 사람들의 목록이 뜬다.
- 포인트를 충전할 수 있다.
- 스트리밍 중에 포인트를 사용할 수 있다.
- Refresh Token 발급
- 비밀번호 찾기, 변경
- 닉네임 중복체크, 변경
- 회원정보 수정, 탈퇴
- 포인트 update
- admin 서비스
-
서버는 클라이언트가 요청을 보내면 응답 또는 예외를 상황에 맞게 적절히 응답해주는 일종의 서비스직이라고 생각한다.
그렇기 때문에, 요청이 들어오면 상황에 맞는 메세지와 적절한 응답값을 줘야한다고 생각했다.
예를 들어, 회원가입을 하다가 에러가 난 경우라면 어떤 에러가 났는지 무슨 문제인지를 알려주는 메세지를 제공해야한다.
이런 생각들로 초반에 어떻게 exception 처리를 할 수 있을지 고민을 많이 했다.- ControllerAdvice라는 것이 있었다.
-
우리의 서비스에서 왜 로그인/회원가입이 필요한가?
- 채팅을 치기 위해
- 추후 개발될 팔로우, 팔로잉을 통해 내가 좋아하는 스트리머의 방송을 손쉽게 찾을 수 있다.
- 방송을 보는 중에 포인트로 방송을 후원할 수 있다.
- 어떤 방송을 주로 보는 지 등의 데이터를 쌓을 수 있고 이를 통해 서비스 방향을 설정할 수 있다.
-
왜 PW, salt의 길이를 VARCHAR(100)으로 잡았나?
-
id를 DB에 저장할 때, 대소문자를 구분할지 안할 것인지 정해야한다.
- 구분을 안하는 서비스도 있지만 테스트 결과(네이버, 카카오, 구글 등)은 이메일의 대소문자를 구분하지 않는다.
ID는 대/소문자를 구분하지 않고 DB에 저장할 때 항상 소문자로 변환하여 저장한다.
- 구분을 안하는 서비스도 있지만 테스트 결과(네이버, 카카오, 구글 등)은 이메일의 대소문자를 구분하지 않는다.
-
Spring Data Redis에서 Redis에 접근하는 2가지 방식이 있다.
- 첫째는 RedisTemplate방식, 두번째는 Redis Repository방식.
-
만약, refresh token도 갈취를 당하면 어떻게 하나?
- refresh token도 새로 발급은 할 수 있지만, 로컬에 담겨있다는 점, header 또는 body에 담겨서 서버로 보내진다는 점을 두고 봤을 때, 갈취를 당했을 경우를 대비해야한다.
- 요청하는 IP 주소나 다른 정보들을 함께 저장해두어 refresh token을 새로 발급해준다던가 하는 방법이 있을 것 같다.
- access token이 만료되서 refresh token을 보내는 과정이 상당히 복잡하여 DB에 저장해놓은 refresh token을 어떻게 잘 활용할 수 있을까?
- 내가 구현한 대로 문제를 해결하면 속도와 성능이 느려질 가능성이 꽤 커보인다.
-
유저 정보를 안전하게 보호하기 위해 어떤 노력을 했나?
- 실제 구현
- 비밀번호 입력을 할 때, 문자, 숫자, 특수문자 조합으로 입력
- salt 사용 ( 고유한 salt값을 가져야하고 32바이트 이상이어야 한다.)
- SHA256 단방향 암호화 사용
- 추후 발전 방안
- 한 장비에서 1초에 5번 정도만 비교할 수 있도록 한다. (원래는 약 50억번정도 가능하다고 한다. 무분별한 해킹의 위험을 막기 위해서)
- 실제 구현
-
token은 Header? Body? Cookie? 어느 곳으로 보내주는 것이 맞을까?
- Header나 Cookie에 포함시켜서 보내는 것이 일반적이라고 한다.
- 처음엔 Response Body에 보내줘도 괜찮지 않을까? 생각했다. Header 또는 Body에 넣던 틀린 것은 아니지만 "일반적"으로는 Header에 사용
-
token을 response header에 담아서 보내는 이유
- jwt라는 데이터의 특성상 body에 담기는 data랑 구분하는 것이 좋을 것이라고 판단
- 해당 페이지도 header에 보내는 것을 추천 하고 있다.
- CSRF, XSS 해킹 위험 감소
- CSRF
- jwt라는 데이터의 특성상 body에 담기는 data랑 구분하는 것이 좋을 것이라고 판단
-
Log 관리
- 요청에 대한 Request, Response 정보들은 Fileter를 통해 로그를 통해 200 또는 400 에러 등 어느 곳에서 에러가 나고 성공이 되었는지 확인하고 있다.
좀 더 고민해 볼만한 상황
- 요청에 대한 Request, Response 정보들은 Fileter를 통해 로그를 통해 200 또는 400 에러 등 어느 곳에서 에러가 나고 성공이 되었는지 확인하고 있다.
- swagger 사용해보기
- 사내 위키에 UpLoad
- header, payload, signature로 나누어진다.
- token, refresh token
- PBKDF2
- bcrypt
- 패스워드 저장을 목적으로 설계
- OpenBSD에서 기본 암호 인증 메커니즘으로 사용
- 입력 값으로 72byte character 제약
- Access Token을 사용하는 이유는 사용자를 인증하는 방식이다. 예를 들어 마이페이지에 접근한다고 생각해보면, '나'라는 것이 증명되어야 한다.
- '나'라는 것을 어떻게 증명할 수 있을까?
- 증명이 필요한 요청마다 로그인을 할 수도 있겠다.
- 하지만, 매번 로그인을 해서 증명하는 것은 너무나 번거로운 일이다. 그렇기 때문에, 로그인을 했을 때, token을 발급받아서 클라이언트에서 token('나'를 증명해줄 증명서)을 저장하여 인증이 필요한 요청 header에 함께 보낸다.
- 클라이언트와 서버사이에서 정보를 주고받을 때, Token을 활용하는데, HTTP로 보내지는 과정에서 갈취를 당할 수도 있다.
- 그렇기 때문에 Access Token의 유지시간을 짧게 주고 Refresh Token을 사용한다.
- 사용자가 로그인을 했을 때, access token과 refresh token을 발급해준다.
- access token의 만료시간은 1시간, refresh token의 만료시간은 1주일로 설정했다.
- 로그인을 한 유저는 header에 token을 담아서 본인임을 인증한다.
- 만약, token이 만료가 된다면 서버에서는 만료된 토큰이라는 응답을 보낸다.
- 클라이언트에서 refresh token을 보내고 자신임을 증명하고 서버는 다시 access token과 refresh token을 발급해준다.
- SHA-256
- salt
- BCryptPasswordEncoder
- 비밀번호 단방향 해시 알고리즘 중 하나
- 암호화시킬 text + salt를 더하여 digest를 만드는 것이다.
- PBKDF2(Password-Based Key Derivation Function)
- scrypt
- Secure Hash Algorithm의 약자로 해시함수를 사용하는 알고리즘이다.
- MD5(Message-Digest algorithm 5)는 128비트 암호화 해시 함수이다.
- 심한 암호화 결함이 있기 때문에 보안관련 용도로는 권장하지 않고 있으며 보통 프로그램이나 파일이 원본 그대로인지 확인하는 무결성 검사등에 활용된다.
- 현재 MD5 알고리즘을 보안 관련 용도로 쓰는 것을 권장하지 않고 있으며, 심각한 보안 문제를 야기할 수 도 있다고 한다. ( SSL 인증서를 변조하는 것이 가능하다는 것이 발표되었다. )
- BCrypto를 사용하기 위해 dependecies에 implementation 'org.springframework.boot:spring-boot-starter-security' 을 추가했다.
- 이것을 추가하고 POSTMAN으로 테스트를 해보니 spring security가 생겨서 401 에러가 발생했고, web으로 url을 입력해보니, 로그인을 하라는 창이 나왔다.
- 원인 : 추가한 dependency는 spring security를 활성화시켜주었다.
- 해결방법 : implementation 'org.springframework.security:spring-security-crypto:5.1.5.RELEASE' 대체
- E-mail 전송하기 위해 사용한 JavaMailSender가 계속해서 NULL값이 들어왔다.
- 기본 생성자 주입을 해주면 되는데, @AutoWired를 썼더니 순환호출로 인해 메모리가 터져서 컴퓨터가 다운됐다.
- @RequiredArgsConstructor 을 사용하자!
- 이것을 사용할 때는 생성자 주입이 될 대상을 private final로 적어주어야 한다.
- properties에서 gmail로 메일을 보내기 위한 설정도 다 해놓았고, JavaMailSender에도 NULL값이 들어가지 않는것도 확인했는데, MailSendException이 발생하였다.
- 에러메시지를 보니 SSL, SSLHandShakeException, certification등 보안관련한 용어들이 나왔다.
- 구글링 끝에 보안 프로그램을 끄고 돌려보니 메일이 잘 온 것을 확인할 수 있었다.
| Column | type | 설명 | PK/FK |
|---|---|---|---|
| user_idx | BIGINT | 유저 고유값 ( 자동 생성 ) | PK |
| VARCHAR(255) | user ID(이메일형식) | ||
| pwd | VARCHAR(255) | user 비밀번호 | |
| salt | VARCHAR(255) | user 비밀번호 Salt | |
| nickname | VARCHAR(255) | 닉네임 | |
| grade | TINYINT | 유저상태 | Default 0 |
| point | BIGINT | 포인트 | Default 50000 |
| create_dt | DATETIME | 생성날짜 | |
| login_dt | DATETIME | 로그인한 시간 | |
| update_dt | DATETIME | update한 시간 |
| Column | type | 설명 | PK/FK |
|---|---|---|---|
| follow_idx | BIGINT | FK | |
| user_follow_idx | BIGINT | FK | |
| user_streamer_idx | BIGINT | FK | |
| create_dt | DATETIME | 생성날짜 |
| Column | type | 설명 | PK/FK |
|---|---|---|---|
| room_idx | BIGINT | 방 고유값 ( 자동 생성 ) | PK |
| user_idx | BIGINT | 스트리머고유값 | |
| title | VARCHAR(255) | 방제목 | |
| thumnail_location | VARCHAR(1000) | 썸네일저장경로 | |
| video_location | VARCHAR(1000) | 영상저장경로 | |
| created_dt | DATETIME | 방송시작날짜 | NOT NULL |
| end_stream_dt | DATETIME | 방송종료날짜 |
| Number | 설명 |
|---|---|
| 0 | 이메일 인증하기 전 |
| 1 | 회원가입완료(일반회원) |
| 3 | 이메일 인증을 한 회원 |
| 5 | 관리자 |
| 9 | 탈퇴한 회원 |
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-devtools'
//db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java'
//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
//lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-test'
//encryption
implementation 'org.springframework.security:spring-security-crypto:5.1.5.RELEASE'
//valid
implementation 'org.springframework.boot:spring-boot-starter-validation:2.3.3.RELEASE'
//jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.7.0'
implementation 'com.auth0:java-jwt:3.4.0'
//email
implementation 'org.springframework.boot:spring-boot-starter-mail'
//compile "com.sun.mail:javax.mail"
//implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// Swagger 2
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
}