编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

asp.net core3.1+jwt+vue+vuex实现简单登录

wxchong 2024-06-24 19:41:05 开源技术 15 ℃ 0 评论

在《Jwt在asp.net core 3.1中的应用》中我们讲述了如何在ASP.NET core3.1web API中应用JWT,本篇将讲述如何在Vue+Vuex中调用上一篇的实现的Web Api,实现简单的登录。Webpack用于编译和绑定所有文件,页面样式由Bootstrap完成。

我已经将完整代码传到了码云上:

https://gitee.com/hanyixuan_net/asp.netcore_jwt_vue


运行项目

  1. 安装和下载Node.js和npm: https://nodejs.org/en/download/.
  2. 从码云上拷贝项目源代码:https://gitee.com/hanyixuan_net/asp.netcore_jwt_vue
  3. 在项目的根目录(package.json所在的位置),运行npm install来安装所有的npm包
  4. 同样在项目根目录运行npm start来启动应用程序

项目结构说明:

├──src     //项目的源码编写文件

│ ├── _helpers    //帮助类

│ │ └──auth-header.js  //返回带有jwt token的经过授权的header

│ │ └── index.js     //将helpers下的所有文件放在一起,便于其他文件一次性导入

│ │ └── router.js     //路由配置文件

│ ├── _services //逻辑服务层

│ │ └──user.service.js //封装对调用后端api的操作,包括CRUD以及登录,注销等

│ │ └── index.js    //将services下的所有文件放在一起,便于其他文件一次性导入

│ ├── _store // vuex存储文件夹包含所有vuex模块以及与vuex存储相关的所有内容

│ │ ├──alert.module.js     //各类提醒消息的状态存储

│ │ └──authentication.module.js //负责认证部分的状态存储

│ │ └──users.module.js     //用户的状态存储

│ │ └── index.js     //将store下的所有文件放在一起,便于其他文件一次性导入

│ └── app          //App入口文件

│ │ └── App.vue    

│ └── home          

│ │ └── HomePage.vue     //Home页面

│ └── login          

│ │ └──LoginPage.vue     //登录页面

│ └── index.html //项目入口文件

│ └── index.js       //主配置文件    

└──.babelrc //babel配置文件定义了babel用来传输ES6代码的预置

└──package.json //项目依赖包配置文件

└──package-lock.json // npm5 新增文件,优化性能

└──README.md // 说明文档

└──webpack.config.js // webpack配置文件

下面对主要的结构进行简单说明:

/src/_helpers/auth-header.js

authHeader是一个辅助函数,它返回一个HTTP授权标头,其中包含本地存储中当前登录用户的JSON Web令牌(JWT)。如果用户未登录,则返回一个空对象。

authHeader用于使用JWT向服务器api发出经过身份验证的HTTP请求。

export function authHeader() {
    // 返回经过认证带有jwt token的header
    let user = JSON.parse(localStorage.getItem('user'));

    if (user && user.token) {
        return { 'Authorization': 'Bearer ' + user.token };
    } else {
        return {};
    }
}

/src/_helpers/router.js

Vue路由定义了应用程序的所有路由,并包含了一个方法beforeEach,该方法每次都会在更改路由之前执行,以防止未经身份验证的用户访问受限制的路由。

import Vue from 'vue';
import Router from 'vue-router';

import HomePage from '../home/HomePage'
import LoginPage from '../login/LoginPage'

Vue.use(Router);

export const router = new Router({
  mode: 'history',
  routes: [
    { path: '/', component: HomePage },
    { path: '/login', component: LoginPage },
    { path: '*', redirect: '/' }
  ]
});
//beforeEach钩子函数,路由钩子主要是给使用者在路由发生变化时进行一些特殊的处理而定义的
//beforeEach函数有三个参数:
//to: router即将进入的路由对象
//from: 当前导航即将离开的路由
//next: Function, 进行管道中的一个钩子,如果执行完了,则导航的状态就是 confirmed (确认的);否则为false,终止导航。
router.beforeEach((to, from, next) => {
  // 如果没有登录就跳转到登录页面
  const publicPages = ['/login'];
  const authRequired = !publicPages.includes(to.path);
  const loggedIn = localStorage.getItem('user');

  if (authRequired && !loggedIn) {
    return next('/login');
  }

  next();
})

/src/_helpers/index.js

Index.js将所有在helper文件夹下的文件集中在一起,以便仅使用文件夹路径将他们导入到应用程序的其他部分,并允许在单个语句中导入多个helper。

export * from './router';
export * from './auth-header';

/src/_services/user.service.js

user.service.js 封装了所有后端api的调用,包括对用户数据的增删改查操作以及登录和注销操作。服务方法通过顶部的userService对象导出,每种方法的实现位于下面的函数中。

handleResponse 方法中服务检查来自api的http响应是否未401未经授权,如果是未经授权,则将用户自动注。这解决了JWT到期或是由于某些原因不在有效的情况。


import config from 'config';
import { authHeader } from '../_helpers';

export const userService = {
    login,
    logout,
    getAll
};

function login(username, password) {
    const requestOptions = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    };

    return fetch(`${config.apiUrl}/user/authenticate`, requestOptions)
        .then(handleResponse)
        .then(user => {
            // 如果获取到了jwt token,则认证成功
            if (user.token) {
                // 将用户信息和jwt令牌保存到本地存储当中,以便在刷新页面之间保存登录状态
                localStorage.setItem('user', JSON.stringify(user));
            }
            return user;
        });
}
function logout() {
    //将用户从本地存储删除
    localStorage.removeItem('user');
}
//获取所有用户
function getAll() {
    const requestOptions = {
        method: 'GET',
        headers: authHeader()
    };

    return fetch(`${config.apiUrl}/user`, requestOptions).then(handleResponse);
}

function handleResponse(response) {
    return response.text().then(text => {
        const data = text && JSON.parse(text);
        if (!response.ok) {
            //如果是401,则自动登出
            if (response.status === 401) {
                logout();
                location.reload(true);
            }

            const error = (data && data.message) || response.statusText;
            return Promise.reject(error);
        }
        return data;
    });
}

/src/_services/index.js

服务索引文件将所有服务导出分组在一起,以便可以仅使用文件夹路径将其导入应用程序的其他部分,并允许在单个语句中导入多个服务

/src/_services/index.js

服务索引文件将所有服务导出分组在一起,以便可以仅使用文件夹路径将其导入应用程序的其他部分,并允许在单个语句中导入多个服务

/src/_store

vuex存储文件夹包含所有vuex模块以及与vuex存储相关的所有内容。Vuex为应用程序管理集中式状态存储,将更改提交给状态更新部分,并调度操作以执行更复杂的操作,其中包括异步调用和多个更改。

/src/_store/alert.module.js

alert.module.js负责状态存储的各类提醒,如成功或错误提醒,以及清除提醒的操作。

export const alert = {
    namespaced: true,
    state: {
        type: null,
        message: null
    },
    actions: {
        success({ commit }, message) {
            commit('success', message);
        },
        error({ commit }, message) {
            commit('error', message);
        },
        clear({ commit }) {
            commit('clear');
        }
    },
    mutations: {
        success(state, message) {
            state.type = 'alert-success';
            state.message = message;
        },
        error(state, message) {
            state.type = 'alert-danger';
            state.message = message;
        },
        clear(state) {
            state.type = null;
            state.message = null;
        }
    }
}

/src/_store/authentication.module.js

authentication.module.js负责状态存储的认证部分,包含用于登录和登出的操作。

通过检查用户是否保存在本地存储中来设置用户的初始登录状态,这样可以在刷新浏览器和两次啊浏览器会话之间来使用户保持登录状态


import { userService } from '../_services';
import { router } from '../_helpers';

const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
    ? { status: { loggedIn: true }, user }
    : { status: {}, user: null };

export const authentication = {
    namespaced: true,
    state: initialState,
    actions: {
        login({ dispatch, commit }, { username, password }) {
            commit('loginRequest', { username });

            userService.login(username, password)
                .then(
                    user => {
                        commit('loginSuccess', user);
                        router.push('/');
                    },
                    error => {
                        commit('loginFailure', error);
                        dispatch('alert/error', error, { root: true });
                    }
                );
        },
        logout({ commit }) {
            userService.logout();
            commit('logout');
        }
    },
    mutations: {
        loginRequest(state, user) {
            state.status = { loggingIn: true };
            state.user = user;
        },
        loginSuccess(state, user) {
            state.status = { loggedIn: true };
            state.user = user;
        },
        loginFailure(state) {
            state.status = {};
            state.user = null;
        },
        logout(state) {
            state.status = {};
            state.user = null;
        }
    }
}

/src/_store/users.module.js

负责状态存储的用户部分,包括一个用于api获取所有用户的操作。

import { userService } from '../_services';

export const users = {
    namespaced: true,
    state: {
        all: {}
    },
    actions: {
        getAll({ commit }) {
            commit('getAllRequest');

            userService.getAll()
                .then(
                    users => commit('getAllSuccess', users),
                    error => commit('getAllFailure', error)
                );
        }
    },
    mutations: {
        getAllRequest(state) {
            state.all = { loading: true };
        },
        getAllSuccess(state, users) {
            state.all = { items: users };
        },
        getAllFailure(state, error) {
            state.all = { error };
        }
    }
}

/src/_store/index.js

这是主要的vuex存储文件,负责上述所有vuex模块的配置。

import Vue from 'vue';
import Vuex from 'vuex';

import { alert } from './alert.module';
import { authentication } from './authentication.module';
import { users } from './users.module';

Vue.use(Vuex);

export const store = new Vuex.Store({
    modules: {
        alert,
        authentication,
        users
    }
});

/src/app/App.vue

该应用程序组件是vue教程应用程序的根组件,它包含该教程应用程序的外部html,路由器视图和全局警报通知。

<template>
    <div class="jumbotron">
        <div class="container">
            <div class="row">
                <div class="col-sm-6 offset-sm-3">
                    <div v-if="alert.message" :class="`alert ${alert.type}`">{{alert.message}}</div>
                    <router-view></router-view>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name: 'app',
    computed: {
        alert () {
            return this.$store.state.alert
        }
    },
    watch:{
        $route (to, from){
            this.$store.dispatch('alert/clear');
        }
    } 
};
</script>

/src/home/HomePage.vue

homePage是登录后跳转的组件,用来显示已登录用户,和获取到所有用户的列表


<template>
    <div>
        <h1>Hi {{user.firstName}}!</h1>
        <span v-if="users.error" class="text-danger">error: {{users.error}}</span>
        <ul v-if="users.items">
            <li v-for="user in users.items" :key="user.id">
                {{user.firstName + ' ' + user.lastName}}
            </li>
        </ul>
        <p>
            <router-link to="/login">logout</router-link>
        </p>
    </div>
</template>

<script>
export default {
    computed: {
        user () {
            return this.$store.state.authentication.user;
        },
        users () {
            return this.$store.state.users.all;
        }
    },
    created () {
        this.$store.dispatch('users/getAll');
    }
};
</script>

/src/login/LoginPage.vue

LoginPage组件呈现一个包含用户名和密码字段的登录表单。当用户尝试提交表单时,它将显示无效字段的验证消息。如果表单有效,则提交表单将执行“authentication/login”vuex操作。

在created()函数中,调用了“authentication / logout” vuex操作,如果用户已登录,该操作会将用户注销,所以登录页面也可用作注销页面。


<template>
    <div>
        <div class="alert alert-info">
            Username: hanyixuan<br />
            Password: hanyixuan
        </div>
        <h2>Login</h2>
        <form @submit.prevent="handleSubmit">
            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" v-model="username" name="username" class="form-control" :class="{ 'is-invalid': submitted && !username }" />
                <div v-show="submitted && !username" class="invalid-feedback">Username is required</div>
            </div>
            <div class="form-group">
                <label htmlFor="password">Password</label>
                <input type="password" v-model="password" name="password" class="form-control" :class="{ 'is-invalid': submitted && !password }" />
                <div v-show="submitted && !password" class="invalid-feedback">Password is required</div>
            </div>
            <div class="form-group">
                <button class="btn btn-primary" :disabled="loggingIn">login</button>
            </div>
        </form>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                username: '',
                password: '',
                submitted: false
            }
        },
        computed: {
            loggingIn() {
                return this.$store.state.authentication.status.loggingIn;
            }
        },
        created() {
            // reset login status
            this.$store.dispatch('authentication/logout');
        },
        methods: {
            handleSubmit(e) {
                this.submitted = true;
                const { username, password } = this;
                const { dispatch } = this.$store;
                if (username && password) {
                    dispatch('authentication/login', { username, password });
                }
            }
        }
    };
</script>

/src/index.html

Index.html文件包含整个教程应用程序的外部html。当应用程序以npm start启动时,Webpack将所有的vue + vuex代码捆绑到单个javascript文件中,并将其注入到index页面主体中。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" />
    <style>

        a {
            cursor: pointer;
        }
</style>
</head>
<body>
    <div id="app"></div>
</body>
</html>

/src/index.js

index.js通过#app将vue组件元素渲染到上面的index.html的<divid="app"></div>中


import Vue from 'vue';

import { store } from './_store';
import { router } from './_helpers';
import App from './app/App';

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App)
});

/webpack.config.js

Webpack用于编译和捆绑所有项目文件,以便准备将其加载到浏览器中,它借助于在webpack.config.js文件中配置的加载程序和插件来完成此任务。

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    resolve: {
        extensions: ['.js', '.vue']
    },
    module: {
        rules: [
            {
                test: /\.vue?$/,
                exclude: /(node_modules)/,
                use: 'vue-loader'
            },
            {
                test: /\.js?$/,
                exclude: /(node_modules)/,
                use: 'babel-loader'
            }
        ]
    },
    plugins: [new HtmlWebpackPlugin({
        template: './src/index.html'
    })],
    devServer: {
        historyApiFallback: true
    },
    externals: {
        // global app config object
        //http://localhost:1022配置是webpai地址
        config: JSON.stringify({
            apiUrl: 'http://localhost:1022'
        })
    }
}

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表