(최종 프로젝트 진행중)
할 일
- Date input타입을 서버로 보내기
- 시작 날짜와 끝 날짜를 requests 테이블에 저장하기
- ‘모집중|모집완료’ 컬럼을 requests 테이블에 추가하기
- ‘작성 완료’ 버튼 ajax 연결하기
- 수정하기 페이지에 기본 글과 함께 불러와놓기
- 작성자 본인이면 수정하기 및 삭제 버튼이 나타나도록 하기
- ‘수정 완료’ 버튼 ajax 연결하기
- ‘모집중’→’모집완료’로 바꿀 방법 생각하기
- 삭제는 마이페이지에 가서 하도록 할 생각. ?
- user에게서 받은 집주소로 geoCoder.address…()으로 좌표와 동네명 얻기.
- ‘처음에 테이블 구조를 단순히 address만 있는 항목으로 만들어서 아직 수정하지 못했다”
※ 이하는 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※
localhost에 HTTPS 연결하기
localhost에 HTTPS 연결하기
- mkcert를 전역으로 설치
npm install -g mkcert
- CA(Certificate Authority) 만들기 = CA private key와 certificate 만들기
mkcert create-ca
⇒ 만들어진 CA private key(=ca.key)는 안전한 곳에 보관하도록 한다.
- localhost 도메인의 인증서 만들기 = 2에서 만든 ca.key와 ca.crt를 가지고 cert.key과 cert.crt 만들기
mkcert create-cert
- 3에서 만들어진 cert-crt와 cert-key을 config 폴더에 옮기고
- ca.crt와 ca.key, cert.crt와 cert.key는 .gitignore에 등록해줘야 한다.
- (참고한 예시 1.) 6. 코드 수정 (이를 참고하여 하단의 ‘결과’에 내 서버에서 작동하도록 코드를 써 놓았으니 그걸 참고하면 좋다)
const express = require("express"); const expressSanitizer = require("express-sanitizer"); // fs and https 모듈 가져오기 const https = require("https"); const fs = require("fs"); // certificate와 private key 가져오기 // ------------------- STEP 2 const options = { key: fs.readFileSync("./config/cert.key"), cert: fs.readFileSync("./config/cert.crt"), }; const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(expressSanitizer()); app.use("/", express.static("public")); const PORT = 8000; // http 서버는 8000번 포트로 실행 app.listen(PORT, () => { console.log(`Server started on port ${PORT}`); }); // https 의존성으로 certificate와 private key로 새로운 서버를 시작 https.createServer(options, app).listen(8080, () => { console.log(`HTTPS server started on port 8080`); });
7. http://localhost:8000과 https://localhost:8080으로 두 개의 서버가 실행되는 것을 확인하기
- 공식 Nest.js 문서가 소개하는 HTTPS 싱글 서버 생성하기:
const httpsOptions = {
key: fs.readFileSync('./secrets/private-key.pem'),
cert: fs.readFileSync('./secrets/public-certificate.pem'),
};
const app = await NestFactory.create(AppModule, {
httpsOptions,
});
await app.listen(3000);
- 공식 Nest.js 문서가 소개하는 HTTPS와 HTTP 멀티 서버 생성하기:
const httpsOptions = {
key: fs.readFileSync('./secrets/private-key.pem'),
cert: fs.readFileSync('./secrets/public-certificate.pem'),
};
const server = express();
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(server),
);
await app.init();
http.createServer(server).listen(3000);
https.createServer(httpsOptions, server).listen(443);
⇒ private-key.pem, public-certificate.pem이라는 게 대신 있네 여긴.
- HTTPS 가 좋은 점:
Secure cookie를 사용할 수 있음(Keycloack, Auth0 같은 Auth서비스를 이용할 때)
프로덕션 환경에서는 https일 떄가 많을 테니 개발 환경에서도 최대한 비슷하게 맞추려고
어떤 SaaS 서비스들은 https를 요구하므로 (Geolocation Web api도 비슷한 케이스)
- private-key.pem, public-certificate.pem 발급받기
- (개발 환경에서 https로열) hostname을 정한다.
무슨 소린지 모르겠음…
- mkcert을 설치하고 나서
- 이런 명령어로 만든다는 걸로 봐서…
$ mkcert -cert-file certs/local-cert.pem -key-file certs/local-key.pem dev.local *.dev.local
위의 Nest.js 공식문서 예제에서도 mkcert으로 공개키와 인증서를 만든는 건 똑같고 그냥 파일 이름을 private-key.pem, public-certificate.pem 라고 내가 정할 수 있는 것 같다.
- 그리고 참고할 수 있는 main.ts 코드는…
// main.ts import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import * as fs from 'fs'; import * as path from 'path'; import { AppModule } from './app/app.module'; async function bootstrap() { const ssl = process.env.SSL === 'true' ? true : false; let httpsOptions = null; if (ssl) { const keyPath = process.env.SSL_KEY_PATH || ''; const certPath = process.env.SSL_CERT_PATH || ''; httpsOptions = { key: fs.readFileSync(path.join(__dirname, keyPath)), cert: fs.readFileSync(path.join(__dirname, certPath)), }; } const app = await NestFactory.create(AppModule, { httpsOptions }); const port = Number(process.env.PORT) || 3333; const hostname = process.env.HOSTNAME || 'localhost'; await app.listen(port, hostname, () => { const address = 'http' + (ssl ? 's' : '') + '://' + hostname + ':' + port + '/'; Logger.log('Listening at ' + address); }); } bootstrap();
결론:
⇒ mkcert에서 만들어진 파일 이름은 중요하지 않다. key와 cert를 구분하여 제대로 경로를 넣어주기만 하면 된다.
⇒ hostname까지 내가 정하고 싶을 땐 저렇게 app.listen할 때 지정해주면 되겠다.
⇒ 이 방식으로 하면 https를 켰다 껐다를 .env에서 SSL=true/false로만 조절할 수 있겠다.
// .env 예시 PORT=3334 HOSTNAME=dev.local SSL=true SSL_KEY_PATH="../../../dev-stack/certs/local-key.pem" SSL_CERT_PATH="../../../dev-stack/certs/local-cert.pem"
(참고: https://dev.to/nightbr/full-https-ssl-development-environment-4dam)
(참고: https://dev.to/nightbr/local-https-for-nestjs-app-api-in-nx-workspace-54n2)
- (개발 환경에서 https로열) hostname을 정한다.
결과
위의 예시들을 참고하고 짬뽕하여 동작하게 만든… 일단 https 싱글 서버 코드:
// src/main.ts
...
import * as fs from 'fs';
async function bootstrap() {
const httpsOptions = {
key: fs.readFileSync('src/config/cert.key'),
cert: fs.readFileSync('src/config/cert.crt'),
};
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
httpsOptions,
});
await app.listen(3000, () => {
console.log('3000번 포트로 서버가 열렸습니다. https://localhost:3000');
});
}
bootstrap();
⇒ 위와 같이 했더니 https://localhost:3000로 서버가 열렸다..!
팁: fs.readFileSync()로 읽을 때 경로명을 ‘./config/cert.key’나 ‘../config/cert.key’로 하면 이상하게 파일을 못 찾는다. 저렇게 ‘src/config/cert.key’라고 해주니 앱이 실행됐다.
첫 접속 시 발생한 사소한 에러
엥 처음에 저 주소로 브라우저 창에 입력하니 이런 에러가 떴는데
3000번 포트로 서버가 열렸습니다. https://localhost:3000 [Nest] 20316 - 2023. 03. 18. 오후 4:01:21 ERROR [ExceptionsHandler] jwt expired TokenExpiredError: jwt expired at C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\jsonwebtoken\verify.js:190:21 at getSecret (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\jsonwebtoken\verify.js:97:14) at Object.module.exports [as verify] (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\jsonwebtoken\verify.js:101:10) at JwtService.verify (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\@nestjs\jwt\dist\jwt.service.js:38:20) at AuthMiddleware.use (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\src\auth\auth.middleware.ts:34:41) at C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\@nestjs\core\router\router-proxy.js:9:23 at middlewareFunction (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\@nestjs\core\middleware\middleware-module.js:166:28) at Layer.handle [as handle_request] (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\layer.js:95:5) at next (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\route.js:144:13) at Route.dispatch (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\route.js:114:3) :19) at C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\@nestjs\core\router\router-proxy.js:9:23 at Layer.handle [as handle_request] (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\layer.js:95:5) at trim_prefix (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\index.js:328:13) at C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\index.js:286:9 at Function.process_params (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\index.js:346:12) at next (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\index.js:280:10) at urlencodedParser (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\body-parser\lib\types\urlencoded.js:91:7) at Layer.handle [as handle_request] (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\layer.js:95:5) at trim_prefix (C:\Users\USER\Desktop\Sparta\05_project_무인냥품\node_modules\express\lib\router\index.js:328:13) <===========================>
새로고침하니 페이지가 잘 보인다.
처음에 kaspersky가 유효하지 않은 인증서라고 막았던 것이 원인이 되었나..?
중요:
팀원들이 위의 1~5번 과정을 각각 실행해야만 함.
// 안내용 커밋 메세지:
[추가] https 인증서와 개인키 발급을 위한 mkcert 라이브러리 설치
사용법(처음 한번은 꼭 해주셔야 합니다!):
- 1. npm install -g mkcert (mkcert를 전역으로 설치)
- 2. mkcert create-ca (CA 인증서와 개인키인 ca.crt, ca.key 파일 만들기)
- 3. mkcert create-cert (2에서 만들어진 파일들을 기반으로 cert.key, cert.crt 파일이 만들어짐)
- 4. 2에서 만든 두 개 파일은 따로 안전한 곳에 보관합니다. (저희 앱에서 쓰이지 않지만 비밀키이므로 보관합니다)
- 5. 3에서 만든 두 개 파일을 src/config/ 폴더 안에 옮겨놓습니다.
- 6. 위의 4개 파일은 .gitignore에 이미 등록시켜놨기 때문에 github에 올라가지 않을 거예요 (안전합니다!)
- 7. 이제 서버를 실행시키면 https://localhost:3000 주소로 접속할 수 있습니다!
걱정: 로그인할 때 https인데도 페이로드에 이메일과 비번이 그대로 보인다.
걱정: 로그인할 때 https인데도 페이로드에 이메일과 비번이 그대로 보인다.
- 내가 로그인 페이지 만들 때 뭘 잘못 만든걸가? 프론트 차원에서 암호화를 해서 보냈어야 하는 건가?!
⇒ https는 ‘종단간’ 암호화로서, 각 종단(서버와 브라우저)가 무결하다는 전제하에 이루어지는 것이라고 한다. 애초에 한 종단의 보안이 뚫렸으면 https로 종단간 암호화를 하거나 어떤 보호 조치를 해도 소용없다고.
⇒ 그래도 클라이언트쪽에서 미리 해시값 등으로 변환을 해서 보내는 게 더 낫다는 의견이 있다. 서버에서 비밀번호 평문값을 알 필요는 없으므로. 결국 ‘프론트 차원에서 암호화를 해서 보냈어야’ 한다는 내 추측이 맞다.
- ⇒ 근데 프론트에서 bcrypt 암호화 했는데…?
(비슷한 질문과 댓글의 의견들을 참고할 수 있다: https://www.clien.net/service/board/kin/13665880)
[Geolocation Web API] 와 Kakao Map 연계하여 도입하기까지 (느린 사고의 흐름)
[Geolocation Web API] 와 Kakao Map 연계하여 도입하기까지 (느린 사고의 흐름)
- Geolocation 으로 유저 위치정보 가져오기
⇒ Geolocation 으로 가져온 좌표와 다음 우편 api 도로명 주소로 가져오는 좌표는 소수점 2번째 자리까지는 일치하는 것 같다.
- 유저 정보에 기록된 ‘우편 주소’로 좌표 얻기 ⇒ 불가능
- 유저 정보에 기록된 ‘동네명’으로 좌표 얻기
⇒ 동네명으로 검색할 땐 검색 결과의 무조건 첫 번째 주소에서 좌표를 따오는 방식이라, 실제 유저의 집 주소에서 따온 동네 좌표와 어긋남이 클 수 있다. 아래에서 구체적인 주소로 ‘삼평동’을 얻어내 반경 1km를 표시한 것과, ‘삼평동’으로 검색해서 얻은 첫 번째 위치를 마크로 표시한 것을 보면 상당히 다름을 알 수 있다.
⇒ 반경 2km 정도까지를 오차범위로 넘어가준다면 동네명으로 식별을 고려할 만 하겠다.
- 좌표와 좌표 비교하기 대신, 유저의 GPS 좌표로 동네명 얻기:
geocoder.coord2Address(경도, 위도, callback)
⇒ data.address.region_3depth_name 이 바로 bname과 같은가?
- 유저의 GPS 좌표로 우편 번호 얻기:
geocoder.coord2Address(경도, 위도, callback)
⇒ data.road_address.zone_no // ‘04524’⇒ 유저가 비교적 좁은 우편번호 내에 위치해야 한다는 점이 단점으로 작용할지 모르겠다.
⇒ 확인 결과 동네명보다 우편번호 구역 크기가 더 작다. (동네명이같아도 우편번호는 다를 수 있음)
- 우편번호가 일치함으로 동네 인증을 허용할 것인지, 동네명이 일치함으로 허용할 것인지.
- 그도 아니면 아슬아슬한 동네 경계에 사는, 그리고 위치한 사람을 위해 좌표 기준 반경으로
⇒ 회원가입시 입력한 주소의 좌표와 현재 접속하여 인증하려는 위치가 +-1km 내라면 위치 인증이 되는 것으로 하기로 한다.
1km 반경 내에 위치하는지 여부 구하기:
- 기존 ‘주소’ 위치를 마커로 찍는다. (혹은 new daum.maps.LatLng(result.y, result.x)로 얻은 coords 객체를 저장한다)
- 현재 접속하여 인증하려는 위치를 마커로 찍는다.(역시 coords 객체를 저장한다)
- 둘 사이를 연결하는 Polyline 객체(선)를 그린다
- .getLength()로 선의 길이를 구해서 1000 이내면 1km이므로 1을 반환하도록 한다.
예를 들면 집 주소는 이렇게 원의 중심이었는데 동네 이름 ‘삼평동’으로 검색한 첫 주소는 반경 1km를 넘어가므로, 동네 인증 처리를 하지 않도록 한다:
왜 1km로 잡았냐 하면 일단 도보로 15정도 거리라서. 그리고 우편번호 구역의 크기와 3계 동네명으로 나오는 크기가 이정도면 충분히 커버한다고 보아서. 마지막으로, 잘 안되면 나중에 로직을 업데이트하면 된다는 생각에.
// 지도는 그리지 않고 계산값으로 1(1km 이내)과 0(1km 반경내 아님)을 도출하는 코드:
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=내 카카오 JavaScript API 키&libraries=services" type="text/javascript"></script>
//주소-좌표 변환 객체를 생성
var geocoder = new daum.maps.services.Geocoder();
function isWithin1000m(userAddress) {
// 유저의 도로명 주소를 불러온다
// const userAddress = ...
// 유저의 현재 GPS 위치를 불러온다.
const userCurrentLocation = getCurrentLocation();
geocoder.addressSearch(userAddress, function(results, status) {
if (status === daum.maps.services.Status.OK) {
let result = results[0];
// 주소로 검색해 kakao map 좌표 객체를 얻고
var userAddressCoords = new daum.maps.LatLng(result.y, result.x);
// 현재 좌표로 kakao map 좌표 객체를 얻어서
var userCurrentCoords = new daum.maps.LatLng(userCurrentLocation.y, userCurrentLocation.x);
// 두 좌표를 경로로 하는 폴리라인 설정
var line = new kakao.maps.Polyline();
var path = [ userAddressCoords, usercurrentCoords ];
line.setPath(path);
console.log('이전 마커와 현재 마커 사이의 거리: ', line.getLength())
const result = line.getLength() <= 1 ? true : false;
alert(`위치 인증을 마쳤습니다. 결과는 ${result}입니다`)
}
})
}
function getCurrentLocation() {
function success(position) {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
return { x: longitude, y: latitude };
}
function error() {
alert('위치 정보를 불러올 수 없습니다.');
}
if(!navigator.geolocation) {
alert('이 브라우저에서는 위치 정보 서비스를 지원하지 않습니다.');
} else {
alert('위치 정보를 성공적으로 불러옵니다')
navigator.geolocation.getCurrentPosition(success, error);
}
}
정확한 도로명 주소를 받지 않고 동네명만 받는다면, 동네명으로 검색해서 나오는 상위 10개 정도의 좌표마다 거리를 비교해서 그 중 하나라도 1km 내이면 인증을 인정하는 것으로.
→ 10개 미만이면 미만대로 비교하도록
→ 상위 10개가 너무 한 구석에 몰려있다면, 랜덤 10개로 로직 변경하기.
—> results.length ≤ 10 { 전체에 대한 forEach 거리비교 } else { 전체 중 10개 랜덤 뽑아 forEach 거리비교 }
Uploaded by N2T