mirror of
https://github.com/whyour/qinglong.git
synced 2026-06-30 20:35:09 +08:00
添加两步验证
This commit is contained in:
+80
-4
@@ -80,10 +80,14 @@ export default (app: Router) => {
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||
if (err) console.log(err);
|
||||
const authInfo = JSON.parse(data);
|
||||
res.send({ code: 200, data: { username: authInfo.username } });
|
||||
const authService = Container.get(AuthService);
|
||||
const authInfo = await authService.getUserInfo();
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
username: authInfo.username,
|
||||
twoFactorActived: authInfo.twoFactorActived,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
@@ -91,4 +95,76 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user/two-factor/init',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const authService = Container.get(AuthService);
|
||||
const data = await authService.initTwoFactor();
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/two-factor/active',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
code: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const authService = Container.get(AuthService);
|
||||
const data = await authService.activeTwoFactor(req.body.code);
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/user/two-factor/deactive',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const authService = Container.get(AuthService);
|
||||
const data = await authService.deactiveTwoFactor();
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/user/two-factor/login',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
code: Joi.string().required(),
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const authService = Container.get(AuthService);
|
||||
const data = await authService.twoFactorLogin(req.body, req);
|
||||
res.send(data);
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,12 @@ export default ({ app }: { app: Application }) => {
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
app.use(
|
||||
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
|
||||
path: ['/api/login', '/api/crons/status', /^\/open\//],
|
||||
path: [
|
||||
'/api/login',
|
||||
'/api/crons/status',
|
||||
/^\/open\//,
|
||||
'/api/user/two-factor/login',
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -52,7 +57,9 @@ export default ({ app }: { app: Application }) => {
|
||||
if (
|
||||
!headerToken &&
|
||||
req.path &&
|
||||
(req.path === '/api/login' || req.path === '/open/auth/token')
|
||||
(req.path === '/api/login' ||
|
||||
req.path === '/open/auth/token' ||
|
||||
req.path === '/api/user/two-factor/login')
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
+85
-1
@@ -5,6 +5,7 @@ import config from '../config';
|
||||
import * as fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticator } from '@otplib/preset-default';
|
||||
|
||||
@Service()
|
||||
export default class AuthService {
|
||||
@@ -32,6 +33,8 @@ export default class AuthService {
|
||||
lastlogon,
|
||||
lastip,
|
||||
lastaddr,
|
||||
twoFactorActived,
|
||||
twoFactorChecked,
|
||||
} = JSON.parse(content);
|
||||
|
||||
if (
|
||||
@@ -42,7 +45,21 @@ export default class AuthService {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
if (twoFactorActived && !twoFactorChecked) {
|
||||
return {
|
||||
code: 420,
|
||||
message: '请输入两步验证token',
|
||||
};
|
||||
}
|
||||
|
||||
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
...JSON.parse(content),
|
||||
twoFactorChecked: false,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${Math.round(
|
||||
@@ -57,8 +74,9 @@ export default class AuthService {
|
||||
const { ip, address } = await getNetIp(req);
|
||||
if (username === cUsername && password === cPassword) {
|
||||
const data = createRandomString(50, 100);
|
||||
const expiration = twoFactorActived ? 30 : 3;
|
||||
let token = jwt.sign({ data }, config.secret as any, {
|
||||
expiresIn: 60 * 60 * 24 * 3,
|
||||
expiresIn: 60 * 60 * 24 * expiration,
|
||||
algorithm: 'HS384',
|
||||
});
|
||||
fs.writeFileSync(
|
||||
@@ -70,6 +88,7 @@ export default class AuthService {
|
||||
retries: 0,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
twoFactorChecked: false,
|
||||
}),
|
||||
);
|
||||
return { code: 200, data: { token, lastip, lastaddr, lastlogon } };
|
||||
@@ -82,6 +101,7 @@ export default class AuthService {
|
||||
lastlogon: timestamp,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
twoFactorChecked: false,
|
||||
}),
|
||||
);
|
||||
return { code: 400, message: config.authError };
|
||||
@@ -105,4 +125,68 @@ export default class AuthService {
|
||||
message: '已初始化密码,请前往auth.json查看并重新登录',
|
||||
};
|
||||
}
|
||||
|
||||
public getUserInfo(): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||
if (err) console.log(err);
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public initTwoFactor() {
|
||||
const secret = authenticator.generateSecret();
|
||||
const authInfo = this.getAuthInfo();
|
||||
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||
return { secret, url: otpauth };
|
||||
}
|
||||
|
||||
public activeTwoFactor(code: string) {
|
||||
const authInfo = this.getAuthInfo();
|
||||
const isValid = authenticator.verify({
|
||||
token: code,
|
||||
secret: authInfo.twoFactorSecret,
|
||||
});
|
||||
if (isValid) {
|
||||
this.updateAuthInfo(authInfo, { twoFactorActived: true });
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
public twoFactorLogin({ username, password, code }, req) {
|
||||
const authInfo = this.getAuthInfo();
|
||||
const isValid = authenticator.verify({
|
||||
token: code,
|
||||
secret: authInfo.twoFactorSecret,
|
||||
});
|
||||
if (isValid) {
|
||||
this.updateAuthInfo(authInfo, { twoFactorChecked: true });
|
||||
return this.login({ username, password }, req);
|
||||
} else {
|
||||
return { code: 430, message: '验证失败' };
|
||||
}
|
||||
}
|
||||
|
||||
public deactiveTwoFactor() {
|
||||
const authInfo = this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
twoFactorActived: false,
|
||||
twoFactorSecret: '',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private getAuthInfo() {
|
||||
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
return JSON.parse(content || '{}');
|
||||
}
|
||||
|
||||
private updateAuthInfo(authInfo: any, info: any) {
|
||||
fs.writeFileSync(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({ ...authInfo, ...info }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user