Showing
11 changed files
with
240 additions
and
34 deletions
app/pages/common/components/captcha.vue
0 → 100644
1 | +<template> | ||
2 | +<div class="captcha-box"> | ||
3 | + <div class="captcha-box-header"> | ||
4 | + <span>请将下列图片点击翻转至正确方向</span> | ||
5 | + <a href="javascript:;" class="img-check-refresh" @click="captchaRefresh">换一批</a> | ||
6 | + </div> | ||
7 | + <div class="captcha-box-content"> | ||
8 | + <div class="item" | ||
9 | + v-for="block in blocks" | ||
10 | + :key="block.posX" | ||
11 | + :style="{ | ||
12 | + 'background-image': `url(${imgSrc})`, | ||
13 | + 'background-position': `${block.posX}px ${block.posY}px` | ||
14 | + }" | ||
15 | + @click="captchaClick(block)"></div> | ||
16 | + </div> | ||
17 | +</div> | ||
18 | +</template> | ||
19 | +<script> | ||
20 | +import _ from 'lodash'; | ||
21 | + | ||
22 | +export default { | ||
23 | + name: 'captcha-box', | ||
24 | + data() { | ||
25 | + return { | ||
26 | + captchaSrc: '/Api/captcha.jpg', | ||
27 | + random: Math.random(), | ||
28 | + blocks: [] | ||
29 | + }; | ||
30 | + }, | ||
31 | + created() { | ||
32 | + this.blocks = [{ | ||
33 | + val: 0, | ||
34 | + posX: 0, | ||
35 | + posY: 0 | ||
36 | + }, { | ||
37 | + val: 0, | ||
38 | + posX: -60, | ||
39 | + posY: 0 | ||
40 | + }, { | ||
41 | + val: 0, | ||
42 | + posX: -120, | ||
43 | + posY: 0 | ||
44 | + }, { | ||
45 | + val: 0, | ||
46 | + posX: -180, | ||
47 | + posY: 0 | ||
48 | + }]; | ||
49 | + }, | ||
50 | + computed: { | ||
51 | + imgSrc() { | ||
52 | + return `${this.captchaSrc}?r=${this.random}`; | ||
53 | + } | ||
54 | + }, | ||
55 | + methods: { | ||
56 | + captchaClick(block) { | ||
57 | + block.val = (block.val + 1) % 4; | ||
58 | + block.posY = (block.posY - 60) % 240; | ||
59 | + let vals = _.map(this.blocks, b => { | ||
60 | + return b.val; | ||
61 | + }); | ||
62 | + | ||
63 | + this.$emit('change', vals); | ||
64 | + }, | ||
65 | + captchaRefresh() { | ||
66 | + this.random = Math.random(); | ||
67 | + } | ||
68 | + } | ||
69 | +}; | ||
70 | +</script> | ||
71 | +<style lang="scss"> | ||
72 | +.captcha-box { | ||
73 | + margin: 0 auto; | ||
74 | + width: 270px; | ||
75 | +} | ||
76 | + | ||
77 | +.captcha-box-header { | ||
78 | + color: #b0b0b0; | ||
79 | + | ||
80 | + a { | ||
81 | + color: #ff1901; | ||
82 | + float: right; | ||
83 | + } | ||
84 | +} | ||
85 | + | ||
86 | +.captcha-box-content { | ||
87 | + display: flex; | ||
88 | + | ||
89 | + .item { | ||
90 | + flex: 1; | ||
91 | + cursor: pointer; | ||
92 | + margin-right: 10px; | ||
93 | + width: 60px; | ||
94 | + height: 60px; | ||
95 | + overflow: hidden; | ||
96 | + border: solid 1px #ccc; | ||
97 | + background-size: 240px; | ||
98 | + | ||
99 | + &:last-child { | ||
100 | + margin-right: 0; | ||
101 | + } | ||
102 | + } | ||
103 | +} | ||
104 | +</style> | ||
105 | + |
app/pages/common/components/index.js
0 → 100644
@@ -16,6 +16,9 @@ | @@ -16,6 +16,9 @@ | ||
16 | <Icon type="ios-locked-outline" slot="prepend"></Icon> | 16 | <Icon type="ios-locked-outline" slot="prepend"></Icon> |
17 | </Input> | 17 | </Input> |
18 | </Form-item> | 18 | </Form-item> |
19 | + <Form-item> | ||
20 | + <captcha v-if="isCaptcha" @change="captchaChange"></captcha> | ||
21 | + </Form-item> | ||
19 | <Form-item class="login-btn"> | 22 | <Form-item class="login-btn"> |
20 | <Button type="primary" :loading="loading" @click="handleSubmit('formInline')"> | 23 | <Button type="primary" :loading="loading" @click="handleSubmit('formInline')"> |
21 | 登录 | 24 | 登录 |
@@ -29,47 +32,65 @@ | @@ -29,47 +32,65 @@ | ||
29 | 32 | ||
30 | <script> | 33 | <script> |
31 | import Vue from 'vue'; | 34 | import Vue from 'vue'; |
35 | +import _ from 'lodash'; | ||
36 | +import {Captcha} from './components'; | ||
32 | 37 | ||
33 | export default { | 38 | export default { |
34 | name: 'login', | 39 | name: 'login', |
40 | + data() { | ||
41 | + return { | ||
42 | + loading: false, | ||
43 | + isCaptcha: false, | ||
44 | + captcha: '', | ||
45 | + formInline: { | ||
46 | + user: '', | ||
47 | + password: '' | ||
48 | + }, | ||
49 | + ruleInline: { | ||
50 | + user: [ | ||
51 | + { required: true, message: '请填写用户名', trigger: 'blur' } | ||
52 | + ], | ||
53 | + password: [ | ||
54 | + { required: true, message: '请填写密码', trigger: 'blur' }, | ||
55 | + { type: 'string', min: 6, message: '密码长度不能小于6位', trigger: 'blur' } | ||
56 | + ] | ||
57 | + } | ||
58 | + }; | ||
59 | + }, | ||
60 | + created() { | ||
61 | + this.isCaptcha = this.$cookie.get('_captcha'); | ||
62 | + }, | ||
35 | methods: { | 63 | methods: { |
36 | handleSubmit(name) { | 64 | handleSubmit(name) { |
37 | this.$refs[name].validate((valid) => { | 65 | this.$refs[name].validate((valid) => { |
38 | if (valid) { | 66 | if (valid) { |
39 | - this.login(this.formInline.user, this.formInline.password); | 67 | + if (this.isCaptcha && !this.captcha) { |
68 | + this.$Message.error('请将图形验证码翻转至正确方向'); | ||
69 | + return; | ||
70 | + } | ||
71 | + this.login(this.formInline.user, this.formInline.password, this.captcha); | ||
40 | } else { | 72 | } else { |
41 | this.$Message.error('表单验证失败!'); | 73 | this.$Message.error('表单验证失败!'); |
42 | } | 74 | } |
43 | }); | 75 | }); |
44 | }, | 76 | }, |
45 | - login(username, password) { | 77 | + captchaChange(vals) { |
78 | + this.captcha = _.join(vals, ''); | ||
79 | + }, | ||
80 | + login(username, password, captcha) { | ||
46 | this.loading = true; | 81 | this.loading = true; |
47 | - Vue.passport.local(username, password).then(() => { | 82 | + Vue.passport.local(username, password, captcha).then(() => { |
48 | this.loading = false; | 83 | this.loading = false; |
49 | this.$router.push('/'); | 84 | this.$router.push('/'); |
50 | }, (error) => { | 85 | }, (error) => { |
86 | + this.isCaptcha = error.captcha; | ||
51 | this.loading = false; | 87 | this.loading = false; |
52 | this.$Message.error(error.message); | 88 | this.$Message.error(error.message); |
53 | }); | 89 | }); |
54 | } | 90 | } |
55 | }, | 91 | }, |
56 | - data() { | ||
57 | - return { | ||
58 | - loading: false, | ||
59 | - formInline: { | ||
60 | - user: '', | ||
61 | - password: '' | ||
62 | - }, | ||
63 | - ruleInline: { | ||
64 | - user: [ | ||
65 | - { required: true, message: '请填写用户名', trigger: 'blur' } | ||
66 | - ], | ||
67 | - password: [ | ||
68 | - { required: true, message: '请填写密码', trigger: 'blur' }, | ||
69 | - { type: 'string', min: 6, message: '密码长度不能小于6位', trigger: 'blur' } | ||
70 | - ] | ||
71 | - } | ||
72 | - }; | 92 | + components: { |
93 | + Captcha | ||
73 | } | 94 | } |
74 | }; | 95 | }; |
75 | </script> | 96 | </script> |
@@ -104,10 +125,16 @@ export default { | @@ -104,10 +125,16 @@ export default { | ||
104 | 125 | ||
105 | .login-btn { | 126 | .login-btn { |
106 | text-align: right; | 127 | text-align: right; |
128 | + | ||
129 | + button { | ||
130 | + width: 100%; | ||
131 | + height: 36px; | ||
132 | + font-size: 14px; | ||
133 | + } | ||
107 | } | 134 | } |
108 | 135 | ||
109 | .login-card { | 136 | .login-card { |
110 | - height: 250px; | 137 | + min-height: 250px; |
111 | } | 138 | } |
112 | } | 139 | } |
113 | </style> | 140 | </style> |
@@ -88,8 +88,8 @@ export default { | @@ -88,8 +88,8 @@ export default { | ||
88 | }); | 88 | }); |
89 | 89 | ||
90 | Vue.passport = { | 90 | Vue.passport = { |
91 | - local: (username, password) => { | ||
92 | - return this.userService.login(username, password).then((res) => { | 91 | + local: (username, password, captcha) => { |
92 | + return this.userService.login(username, password, captcha).then((res) => { | ||
93 | if (res.code === 200) { | 93 | if (res.code === 200) { |
94 | return Promise.all([ | 94 | return Promise.all([ |
95 | this.initPurview(Vue, res.data), | 95 | this.initPurview(Vue, res.data), |
@@ -2,8 +2,12 @@ import _ from 'lodash'; | @@ -2,8 +2,12 @@ import _ from 'lodash'; | ||
2 | import Service from '../service'; | 2 | import Service from '../service'; |
3 | 3 | ||
4 | class UserService extends Service { | 4 | class UserService extends Service { |
5 | - login(username, password) { | ||
6 | - return this.post('/login', {username, password}); | 5 | + login(username, password, captcha) { |
6 | + return this.post('/login', { | ||
7 | + username, | ||
8 | + password, | ||
9 | + captcha | ||
10 | + }); | ||
7 | } | 11 | } |
8 | purviews() { | 12 | purviews() { |
9 | return this.post('/erp/getPurview', { | 13 | return this.post('/erp/getPurview', { |
@@ -63,7 +63,7 @@ | @@ -63,7 +63,7 @@ | ||
63 | "request": "^2.81.0", | 63 | "request": "^2.81.0", |
64 | "request-promise": "^4.2.0", | 64 | "request-promise": "^4.2.0", |
65 | "serve-favicon": "^2.4.2", | 65 | "serve-favicon": "^2.4.2", |
66 | - "uuid": "^3.0.1", | 66 | + "uuid": "^3.1.0", |
67 | "vue": "^2.3.4", | 67 | "vue": "^2.3.4", |
68 | "vue-cookie": "^1.1.4", | 68 | "vue-cookie": "^1.1.4", |
69 | "vue-html5-editor": "^1.1.1", | 69 | "vue-html5-editor": "^1.1.1", |
@@ -116,7 +116,7 @@ | @@ -116,7 +116,7 @@ | ||
116 | "html-webpack-plugin": "^2.28.0", | 116 | "html-webpack-plugin": "^2.28.0", |
117 | "husky": "^0.13.3", | 117 | "husky": "^0.13.3", |
118 | "ignore-file-loader": "^1.0.0", | 118 | "ignore-file-loader": "^1.0.0", |
119 | - "node-sass": "^4.5.2", | 119 | + "node-sass": "^4.5.3", |
120 | "nodemon": "^1.11.0", | 120 | "nodemon": "^1.11.0", |
121 | "optimize-css-assets-webpack-plugin": "^1.3.0", | 121 | "optimize-css-assets-webpack-plugin": "^1.3.0", |
122 | "ora": "^1.2.0", | 122 | "ora": "^1.2.0", |
server/common/captcha.json
0 → 100644
This diff could not be displayed because it is too large.
server/controllers/captcha-controller.js
0 → 100644
1 | +/** | ||
2 | + * 用户controller | ||
3 | + * @author: feng.chen<feng.chen@yoho.cn> | ||
4 | + * @date: 2017/04/13 | ||
5 | + */ | ||
6 | +const _ = require('lodash'); | ||
7 | +const uuid = require('uuid'); | ||
8 | +const request = require('request'); | ||
9 | +const Context = require('../framework/context'); | ||
10 | +const captchaData = require('../common/captcha'); | ||
11 | + | ||
12 | +class CaptchaController extends Context { | ||
13 | + constructor() { | ||
14 | + super(); | ||
15 | + } | ||
16 | + captcha(req, res) { | ||
17 | + let random = _.random(0, captchaData.length); | ||
18 | + let captcha = captchaData[random]; | ||
19 | + | ||
20 | + let codeStr = captcha.degrees.reduce((str, rotate) => { | ||
21 | + return str.concat((4 - rotate / 90 % 4) % 4); | ||
22 | + }, ''); | ||
23 | + | ||
24 | + req.session.captcha = codeStr; | ||
25 | + req.session.captchaTimeout = new Date().getTime() + 1000 * 60; | ||
26 | + req.session.captchaSrc = captcha.verifiedGraphicCode; | ||
27 | + return request(`${captcha.verifiedGraphicCode}?imageView2/0/format/jpg/q/70|watermark/2/text/${uuid.v4()}/fontsize/120/dissolve/10`).pipe(res); // eslint-disable-line | ||
28 | + } | ||
29 | + check(req, res, next) { | ||
30 | + let isCaptcha = req.session.isCaptcha; | ||
31 | + | ||
32 | + if (isCaptcha) { | ||
33 | + if (req.body.captcha === req.session.captcha) { | ||
34 | + if (new Date().getTime() > req.session.captchaTimeout) { | ||
35 | + return res.json({ | ||
36 | + code: 400, | ||
37 | + captcha: true, | ||
38 | + expired: true, | ||
39 | + message: '验证码过期' | ||
40 | + }); | ||
41 | + } | ||
42 | + return next(); | ||
43 | + } else { | ||
44 | + return res.json({ | ||
45 | + code: 400, | ||
46 | + captcha: true, | ||
47 | + message: '验证码错误' | ||
48 | + }); | ||
49 | + } | ||
50 | + } | ||
51 | + return next(); | ||
52 | + } | ||
53 | +} | ||
54 | + | ||
55 | +module.exports = CaptchaController; |
@@ -7,20 +7,22 @@ | @@ -7,20 +7,22 @@ | ||
7 | 'use strict'; | 7 | 'use strict'; |
8 | 8 | ||
9 | const Express = require('express'); | 9 | const Express = require('express'); |
10 | -const UserController = require('./user-controller'); | ||
11 | -const FileController = require('./file-controller'); | ||
12 | -const ImportController = require('./import-controller'); | ||
13 | const middleware = require('../framework/middleware'); | 10 | const middleware = require('../framework/middleware'); |
14 | const before = require('../middleware/before'); | 11 | const before = require('../middleware/before'); |
15 | const auth = require('../middleware/auth'); | 12 | const auth = require('../middleware/auth'); |
13 | +const UserController = require('./user-controller'); | ||
14 | +const FileController = require('./file-controller'); | ||
15 | +const ImportController = require('./import-controller'); | ||
16 | +const CaptchaController = require('./captcha-controller'); | ||
16 | 17 | ||
17 | let router = Express.Router(); // eslint-disable-line | 18 | let router = Express.Router(); // eslint-disable-line |
18 | 19 | ||
19 | -router.post('/login', middleware(UserController, 'login')); | 20 | +router.post('/login', middleware(CaptchaController, 'check'), middleware(UserController, 'login')); |
20 | router.post('/logout', middleware(UserController, 'logout')); | 21 | router.post('/logout', middleware(UserController, 'logout')); |
21 | router.post('/switchShop', before, auth, middleware(UserController, 'switchShop')); | 22 | router.post('/switchShop', before, auth, middleware(UserController, 'switchShop')); |
22 | router.post('/upload/image', before, auth, middleware(FileController, 'uploadImage')); | 23 | router.post('/upload/image', before, auth, middleware(FileController, 'uploadImage')); |
23 | router.post('/import', before, auth, middleware(ImportController, 'import')); | 24 | router.post('/import', before, auth, middleware(ImportController, 'import')); |
24 | router.post('/config', middleware(UserController, 'config')); | 25 | router.post('/config', middleware(UserController, 'config')); |
26 | +router.get('/captcha.jpg', middleware(CaptchaController, 'captcha')); | ||
25 | 27 | ||
26 | module.exports = router; | 28 | module.exports = router; |
@@ -49,7 +49,13 @@ class UserController extends Context { | @@ -49,7 +49,13 @@ class UserController extends Context { | ||
49 | } | 49 | } |
50 | }); | 50 | }); |
51 | }, err => { | 51 | }, err => { |
52 | - return res.json(err); | 52 | + req.session.isCaptcha = true; |
53 | + res.cookie('_captcha', true, { | ||
54 | + path: '/' | ||
55 | + }); | ||
56 | + return res.json(Object.assign(err, { | ||
57 | + captcha: true | ||
58 | + })); | ||
53 | }).catch(next); | 59 | }).catch(next); |
54 | } | 60 | } |
55 | 61 | ||
@@ -123,6 +129,8 @@ class UserController extends Context { | @@ -123,6 +129,8 @@ class UserController extends Context { | ||
123 | } | 129 | } |
124 | 130 | ||
125 | syncSession(context, user, sess, currentShop) { | 131 | syncSession(context, user, sess, currentShop) { |
132 | + delete context.req.session.isCaptcha; | ||
133 | + context.res.clearCookie('_captcha'); | ||
126 | context.req.session.USER = user; | 134 | context.req.session.USER = user; |
127 | context.req.session.LOGIN_UID = user.pid; // pid 为用户名 | 135 | context.req.session.LOGIN_UID = user.pid; // pid 为用户名 |
128 | 136 |
@@ -4165,7 +4165,7 @@ node-pre-gyp@^0.6.36: | @@ -4165,7 +4165,7 @@ node-pre-gyp@^0.6.36: | ||
4165 | tar "^2.2.1" | 4165 | tar "^2.2.1" |
4166 | tar-pack "^3.4.0" | 4166 | tar-pack "^3.4.0" |
4167 | 4167 | ||
4168 | -node-sass@^4.5.2: | 4168 | +node-sass@^4.5.3: |
4169 | version "4.5.3" | 4169 | version "4.5.3" |
4170 | resolved "http://npm.yoho.cn/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568" | 4170 | resolved "http://npm.yoho.cn/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568" |
4171 | dependencies: | 4171 | dependencies: |
@@ -6343,7 +6343,7 @@ uuid@^2.0.1, uuid@^2.0.2: | @@ -6343,7 +6343,7 @@ uuid@^2.0.1, uuid@^2.0.2: | ||
6343 | version "2.0.3" | 6343 | version "2.0.3" |
6344 | resolved "http://npm.yoho.cn/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" | 6344 | resolved "http://npm.yoho.cn/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" |
6345 | 6345 | ||
6346 | -uuid@^3.0.0, uuid@^3.0.1: | 6346 | +uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: |
6347 | version "3.1.0" | 6347 | version "3.1.0" |
6348 | resolved "http://npm.yoho.cn/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" | 6348 | resolved "http://npm.yoho.cn/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" |
6349 | 6349 |
-
Please register or login to post a comment