Use front-end and back-end separation, and the front-end file is located in front_end folder
Configure in config / default JS, of course, can be configured in specific files, but it's more convenient here
Online demo: https://auth.bilibilianime.com/
Warehouse: https://github.com/ayasa520/NJU-WEB/web_3/
Deploy (start)
- Preview front end
- live-server ./front_end
- Open the rear end only
- node ./bin/www or npm start
- Both front and rear ends are open
- npm run dev
Completion point
- [x] Login
- [x] Register
- [x] Password strength front-end judgment (very weak, just pretend)
- [x] token keeps login status
- [x] Graphic verification code
- [x] bcrypt encrypted storage password
rely on
- express lightweight web framework
- JSON webtoken generates a token
- Models object model of mongodb
- Svg captcha verification code
- cookie-parser
- express-session
- bcryptjs encryption
- mongoose operation MongoDB
About it
The front and back ends are separated and information is transmitted through json
The front end wraps XMLHttpRequest with Promise and uses chain calls to avoid callback
const _ajax = ({ url, method = "GET", data = null, contentType = false }) => { return new Promise((resolve, reject) => { const req = new XMLHttpRequest(); req.open(method, url, true); req.withCredentials = true; req.setRequestHeader("Content-Type", contentType); req.setRequestHeader("Authorization", "Bearer"+" "+ window.localStorage.token); req.send(data); req.onreadystatechange = () => { if (req.readyState === 4) { if (req.status === 200) { resolve(req.responseText); } else { reject(req.responseText); } } }; }); };
This can be done when calling
_ajax({url:xxxxx}).then(res=>{fun(res)},rej=>{fun(rej)}).then...
Use form form on html and onsubmit to send a request
const _onsubmit = (route) => { const username = String(document.getElementById("username").value); const password = String(document.getElementById("password").value); const captcha = String(document.getElementById("captcha").value); if (username.length === 0) { alert("User name cannot be empty"); return false; } if (password.length === 0) { alert("Password cannot be empty"); return false; } _ajax({ url: `${url}/api/${route}`, method: "POST", contentType: "application/json", data: JSON.stringify({ username: username, password: password, captcha: captcha, }), }).then( (resolved) => { alert(resolved); if (route === "login") window.localStorage.token = JSON.parse(resolved).data.token; window.location.href = "/"; }, (rejected) => { alert(rejected); } ); return false; };
server.js to write various APIs through model JS to add and query data, Some middleware are defined to verify the input user name, password and verification code
app.post("/api/register",[nameValid,pwdValid,captcha],async (req, res) => { console.log(req.body) // This step is written to the database try { const user = await User.create({ username: req.body.username, password: req.body.password, }); res.send({"message":"success",user:user}); } catch (e) { res.status(422).send(`${req.body.username} User name already exists`); console.log(e) } });
model.js defines the User object when server JS During the create operation, The password will be salted, hashed and stored in the database bcrypt's salt does not need to be stored The database uses MongoDB
const UserSchema = new mongoose.Schema({ username: { type: String, unique: true,required: true }, password: { type: String, required: true, set(val) { // Hash with bcrypt return bcryptjs.hashSync(val,bcryptjs.genSaltSync(10)); }, }, });
After the user logs in successfully, nodejs generates a token and sends it to the client, which is saved in localStorage In, when requesting some specific APIs, bring this token for back-end authentication, and delete it when logging off token. I didn't make the token expire regularly
SECRET is required for token encryption. It is better to make it into an environment variable. Here I define it as a global variable
app.post("/api/login",[nameValid,pwdValid,captcha], async (req, res) => { // console.log(req.body); const user = await User.findOne({ username: req.body.username, }); if (!user) { return res.status(422).send(`${req.body.username} user name does not exist`); } const valid = require("bcryptjs").compareSync( req.body.password, user.password ); if (!valid) { return res.status(422).send("Password error"); } const token = jwt.sign( { id: String(user._id), }, SECRET ); res.send({"message":"success", "data": { user, token: token }}); });
The verification code is generated by SVG captcha, and an img tag at the front end calls the verification code api and gets the picture, The server stores the verification code text into the session The verification code entered at the current end is transmitted, which is the same as this session Compare them in It is also a middleware
const captcha = async (req, res, next) => { const cap = String(req.body.captcha) // console.log(req.session) req.session.captcha===cap?next():res.status(422).send("Incorrect verification code") };
We need to deal with the cross domain problem, because the api for generating verification code and verification code is different, so Sessions can be shared
app.all("*", function (req, res, next) { res.header("Access-Control-Allow-Credentials", "true"); //Set the domain name that allows cross domain, * means that any domain name is allowed to cross domain res.header("Access-Control-Allow-Origin", "http://localhost:8080"); //Allowed header types res.header("Access-Control-Allow-Headers", "origin, expires, content-type, x-e4m-with, authorization"); //Cross domain allowed request mode res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS"); if (req.method.toLowerCase() == "options") res.send(200); //Let options try to request a quick end else next(); });