1. 인증
- 시스템에 접근하려눈 사용자가 누구인지 확인하는 과정
- 사용자가 제공한 정보가 시스템의 기록과 일치하는지 확인하는 절차
- 예시
- 로그인 시 사용자의 아이디와 비밀번호를 입력하는 과정. 시스템은 아이디와 비밀번호가 DB에 저장된 정보와 일치하는지 확인
- 두 단계 인증(2FA)도 인증의 한 예시이다.
- 주요 요소
- 무엇을 알고 있는가? : 비밀번호, PIN
- 무엇을 가지고 있는가? : 스마트폰, 보안 토큰 등
- 누구인가? : 지문, 얼굴 인식
2. 인가
- 인증에 성공한 후에, 해당 사용자가 무엇을 할 수 있는지 결정 하는 과정
- 사용자가 시스템에 접근할 수 있는 자원이나 기능에 대한 권한을 부여하는 것
- 예시
- 일반 사용자와 관리자가 접근 할 수 있는 기능은 차이가 있다.
- 누군가는 읽기만 할 수 있고, 누군가는 읽고 수정까지 할 수 있다.
- 주요 요소
- 역할 기반 접근 제어(RBAC) : 사용자에게 역할을 부여하고, 그 역할에 따라 권한을 할당
- 자원 기반 접근 제어 : 특정 자원에 대한 접근 권한을 개별적으로 설정
3. JWT(JSON Web Token)
- 웹 애플리케이션에서 사용자 인증 및 권한 부여를 위해 널리 사용되는 토큰 기반 인증 방식
- 클라이언트와 서버 간의 안전하고 효율적인 데이터 교환을 가능하게 하는 JSON 객체로, 정보의 무결성과 인증을 보장할 수 있다.
- 구조
- 헤더 : JWT의 유형과 해싱 알고리즘을 정의하는 정보를 포함
- 페이로드 : 토큰에 담길 실제 정보를 포함. 주로 사용자 관련 데이터와 토큰의 수명 등을 나타내는 클레임(Claims)으로 구성
- 등록된 클레임 (Registered Claims): 표준화된 클레임으로, iss (발행자), exp (만료 시간), sub (주체), aud (대상자) 등이 포함됩니다.
- 공개 클레임 (Public Claims): 충돌을 방지하기 위해 IANA JSON 웹 토큰 클레임 등록에서 정의된 클레임.
- 비공개 클레임 (Private Claims): 발행자와 소비자 간에 정의된 클레임으로, 특정 애플리케이션 요구사항에 맞게 설정됩니다.
- 서명 : 헤더와 페이로드를 결합한 후, 비밀 키를 사용해서 지정된 해싱 알고리즘으로 생성된다.
- 작동 원리
- 로그인 요청 : 사용자가 로그인할 때, 서버는 사용자의 자격 증명을 확인하고, 유효한 경우 JWT를 생성하여 클라이언트에게 반환합니다.
- 클라이언트 저장 : 클라이언트는 이 JWT를 로컬 스토리지나 쿠키에 저장합니다.
- 요청 시 토큰 포함 : 클라이언트는 이후 서버에 요청을 보낼 때, 이 JWT를 HTTP 헤더(일반적으로 Authorization: Bearer <token>)에 포함하여 전송합니다.
- 서버 검증 : 서버는 전달된 JWT의 서명을 검증하여 토큰의 무결성을 확인하고, 페이로드의 클레임을 기반으로 요청의 유효성을 판단합니다.
- 요청 처리 : 토큰이 유효하면 서버는 요청을 처리하고, 그렇지 않으면 적절한 오류를 반환합니다.
- 장점
- 자급자족 : JWT는 페이로드에 필요한 모든 정보를 포함하기 때문에, 서버는 별도의 상태를 저장할 필요가 없다.
- 확장성 : 분산 시스템에서 인증 정보를 쉽게 전파 가능
- 보안 : 서명된 JWT는 클라이언트에서 변경되지 않도록 보호
- 단점
- 크기 문제 : JWT는 필요한 모든 정보를 포함하여 길이가 길어질 수 있고, 네트워크 비용을 증가 시킬 수 있다.
- 서명 검증 : JWT가 서명되었다고 해서 암호화된 것은 아니므로, 민감한 정보는 페이로드에 담지 않아야한다.
- 무효화 어려움 : 서버에서 발급된 JWT는 만료될 때까지 유효하기 때문에, 무효화 하기 어렵다.
4. 쿠키 (Cookie)
- 사용자의 웹 브라우저에 자장되는 작은 데이터 조각
- 웹 서버는 클라이언트(브라우저)에게 쿠키를 생성하여 보내며, 이후 클라이언트는 해당 쿠키를 요청 시마다 서버에 자동으로 전송
- 사용 목적
- 사용자의 로그인 상태, 사이트 설정, 장바구니 정보 등과 같은 상태를 유지하는 데 사용
- 사용자가 다시 방문할 때, 이전의 상태를 기억하기 위해 사용
- 특징
- 저장 위치 : 쿠키는 클라이언트(사용자 브라우저)에 저장된다.
- 수명 : 쿠키는 만료 시간을 지정할 수 있다. 설정하지 않으면 브라우저가 종료될 때 삭제되는 세션 쿠키가 된다.
- 보안 : 쿠키는 클라이언트 측에서 저장되기 때문에, 민감한 정보는 저장하지 않는 것이 좋다. 아래 속성을 통해 보안을 강화 할 수 있다.
- Secure : HTTPS로만 쿠키 전송
- HttpOnly : 자바스크립트에서 쿠키에 접근하지 못하도록 설정
- SameSite : 크로스 사이트 요청 위조(CSRF) 공격을 방지하기 위해 쿠키 전송을 제한
5. 세션 (Session)
- 서버에서 생성된 사용자의 상태 정보를 유지하는 방식
- 사용자가 웹 사이트에 접솔할 때, 서버는 고유한 세션 ID를 생성하고, 이 ID를 통해 사용자와 관련된 데이터를 서버측에 저장
- 세션 ID는 클라이언트의 쿠키에 저장되며, 이후 클라이언트의 요청마다 이 세션 ID가 서버에 전송되어 사용자의 상태를 추적
- 사용 목적
- 로그인 상태, 사용자 환경 설정, 권한 정보 등의 상태를 유지하는 데 사용
- 특징
- 저장 위치 : 세션 데이터는 서버 측에 저장되며, 클라이언트에는 세션 ID만 쿠키로 저장
- 수명 : 세션은 서버 측에서 관리되며, 일정 시간 동안 활동이 없으면 세션이 만료될 수 있습니다. 일반적으로 브라우저가 종료되면 세션도 종료
- 보안 : 세션 데이터는 서버에 저장되기 때문에, 민감한 정보를 비교적 안전하게 관리할 수 있다. 세션 ID를 도난당하면 보안 문제가 발생할 수 있으므로, HTTPS를 사용해 세션 ID를 보호해야 한다
6. passport
- Node.js 애플리케이션에서 인증을 처리하기 위해 널리 사용되는 미들 웨어
- 다양한 인증 전략을 쉽게 통합할 수 있도록 도와주며, 로컬 로그인, OAuth, SAML 등 다양한 방식의 인증을 지원
- 주요 개념
- 전략(Strategy)
- 각 전략은 특정 인증 방법을 처리하기 위해 구현된 모듈이다.
- 예를들어 로컬 로그인은 passport-local 전략을 사용하고, Facebook 로그인은 passport-facebook 전략을 사용한다.
- 원하는 인증 방법에 따라 전략을 선택하고 이를 passport에 등록하여 사용할 수 있다.
- 각 전략은 특정 인증 방법을 처리하기 위해 구현된 모듈이다.
- 시리얼라이즈(serialize) / 디시리얼라이즈(deserialize)
- 시리얼라이즈는 사용자가 인증에 성공한 후, 사용자 정보를 세션에 저장하는 방식
- 디시리얼라이즈는 세션에 저장된 정보를 기반으로, 사용자를 다시 인증된 상태로 복원하는 역할
- 이 과정에서 주로 사용자 ID와 같은 최소한의 정보를 세션에 저장하고, 나중에 이 정보를 사용해 데이터베이스에서 사용자 전체 정보를 가져온다.
- 전략(Strategy)
7. passport의 주요 메서드와 기능
- passport의 흐름
- 사용자 요청이 들어오면 passport에 설정된 전략을 사용해 인증 시도
- 인증이 성공하면 serializeUser를 통해 사용자 ID를 세션에 저장
- 이후 모든 요청에서는 세션에 저장된 ID를 사용해 deserializeUser를 호출하고, 사용자 정보를 복원하여 인증된 사용자로서 요청을 처리
- 사용자가 로그아웃하면 세션이 파기되고, 사용자 인증 상태가 종료
1) passport.use()
- 인증 전략을 등록할 때 사용
- 예를 들어 로컬 전략을 사용하려면 아래와 같이 사용하면 된다.
passport.use(new LocalStrategy(
function(username, password, done) {
// 인증 로직
}
));
// 간단한 예시
passport.use(new LocalStrategy(async (username, password, done) => {
try {
const [rows] = await connection.execute('SELECT id, userId, password FROM member WHERE userId = ? AND password = ?', [username, password]);
if (rows.length > 0) {
const user = rows[0]; // 로그인 성공한 사용자
return done(null, user);
} else {
return done(null, false, { message: 'Invalid credentials.' });
}
} catch (err) {
return done(err);
}
}));
- done함수의 의미와 파라미터들
- done(error, user, info) 가 시그니처이다.
- error
- 인증 과정에서 발생한 오류를 전달하며, 오류가 없을 경우 null을 전달한다.
- user
- 인증이 성공했을 때 사용자 정보를 전달한다. 만약 인증이 실패하거나 사용자를 찾지 못한 경우에는 이 자리에 false를 전달한다.
- info
- 선택정인 정보 객체이다. 인증 실패 이유와 같은 추가적인 정보를 전달할 때 사용된다.
- 꼭 username, password만 써야하는 건 아니지만 다른 걸로 필드명을 바꾸기 위해서는 아래와 같이 코드가 난잡해 질 수 있기 때문에 비추천한다.
passport.use(new LocalStrategy(
{ usernameField: 'userId', passwordField: 'secretword' }, // 필드명 변경
async (userId, secretword, done) => {
try {
const [rows] = await connection.execute('SELECT id, userId, password FROM member WHERE userId = ? AND password = ?', [userId, secretword]);
if (rows.length > 0) {
const user = rows[0]; // 로그인 성공한 사용자
return done(null, user);
} else {
return done(null, false, { message: 'Invalid credentials.' });
}
} catch (err) {
return done(err);
}
}
));
2) passport.serializeUser()
- 사용자 정보를 세션에 저장할 때 사용
- 인증이 성공한 후, 사용자 객체를 어떻게 세션에 저장할 지 결정
- 보통은 id만 세션에 저장하고, 나중에 이 id를 사용해 전체 사용자 정보를 불러온다.
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.serializeUser((user, done) => {
done(null, user.id);
});
3) passport.deserializeUser()
- 세션에 저장된 사용자 정보를 기반으로, 실제 사용자 객체를 복원할 때 호출
- 세션에 저장된 사용자 ID를 사용해 데이터베이스에서 사용자 정보를 가져온다
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
passport.deserializeUser(async (id, done) => {
try {
const [rows] = await connection.execute('SELECT id, userId, password FROM member WHERE id = ?', [id]);
if (rows.length > 0) {
const user = rows[0];
done(null, user);
} else {
done(new Error('User not found'));
}
} catch (err) {
done(err);
}
});
4) passport.authenticate()
- 특정 전략을 사용하여 인증을 수행할 때 사용
- 이 메서드는 미들웨어로 사용되며, 요청 객체(req), 응답 객체(res), 다음 미들웨어(next)를 인수로 받는다.
- 성공, 실패, 리다이렉트 등의 동작을 설정할 수 있다.
- 다양한 옵션들 : 아래 예시에 적힌 것 외에도 다양한 옵션이 있다.
- successFlash, assignProperty, session 등이 있다. 공식 문서를 참고하자
app.post('/login',
passport.authenticate('local', {
successRedirect: '/', //인증 성공 시 메인 페이지("/")로 이동
failureRedirect: '/login', //인증 실패 시 다시 로그인 페이지로 이동
failureFlash: true //인증 실패 시 사용자에게 보여줄 메시지 설정, true면 기본 메시지 사용
})
);
5) passport.isAuthenticated()
- 현재 사용자가 인증되어 있는지 확인하는 메서드
- 인증된 사용자는 true, 아니면 false를 반환한다.
app.get('/profile', (req, res) => {
if (req.isAuthenticated()) {
res.render('profile');
} else {
res.redirect('/login');
}
});
6) passport.login()
- 사용자를 로그인 상태로 만드는 메서드
- 직접 사용자가 인증되었을 때, 인증 상태를 유지하려고 할 때 사용된다.
req.login(user, function(err) {
if (err) {
return next(err);
}
return res.redirect('/profile');
});
7) passport.logout()
- 현재 사용자 세션을 종료하는 메서드
- 사용자가 로그아웃을 할 때 호출된다.
app.get('/logout', (req, res) => {
req.logout(function(err) {
if (err) {
return next(err);
}
res.redirect('/');
});
});