나의개발일지

[Express - passport] passport 여권 인증 인가 <<><><><>< 본문

NodeJS.Express.MongoDB

[Express - passport] passport 여권 인증 인가 <<><><><><

kdy9kdy 2023. 1. 28. 18:35

 

 

 

본 글은 작성자가 어디선가 주워듣고 이해한 내용들을 개인적인 언어로 작성한 게시물입니다.

잘못된 내용이 존재할 수 있으니, 읽게 되신다면 이점을 감안해 주세요!!!

 

우리는 결국 자신이 가진 이야기로 상대방을 이해할 수 있을 뿐이다.-

 

 

 

 

 

인증과 인가(권한)를 구현하는 방법은 다양하게 있다. 이번에 사용해볼 모듈은 passport라는 npm 모듈로 인증과 인가를 보다 쉽게 구현할 수 있게 도와주는 모듈이다. 이전 프로젝트에서는 express-session을 이용하고 bcrypt를 이용해서 암호화해서 데이터베이스에 패스워드를 저장하고 로그인을 유지시켰다. passport도 비슷한 로직을 따르지만 숨겨진 기능이 우리를 대신해서 그러한 로직을 처리해 준다. 간단하게 로그인/회원가입을 구현하고 로그인한 유저가 post를 만들고 post를 작성한 유저가 해당 post를 수정/삭제할 수 있는 로직을 만들어 보자!!!! 이번 프로젝트에서 사용한 큰 틀의 모듈은 express와 mongoose, passport, passport-local, passport-local-mongoose이다. 그리고 세션 설정을 위해서 express-session도 설치해서 사용했다.

 

 

 

passport 여권???

passport모듈 이름을 정말 잘 지은 거 같다. 우리가 해외를 나가거나 때로는 신분을 확인 시키기 위해서 사용할 수 있는 게 여권인데, 그런 의미에서 회원가입, 로그인을 돕는 모듈의 이름으로 안성맞춤인 거 같다. passport에는 다양한 로그인 방법이 존재한다. 로그인 방법은 전략이라고 해서 구글, 트위터, 인스타그램, auth2등 다양한 전략이 존재한다. 이번 프로젝트에서는 그중 간단하게 진행할 수 있는 local전략을 이용하기로 했다. local은 일반적인 회원가입/로그인과 마찬가지로 username과 password를 받아서 로그인을 하는 것이다.

그리고 local전략을 사용하고 mongoose를 사용한다면 좀 더 쉽게 사용할 수 잇는 passport-local-mongoose도 사용할 수 있다. 이제 모델부터 하나씩 알아 가 보자!!!!

 

user모델

//user모델
const mongoose = require("mongoose");
const passportLocalMongoose = require("passport-local-mongoose");

const userSchema = new mongoose.Schema({
    email:String,
    age: Number
})

//내가 설정한 user스키마에 passport-local-mongoose를 플러그인 해준다.
//username필드와 passport필드, salt필드가 추가된다.
userSchema.plugin(passportLocalMongoose);

//user모델을 만든다.
const User = new mongoose.model("User", userSchema);
module.exports = User;

mongoose스키마를 만든 것을 passportLocalMongoose에 플로그인 해준다. 플로그인 하는 것만으로 username필드와 password필드, salt필드가 추가 되게 된다. 따로 스키마에서 설정해 줄 필요 없다. 또한, 다양한 메서드를 이용할 수 있게 된다. 

  • authenticate() Generates a function that is used in Passport's LocalStrategy
  • serializeUser() Generates a function that is used by Passport to serialize users into the session
  • deserializeUser() Generates a function that is used by Passport to deserialize users into the session
  • register(user, password, cb) Convenience method to register a new user instance with a given password. Checks if username is unique.
  • findByUsername() Convenience method to find a user instance by it's unique username.
  • createStrategy() Creates a configured passport-local LocalStrategy instance that can be used in passport

위와 같으 메서드를 이용할 수 있으니, 해당 내용은 passport-local-mongoose에서 확인해 보자!!!

 

 

passport 설정하기

const session = require("express-session");
const passport = require("passport");
const LocalStrategy = require("passport-local");
const User = require("./models/user");

// 세션설정 세팅하기
const sessionCongif = {
    secret:"SecretMustBeMoreLonger",
    resave:false,
    saveUninitialized:true,
    cookie:{
        httpOnly: true,
        expires : Date.now() + 1000 * 60 * 60 * 24 *7,
        maxAge  : 1000 * 60 * 60 * 24 *7
    }
}
app.use(session(sessionCongif));

// passport 초기화 및 세션 이용
app.use(passport.initialize());
app.use(passport.session());

// passport모듈은 session을 이용하기 때문에 session을 설정한 다음에 설정해주어야 한다.
// Generates a function that is used in Passport's LocalStrategy
// passport모듈이 로컬스토리지를 사용하고 로컬스토리지에 다시 우리가 설정한 유저모델을 플로그인 해준다.
passport.use(new LocalStrategy(User.authenticate()));
// Generates a function that is used by Passport to serialize users into the session
passport.serializeUser(User.serializeUser());
// Generates a function that is used by Passport to deserialize users into the session
passport.deserializeUser(User.deserializeUser());

passport는 세션을 이용하기 때문에 세션을 설정 한 뒤에 passport관련 설정을 해주어야 한다. 현재 위에 세팅된 로직은 passport와 passport-local-mongoose와 passport-local을 사용한다는 가정하에 작성된 로직이기에 만약 다른 전략을 사용한다면, 해당 로직은 변경되어야 한다.

 

유저 회원가입 하기

//회원가입 폼 만들기
app.get("/register", (req,res)=>{
    res.render("users/register")
})

//회원가입 하기
app.post("/register", userValidate, wrapAsync(async(req,res, next)=>{
    try{
        const {username, password, email, age} = req.body;
        // password를 제외한 유저만들기
        const user = new User({username, email, age});
        // passport-local-mongoose의 메서드인 register를 이용해서 패스워드 암호화후 등록 
        const registUser = await User.register(user, password);
        // passport의 login메서드로 로그인 처리
        req.login(registUser,err=>{
            if(err)return next(err)
            res.redirect("/homepage")
        })
    }catch(e){
        res.redirect("/register")
    }
}))

userValidate는 Joi를 이용한 유효성 검사 미들웨어이다. wrapAsync는 비동기함수 처리 시 오류가 발생하면 제네릭 오류 핸들러로 보내는 메스드이다. 여기서는 살 명하지 않겠다.

<회원가입>

Joi를 이용한 유효성 검사가 완료되면 해당 데이터를 뽑아서 User를 만든다. register는 passport-local-mongoose의 메서드로 먼저 유저를 만들고 만들어진 유저와 패스워드를 넘겨서 암호화해서 mongoose에 저장한다. 

<mongoose 저장 유저 정보>

위와 같이 hash 된 값이 들어가게 된다. 위에 찍힌 이미지 보다 한참 더 긴 문자열이 들어가게 된다. 회원가입을 완료 후에는 passport의 login() 메서드를 이용해서 로그인을 시키게 된다. 로그인이 완료되면 세션에 req.user에 유저 정보가 들어가게 된다. req.user는 어떻게 사용하냐에 따라서 권한을 설정할 수도, 인증을 유지할 수도 있다.

<homepage>

본 프로젝트에서는 미들웨어를 이용해서 res.locals를 설정해 템플릿에서 전역변수로 사용할 수 있게 해서 해당 유저 이름을 보이게 했다. 

 

 

유저 로그인 하기

app.get("/login", (req,res)=>{
    res.render("users/login")
})

app.post("/login", passport.authenticate("local"), wrapAsync(async(req,res)=>{
    res.redirect("/homepage")
}))

passport의 authenticate("전략")을 이용해서 username과 password를 넘겨주게 되면 자동으로 로그인이 가능한지 아는지 확인해 준다. 로그인이 완료하면 위의 이름이 들어간 템플릿이 로딩된다. 전략 뒤에 옵션을 넣어서 실패 시 어떻게 동작할 것인지를 넣을 수도 있다. 현재 위의 코드에서 로그인 실패하면 Unauthorized가 화면에 나온다.

 

 

유저 로그아웃하기

app.get("/logout", (req,res,next)=>{
    req.logout((err) => {
        if (err) next(err);
        res.redirect("/homepage");
    });
})

passport에서 제공하는 logout() 메서드를 이용해서 세션을 지워주기 때문에 로그아웃 처리가 된다. 

 

 

미들웨어

const {userJoiSchema, postJoiSchema} = require("./schemas");
const ExpressError = require("./utils/ExpressError")
const Post = require("./models/post");

module.exports.userValidate = (req,res,next)=>{
    const {error} = userJoiSchema.validate(req.body);
    if (error) {
        const msg = error.details.map(el => el.message).join(',')
        throw new ExpressError(msg, 400)
    } else {
        next();
    }
}

module.exports.postValidate = (req,res,next)=>{
    const {error} = postJoiSchema.validate(req.body);
    if (error) {
        const msg = error.details.map(el => el.message).join(',')
        throw new ExpressError(msg, 400)
    } else {
        next();
    }
}

module.exports.isLogin = (req,res,next)=>{
    if(!req.user){
        return res.redirect("/login")
    }
    next();
}

module.exports.isValidUser =async(req, res, next)=>{
    const {id} = req.params;
    const post = await Post.findById(id);
    if(!post.user.equals(req.user._id)){
        return res.redirect(`/post`)
    }
    next()
}

Joi스키마 관련된 유효성검사 미들웨어와 아래의 두 개는 각각 인증과 권한 관련 미들웨어이다. 해당 미들웨어를 필요한 라우트 핸들러에 콜백함수로 넣어주면 된다. 아래의 코드는 Post모델에 관련된 CRUD로직으로 참고용으로 함께 게시한다.

app.get("/post", wrapAsync(async(req,res)=>{
    const posts = await Post.find({}).populate("user")
    res.render("posts/index", {posts})
}))

app.get("/post/new", isLogin, (req,res)=>{
    res.render("posts/new")
})

app.post("/post", isLogin, postValidate, wrapAsync(async(req,res)=>{
    const post = new Post(req.body)
    post.user = req.user
    await post.save();
    res.redirect("/post")
}))

app.get("/post/:id", wrapAsync(async(req, res)=>{
    const { id } = req.params;
    const post = await Post.findById(id).populate("user")
    res.render("posts/detail", {post});
}))

app.get("/post/:id/edit", isLogin, isValidUser, wrapAsync(async(req, res)=>{
    const { id } = req.params;
    const post = await Post.findById(id);
    res.render("posts/edit", {post});
}))

app.put("/post/:id", isLogin, isValidUser, wrapAsync(async(req,res)=>{
    const { id } = req.params;
    await Post.findByIdAndUpdate(id, {...req.body}, {new:true});
    res.redirect(`/post/${id}`);
}))

app.delete("/post/:id", isLogin, isValidUser, wrapAsync(async(req,res)=>{
    const { id } = req.params;
    await Post.findByIdAndDelete(id);
    res.redirect("/post");
}))

권한을 확인하는 것은 반드시 로그인이 선행이 된 후에 이루어져야 한다. 로그인된 유저와 해당 Post를 작성한 유저가 일치하는지를 isValidUser미들웨어가 확인하게 된다. 만약 같다면 next()로 계속해서 로직을 이어나가고, 아니라면 미들웨어에서 처리한 대로 흘러가게 된다. 위의 로직에서 권한을 확인하는 것은 Edit와 Delete 라우트핸들러만 존재한다. 그리고 로그인한 유저가 누구인지 알기 위해서 사용할 수 있는 것인 req.user이다. 이는 passport모듈에서 세션에 자동적으로 해당 유저 정보를 넣어주게 된다. 

 

 

 

정리하자면

passport의 local전략을 이용한 간단한 인증/권한 작업이었다. 사실 요즘 많은 웹애플리케이션은 여기서 말하는 local전략보다는 구글이나 인스타그램 트위터 페이스북 카카오와 같은 다른 전략을 이용하는 경우가 많다. 하지만, 다른 전략을 알기 전에 local전략으로 어떻게 돌아가는지는 알아야 된다고 생각해서 정리한다. 현재 local전략 역시 너무 가려진 부분이 많아서 해당 로직이 왜 이렇게 동작하는지에 대한 의문이 남긴 한다. 하지만, 하나하나 모두 알 수는 없기 때문에, 제공되는 메서드는 그냥 사용하기로 했다.

 

 

 

 

 


본 글은 작성자가 공부하면서 정리한 내용입니다.

잘못된 내용 및 부족한 내용이 다수 포함될 수 있습니다.

전화가 왔는데......... 모르겠다.........

힘내자!!!!!!!