阶段三:项目开发---搭建项目前后端系统基础架构:任务13:实现基本的登录功能

任务描述

任务名称:

实现基本的登录功能

知识点

了解前端Vue项目的基本执行过程

重  点

构建项目的基本登陆功能

内  容

通过实现项目的基本登录功能,来了解前端Vue项目的基本执行过程,并完成基础环境的准备,包括:工具类、静态资源文件等。

任务指导

通过实现项目的基本登录功能,来了解前端Vue项目的基本执行过程,并完成基础环境的准备,包括:工具类、静态资源文件等:

1、掌握各工具类的作用

2、完成登录的前端设计

3、完成登录的后端设计

4、最终掌握前端Vue项目的开发流程,以及各个文件的功能和作用

任务实现

在上一个阶段任务创建的前端基础框架的基础上,进行本阶段任务的开发。

一、登录功能的前端处理过程

1、导入项目所需的图片和CSS等静态文件

  • 参考代码存放client节点的/opt/code目录下

  • 执行如下命令:
[root@client ~]# cp -r /opt/code/kongguan_web/src/assets/* /root/kongguan_web/src/assets/
  • 将参考代码中的css、icon、images等文件夹或文件直接拷贝到当前创建的新项目的src/assets/目录中

2、完成项目中使用的工具类的编写(此处的代码可由学生独立完成,也可以由老师提供,直接导入到项目中

  • 编写或直接导入工具类文件,所有的工具类都放在src/utils目录下
src/utils/message.js消息弹窗管理
src/utils/request.js用于请求的身份验证
  • 在src/utils/message.js中,定义了消息弹窗管理,代码如下:
import { Message } from "element-ui";
let messageInstance = null;
let mainMessage = function DoneMessage(options) {
  //如果弹窗已存在先关闭
  if (messageInstance) {
    messageInstance.close();
  }
  messageInstance = Message(options);
}
let arr = ['success', 'warning', 'info', 'error'];
arr.forEach(function (type) {
  mainMessage[type] = function (options) {
    if (typeof options === 'string') {
      options = {
        message: options
      };
    }
    options.type = type;
    return mainMessage(options);
  };
});
export const message = mainMessage;
  • 在src/utils/request.js中,定义了拦截器,用于拦截“请求”和“响应”,进行身份验证,代码如下:
import Vue from 'vue'
import axios from 'axios'

Vue.prototype.$ajax = Vue.ajax = axios

axios.defaults.baseURL = process.env.NODE_ENV !== 'production' ? "" : (process.env.VUE_APP_BASE_API)

axios.interceptors.request.use(config => {
  if (localStorage.getItem('Authorization')) {
    config.headers.common['Authorization'] = localStorage.getItem('Authorization');
  } else {
  }
  return config;
}, err => {
  return Promise.reject(err);
})
axios.interceptors.response.use(function (response) {
  // console.log(response);
  // console.log(response.headers.Authorization);
  // 对响应数据做点什么
  return response.data;
}, function (error) {
  if (JSON.stringify(error).indexOf('401') !== -1) { // token失效
    localStorage.removeItem('Authorization')
    Vue.prototype.$message.error("token过期")
    Vue.prototype.$router.push({
      path: '/login',
    })
  } else if (JSON.stringify(error).indexOf('403') !== -1) {
    localStorage.removeItem('Authorization')
    Vue.prototype.$message.error("token无效")
    Vue.prototype.$router.push({
      path: '/login',
    })
  }
  // 对响应错误做点什么
  return Promise.reject(error);
});

export default axios;

3、编写 src/api/login/login.js文件,向服务端发送请求,实现登录

  • 定义login方法,使用POST请求向服务端发送表单数据data,服务端返回的数据包括:登录状态、身份token、权限等信息。
import request from '../../utils/request'

const baseUrl="/api"

/**
 * 用户登录
 */
export function login(data){
  return request({
    url:baseUrl+"/login",
    method:"post",
    data:data
  })
}

4、编写src/store/index.js,用于Token的存储

  • src/store/index.js文件,用于存储token,修改token,并将token存入localStorage
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    // 存储token
    Authorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : '',
  },
  mutations: {
    // 修改token,并将token存入localStorage
    changeLogin (state, user) {
      state.Authorization = user.Authorization;
      localStorage.setItem('Authorization', user.Authorization);
      console.log(state.Authorization);
    }
  }
})

5、创建登录的Vue视图组件Login.vue

  • 登录页面设计:在src/views目录下创建Login目录,然后在Login目录下,创建Login.vue 登录页面
<template>
  <div class="login">
    <div class="top_logo">
    </div>
    <!-- 登录框区域 -->
    <div class="form_box">
      <div style="color: #f0f0f0;">大数据航空案例</div>
      <el-form style="margin-top: 60px" :model="loginForm" status-icon :rules="rules" ref="loginForm">
        <el-row type="flex" justify="left" :gutter="20">
          <el-col :span="3" align="center">
            <img src="../../assets/images/user-icon.png" style="margin-top: 3px"/>
          </el-col>
          <el-col :span="21" align="center">
            <el-form-item prop="account">
              <el-input  class="el-input__inner1" v-model="loginForm.account" placeholder="请输入用户名" maxlength="20"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row type="flex">
            <div class="tableTitle"/>
        </el-row>
        <el-row type="flex" justify="left" style="margin-top: 50px" :gutter="20">
          <el-col :span="3" align="center">
            <img src="../../assets/images/pwd-icon.png" style="margin-top: 3px"/>
          </el-col>
          <el-col :span="21">
            <el-form-item prop="password">
              <el-input type="password" v-model="loginForm.password" placeholder="请输入密码" maxlength="16"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row type="flex">
          <div class="tableTitle"/>
        </el-row>
        <el-form-item align="center" style="margin-top: 100px">
          <el-button class="el-button1" type="primary;" @click="submitForm('loginForm')">登 录</el-button>
        </el-form-item>
      </el-form>
    </div>
    <!-- 底部区域 -->
  </div>
</template>
... 接下页...
  • 初始化表单数据,并定义表单验证规则,例如:如果用户名输入框为空,则提示“请输入登陆账号”
 ... 接上页...
 <script>
  import {mapMutations} from 'vuex';
  import {login} from "@/api/login/login";
  export default {
    data() {
      return {
        loginForm: {
          account: "",
          password: "",
        },
        otherQuery: {},
        rules: {
          account: [
            {required: true, message: "请输入登陆账号", trigger: "blur"}
          ],
          password: [
            {required: true, message: "请输入登陆密码", trigger: "blur"}
          ]
        }
      };
    },
... 接下页...
  • 处理请求的重定向地址,获取请求的地址,登录后,直接跳转到请求的地址

其中getOtherQuery(query)方法是在下面的代码中定义的方法,返回上一次请求的地址,登录成功后,直接跳转到该地址。

... 接上页...
    watch: {
      $route: {
        handler: function (route) {
          const query = route.query
          if (query) {
            this.redirect = query.redirect
            this.otherQuery = this.getOtherQuery(query)
          }
        },
        immediate: true
      }
    },
... 接下页...
  • 提交表单的过程如下:

调用src/api/Login/Login.js中的login方法,向服务端发送请求,登录成功后将用户token和权限保存到本地vuex中,然后跳转到上一次请求的地址。

... 接上页...//
    methods: {
      ...mapMutations(['changeLogin']),
      submitForm(formName) {
        this.$refs[formName].validate(valid => {
          if (valid) {
            login(this.loginForm).then(data => {
              if (data.successful) {
                console.log("登录成功");
                // 将用户token保存到vuex中
                // this.changeLogin({Authorization: data.result.token});
                //localStorage.setItem('account', data.result.name)
                if (data.resultValue.Authorization != null) {
                  localStorage.setItem('Authorization', data.resultValue.Authorization);
                }
                if (data.resultValue.userAuth != null) {
                  localStorage.setItem('userAuth', data.resultValue.userAuth);
                }
                this.$router.push({path: this.redirect || '/', query: this.otherQuery});
                console.log("跳转");
              } else {
                console.log("登录失败");
                this.$message.error("登录失败");
              }
            });
          } else {
            return false;
          }
        });
      },
      //获取请求的地址,登录后,直接跳转到请求的地址
      getOtherQuery(query) {
        return Object.keys(query).reduce((acc, cur) => {
          if (cur !== 'redirect') {
            acc[cur] = query[cur]
          }
          return acc
        }, {})
      }
    }
  };
</script>
... 接下页...
  • 页面样式如下
... 接上页...
<style scoped>
  .login {
    background: url('../../assets/images/loginBg.png') no-repeat;
    background-size: cover;
    height: 100%;
  }
  .top_logo {
    height: 130px;
  }
  .form_box {
    text-align: center;
    width: 550px;
    margin: 0 auto;
  }
  .login-footer {
    width: 100%;
    text-align: center;
    color: #ffffff;
    position:fixed;
    bottom:0;
    margin-bottom: 64px;
    size: 23px;
    padding-top: 100px;
  }
  .el-button1{
    background: #0376bf;
    border-color: #0376bf;
    width: 100%;
    color: #f0f0f0;
    
    height: 62px;
  }
  .tableTitle {
    margin: 0 auto;
    margin-top: 10px;
    width: 550px;
    height: 1px;
    background-color: #d4d4d4;
  }
</style>
<style>
  .el-form-item {
    margin-bottom: 1px;
  }
  .el-form-item__error{
    
    margin-left: 15px;
    margin-top: 20px;
  }
  .el-input__inner {
    background: transparent;
    border: 0;
    color: #f0f0f0;
    
  }
</style>

6、创建登录成功后的主页面的Vue视图组件src/views/Home/Index.vue

  • 由于当前任务是实现登录功能,所以登录成功后的src/views/Home/Index.vue页面只显示简单的内容即可,详细展示会在后续任务中完成。
<template>
    <div style="color: #000000;">大数据航空案例</div>
</template>

<script>
    export default{
    }
</script>

<style>
</style>

7、为了实现页面风格的统一,所以这里还需要创建一个src/views/Layout/Layout.vue和一个src/views/Layout/Header.vue 布局视图组件

除登录页面外,其他显示的页面都嵌套在一个Layout.vue内,且都显示同一个头部内容Header.vue。由于这是在后续任务中完成的工作,所以这里只显示简单的内容即可。

  • src/views/Layout/Layout.vue
<template>
  <div class="main">
    <Header></Header>
    <div class="common-right">
      <router-view/>
    </div>
  </div>
</template>

<script>
    export default{
    }
</script>

<style>
</style>
  • src/views/Layout/Header.vue
<template>
    <div style="color: red;">大数据航空案例</div>
</template>

<script>
    export default{
    }
</script>

<style>
</style>

8、编写src/router/index.js路由文件,当我们访问任何页面时,如果没有身份认证信息,则会跳转到Login.vue

  • 通过vue-router重写路由的push方法,解决相同路径跳转报错的问题
import Vue from 'vue'
import Router from 'vue-router'

const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
    return originalPush.call(this, location).catch(err => err)
}
... 接下文 ...
  • 设置基本路由规则
... 接上文 ...
Vue.use(Router)
/* Layout */
import Layout from '@/views/Layout/Layout'
const router =  new Router({
  base: process.env.BASE_URL,
  mode: 'history',
  routes: [
    {
      path: "/login",
      component: resolve => require(['@/views/Login/Login'], resolve),
      hidden: true,
      meta: {
        auth: true
      }
    },
    {
      path: '/',
      component: Layout, 
      redirect: '/home',
      children: [
        {
          path: 'home',
          component: resolve => require(['@/views/Home/Index'], resolve),
          name: 'home',
          meta: { title: 'home' }
        }
      ]
    },
  ]
})
... 接下文 ...
  • 导航守卫:使用router.beforeEach注册一个全局前置守卫,用于获取Token来判断用户是否登陆,如果没有登录,则跳转到/Login.vue页面
... 接上文 ...
router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next();
  } else {
    let token = localStorage.getItem('Authorization');
    
    if (token === null || token === '') {
      next('/login');
    } else {
      next();
    }
  }
});
export default router

9、编写src/main.js文件,该文件是应用系统入口、主方法

  • 在src/main.js中导入  src/store/index 等文件,有些组件,如ECharts报表组件,在当前任务中没有使用,可以先导入暂不使用
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store/index'
import './assets/css/basic.css'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import './assets/icon/iconfont.css';
import * as echarts from 'echarts';
import axios from 'axios';
import moment from 'moment';
import { message } from './utils/message'
... 略,接下文 ...
  • 定义全局属性,以便可以在项目的任意位置使用
... 略,接上文 ...
Vue.prototype.$echarts = echarts;
Vue.prototype.$axios = axios;
Vue.config.productionTip = false
Vue.use(ElementUI);
Vue.prototype.$message = message;
Vue.prototype.$moment = moment;

new Vue({
  router,
  store,
  render:h =>h(App)
}).$mount('#app')
... 略,接下文 ...
  • 定义全局响应(response)拦截器和请求(request)拦截器
... 略,接上文 ...
//定义一个响应拦截器
axios.interceptors.response.use(function (config) {
  let status = config.code;
  //401,未登录,跳转到登录页
  if (status == 401) {
    console.log(401);
    router.push("/login");
  }
  //403,无权限,跳转到登录页
  if (status == 403) {
    console.log(403);
    router.push("/login");
  }
  return config
})

// request拦截器
axios.interceptors.request.use(config => {
  // 如果想请求可以重复发起,给在请求参数中加allowedRepeat:true  (后续会删除,不会发送给服务端)
  if (!config.data || !config.data.allowedRepeat) { // 如果不允许重复请求,开启拦截
    // todo: 1. 设置拦截 防止重复请求
    // 拦截重复请求(即当前正在进行的相同请求)
    const requestData = getRequestIdentify(config)
    removePending(requestData, true)
    // 使用 cancel token 取消请求 参考:http://www.axios-js.com/zh-cn/docs/#%E6%8B%A6%E6%88%AA%E5%99%A8
    config.cancelToken = new CancelToken((c) => {
      pending[requestData] = c
    })
  } else { // 允许重复请求,不进行拦截
    delete config.data.allowedRepeat // 把自定义的请求参数给删掉,不发送给服务端
  }
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})
... 略,接下文 ...
  • 拦截重复请求
... 略,接上文 ...
// 拦截重复请求
let pending = {}
const CancelToken = axios.CancelToken
// 请求标识;完成请求后也需要执行删除记录,所以添加此参数避免执行无用操作
const removePending = (key, isRequest = false) => {
  if (pending[key] && isRequest) {
    pending[key]('取消重复请求')
  }
  delete pending[key]
}
/**
 * 由于我们请求用了代理 直接代理到测试服务器 因此请求响应拦截器的config.url是一致的,不需要标识值区分
 * 如果请求拦截器和响应拦截器的config.url不一致,就需要一个标识值用来区分判断
 */
const getRequestIdentify = (config) => {
  const url = config.url
  // 返回url及请求参数 post方法请求参数为config.data  get方法请求参数为config.params
  if (config.method === 'post') {
    return encodeURIComponent(config.url + JSON.stringify(config.data))
  }
  return encodeURIComponent(url + JSON.stringify(config.params))
}

10、配置环境变量,在项目根目录下创建 .env.development 文件,内容如下:

NODE_ENV='development'
VUE_APP_TITLE = 'development'
#测试环境,使用此配置文件
#请求前缀
VUE_APP_BASE_API = 'http://localhost:8848/api'

11、修改vue.config.js文件,覆盖文件内容,配置服务器端的IP和访问端口等,文件的完整内容如下:

module.exports = {
    runtimeCompiler: true,
    lintOnSave: process.env.NODE_ENV !== 'production',
    lintOnSave: false,
    productionSourceMap:false,
    chainWebpack(config) {
        config.plugins.delete('prefetch')  
    },
    pwa: {
        iconPaths: {
        }
    },
    devServer: {
        host: "0.0.0.0",
        port: 8089,
        proxy: {
            '/api': {
                target: process.env.VUE_APP_BASE_API,
                pathRewrite:{"^/api":"/"},
                changeOrigin: true,
                ws: false
            },
        }
    }
}

12、运行测试前端Vue程序

  • 进入命令行模式,在项目根目录下执行npm run dev命令运行项目(注意:npm默认使用国外镜像,可能会出现连接失败的问题,如出现连接失败可将npm命令切换成cnpm命令)
[root@client KongGuan-Web]# npm run dev
  • 打开浏览器,可以查看到登录页面

二、登录功能的后端处理过程

1、在后端BigData-KongGuan项目的pom.xml文件中引入 spring-boot-starter-security 包和Redis相关包

  • 当前项目使用SpringBoot的WebSecurityConfig安全组件,并使用Redis保存用户token和权限,并为Redis设置过期时间,在前面搭建后端基础框架的任务中,已经在pom.xml文件引入了相关依赖包,打开pom.xml文件可以查看到以下内容。
<!-- Spring Boot Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

<!-- spring-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

2、编写com/qrsoft/config/WebSecurityConfig.java类,使用SpringBoot的WebSecurity安全组件

  • 使用自定义身份验证组件TokenAuthenticationProvider
    // ... ...
    // ... 略 ...
    
    @Autowired
    private TokenAuthenticationProvider tokenAuthenticationProvider;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        // 使用自定义身份验证组件
        auth.authenticationProvider(tokenAuthenticationProvider);
    }
    
    // ... 略 ...
    // ... ...
  • 在类中添加拦截器,拦截请求,如果非/api/login请求,则执行TokenLoginFilter过滤器进行登录验证,执行TokenAuthenticationFilter进行身份验证。
@Override
protected void configure(HttpSecurity http) throws Exception {/
    // ... ...
    // ... 略 ...
    
	// 添加拦截器
	http.addFilterBefore(new TokenLoginFilter("/api/login", authenticationManager(), redisTemplate), UsernamePasswordAuthenticationFilter.class).addFilterBefore(new TokenAuthenticationFilter(userMapper, redisTemplate), UsernamePasswordAuthenticationFilter.class);
   
    // ... 略 ...
    // ... ...
}
  • com/qrsoft/config/WebSecurityConfig.java类的完整代码如下:
package com.qrsoft.config;

import com.google.gson.Gson;
import com.qrsoft.common.WrappedResult;
import com.qrsoft.filter.TokenAuthenticationFilter;
import com.qrsoft.filter.TokenAuthenticationProvider;
import com.qrsoft.filter.TokenLoginFilter;
import com.qrsoft.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletResponse;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private SysUserMapper userMapper;
	@Autowired
	private StringRedisTemplate redisTemplate;
	@Autowired
	private TokenAuthenticationProvider tokenAuthenticationProvider;
	@Override
	protected void configure(AuthenticationManagerBuilder auth) {
		// 使用自定义身份验证组件
		auth.authenticationProvider(tokenAuthenticationProvider);
	}
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 配置 CSRF 关闭,允许跨域访问
		http.csrf().disable();
		// 开启Spring Security cors支持,允许跨域访问
		http.cors();
		// 关闭 Session
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		// 允许 登录接口 的无授权访问,其他需要授权访问
		http.authorizeRequests().antMatchers("/*").permitAll().anyRequest().authenticated();
		// 禁用缓存
		http.headers().cacheControl();
		// 添加拦截器
		http.addFilterBefore(new TokenLoginFilter("/api/login", authenticationManager(), redisTemplate), UsernamePasswordAuthenticationFilter.class).addFilterBefore(new TokenAuthenticationFilter(userMapper, redisTemplate), UsernamePasswordAuthenticationFilter.class);
		// 指定错误未授权访问的处理类
		http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
			response.setContentType("application/json");
			response.setCharacterEncoding("UTF-8");
			response.setStatus(HttpServletResponse.SC_FORBIDDEN);
			response.getWriter().print(new Gson().toJson(WrappedResult.failedWrappedResult(accessDeniedException.getMessage(), "403")));
		});
	}
	@Override
	public void configure(WebSecurity web) {
		web.ignoring().antMatchers("/doc.html/**", "/swagger-ui.html/**", "/v2/**", "/swagger-resources/**", "/webjars/**", "/minio/**");
	}
}

3、编写com/qrsoft/filter下面的类,TokenLoginFilter是登录过滤器,除此之外,还会用到TokenAuthenticationFilter身份认证过滤器和TokenAuthenticationProvider用来处理认证实体。

  • 登录过程包括4个部分(按①②③④的顺序执行),其中com/qrsoft/filter/TokenLoginFilter.java类是登录的核心过滤器,包括①④两个部分:
package com.qrsoft.filter;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.google.gson.Gson;
import com.qrsoft.common.TokenVO;
import com.qrsoft.common.WrappedResult;
import com.qrsoft.entity.SysUser;
import com.qrsoft.util.TokenUtil;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TokenLoginFilter extends AbstractAuthenticationProcessingFilter {
    private final StringRedisTemplate redisTemplate;

    public TokenLoginFilter(String url, AuthenticationManager authManager, StringRedisTemplate redisTemplate) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
        this.redisTemplate = redisTemplate;
    }

    /**
     * 尝试认证(从request中取用户名和密码,生成认证实体)
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
        return null;
        //① ...
    }

    /**
     * 认证成功返回统一格式的JSON
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain
            chain, Authentication auth) throws IOException {
        //④...
    }

    /**
     * 认证失败返回统一格式的JSON
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse
            response, AuthenticationException failed) throws IOException {
        //④ ...
    }
}
  • com/qrsoft/filter/TokenAuthenticationProvider.java类是用来处理认证实体,包括②③两个部分
package com.qrsoft.filter;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.qrsoft.entity.SysAuth;
import com.qrsoft.entity.SysUser;
import com.qrsoft.mapper.SysAuthMapper;
import com.qrsoft.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Component
public class TokenAuthenticationProvider implements AuthenticationProvider {

    private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    @Autowired
    private SysUserMapper userMapper;
    @Autowired
    private SysAuthMapper authMapper;

    /**
     * 身份认证
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return null;
        //③ ... 
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return false;
        //② ... 
    }

    /**
     * 获取用户权限
     */
    private List<GrantedAuthority> getGrantedAuthorities(SysUser sysUser) {
        List<SysAuth> authList = authMapper.getAuthByUserId(sysUser.getId());
        //先过滤,在转换
        Set<String> perms = authList.stream().map(SysAuth::getAuthCode).filter(StringUtils::isNotBlank).collect(Collectors.toSet());
        return AuthorityUtils.createAuthorityList(perms.toArray(new String[0]));
    }
}
  • 在TokenLoginFilter类中的 ① 部分的代码如下(请在TokenLoginFilter类中的相应位置替换为如下代码):
	/**
	 * 尝试认证(从request中取用户名和密码,生成认证实体)
	 */
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { //①
		String account = request.getParameter("account");
		String password = request.getParameter("password");

		if (StringUtils.isBlank(account) && StringUtils.isBlank(password)) {
			InputStreamReader streamReader = new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8);
			BufferedReader reader = new BufferedReader(streamReader);
			StringBuilder builder = new StringBuilder();
			String inputStr;
			while ((inputStr = reader.readLine()) != null)
				builder.append(inputStr);
			JSONObject jsonObject = JSONObject.parseObject(builder.toString());
			account = jsonObject.getString("account");
			password = jsonObject.getString("password");
			streamReader.close();
			reader.close();
		}
		if (account.contains(" ")) throw new BadCredentialsException("用户名或密码错误");
		// 返回一个验证令牌
		return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(account, password));
	}
  • 在TokenAuthenticationProvider类中的 ② 部分的代码如下(请在TokenAuthenticationProvider类中的相应位置替换为如下代码):
   @Override
	public boolean supports(Class<?> authentication) {//②
		return authentication.equals(UsernamePasswordAuthenticationToken.class);
	}
  • 在TokenAuthenticationProvider类中的 ③ 部分代码如下(请在TokenAuthenticationProvider类中的相应位置替换为如下代码):
	/**
	 * 身份认证
	 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {//③
		if (authentication.getPrincipal() == null || authentication.getCredentials() == null)
			throw new BadCredentialsException("用户名或密码错误");
		// 获取认证的用户名 & 密码
		String account = authentication.getPrincipal().toString();
		String password = authentication.getCredentials().toString();
		SysUser sysUser = userMapper.getByAccount(account);
		if (sysUser != null) {
			// 密码不匹配直接抛出异常
			if (!passwordEncoder.matches(password, sysUser.getPassword()))
				throw new BadCredentialsException("用户名或密码错误");
			// 获取用户权限
			List<GrantedAuthority> authorities = getGrantedAuthorities(sysUser);
			UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(sysUser.getAccount(), sysUser.getPassword(), authorities);
			token.setDetails(sysUser);
			return token;
		} else {
			throw new UsernameNotFoundException("用户不存在或已删除");
		}
	}
  • 在TokenLoginFilter类中的 ④ 部分的代码如下(请在TokenLoginFilter类中的相应位置替换为如下代码):
/**
* 认证成功返回统一格式的JSON
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException {//④
	response.setContentType("application/json");
	response.setStatus(HttpServletResponse.SC_OK);
	response.setCharacterEncoding("UTF-8");
	String token;
	//生成token
	try {
		Map<String, Object> payload = new HashMap<>();
		payload.put("account", auth.getName());
		token = TokenUtil.genToken(payload);
	} catch (Exception e) {
		response.getWriter().print(new Gson().toJson("Token生成失败"));
		return;
	}
	if (auth.getDetails() instanceof SysUser) {
		SysUser sysUser = (SysUser) auth.getDetails();
		TokenVO tokenVO = new TokenVO();
		tokenVO.setAccount(auth.getName());
		Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
		List<String> authorityList = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
		tokenVO.setName(sysUser.getName());
		tokenVO.setId(sysUser.getId().toString());
		tokenVO.setType(sysUser.getType());
		Boolean userboo = redisTemplate.hasKey(sysUser.getId().toString());
		if (userboo != null && userboo) {
			String tok = redisTemplate.boundValueOps(sysUser.getId().toString()).get();
			if (tok != null) {
				Boolean tokboo = redisTemplate.hasKey(tok);
				if (tokboo != null && tokboo) {
					redisTemplate.delete(tok);
				}
			}
			redisTemplate.delete(sysUser.getId().toString());
		}
		//存储用户对应token
		redisTemplate.boundValueOps(sysUser.getId().toString()).set(token);
		//存储token对应权限
		BoundSetOperations<String, String> setOperations = redisTemplate.boundSetOps(token);
		setOperations.add(authorityList.toArray(new String[]{}));
		response.setHeader("Authorization", token);
		response.setHeader("userAuth",authorityList.toString());
		tokenVO.setAuthorization(token);
		tokenVO.setUserAuth(authorityList.toString());
		response.getWriter().print(new Gson().toJson(WrappedResult.successWrapedResult(tokenVO)));
	} else {
		response.setContentType("application/json");
		response.setStatus(HttpServletResponse.SC_OK);
		response.setCharacterEncoding("UTF-8");
		response.getWriter().print(new Gson().toJson(WrappedResult.failedWrappedResult("登录失败")));
	}
}
/**
* 认证失败返回统一格式的JSON
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {//④
	response.setContentType("application/json");
	response.setStatus(HttpServletResponse.SC_OK);
	response.setCharacterEncoding("UTF-8");
	response.getWriter().print(new Gson().toJson(WrappedResult.failedWrappedResult(failed.getMessage())));
}
  • com/qrsoft/filter/TokenAuthenticationFilter.java类是用于身份认证的过滤器,其完整代码如下:
package com.qrsoft.filter;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.google.gson.Gson;
import com.qrsoft.common.R;
import com.qrsoft.common.WrappedResult;
import com.qrsoft.entity.SysUser;
import com.qrsoft.mapper.SysUserMapper;
import com.qrsoft.util.TokenUtil;
import org.apache.commons.collections.MapUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;

/**
 * 身份认证过滤器
 */
public class TokenAuthenticationFilter extends GenericFilterBean {
    private final SysUserMapper userMapper;
    private final StringRedisTemplate redisTemplate;

    public TokenAuthenticationFilter(SysUserMapper sysUserDao, StringRedisTemplate redisTemplate) {
        this.userMapper = sysUserDao;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("权限信息打印");
        // 从Http头中取token
        String token = ((HttpServletRequest) request).getHeader("Authorization");
        if (token != null) {
            // 校验token,并获取token中存储的用户名
            R r = TokenUtil.valid(token);
            // 验证redis是否存在token
            Boolean tokenBoo = redisTemplate.hasKey(token);
            if (tokenBoo != null && tokenBoo && r.isSuccess()) {
                // 从token检验结果获取用户名
                String account = MapUtils.getString(r.getPayloadMap(), "account", StringUtils.EMPTY);
                // 根据token获取redis中权限set
                Set<String> authoritySet = redisTemplate.boundSetOps(token).members();
                SysUser sysUser = userMapper.getByAccount(account);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(sysUser.getAccount(), sysUser.getPassword(), AuthorityUtils.createAuthorityList(authoritySet.toArray(new String[]{})));
                authentication.setDetails(sysUser);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                filterChain.doFilter(request, response);
            } else {
                //token验证不通过
                writeResponse((HttpServletResponse) response, "Token失效");
            }
        } else {
            // token不存在放回401(未登录)
            writeResponse((HttpServletResponse) response, "未登录");
        }
    }

    private void writeResponse(HttpServletResponse response, String str) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().print(new Gson().toJson(WrappedResult.failedWrappedResult(str, String.valueOf(HttpServletResponse.SC_UNAUTHORIZED))));
    }
}

4、由于com.qrsoft.entity、com.qrsoft.mapper、com.qrsoft.common、com.qrsoft.util包下的帮助类不是当前项目的重点,所以请参考相应的源代码,自行完成或直接拷贝这些通用类即可。主要使用到以下的类:

类/接口

功能

com.qrsoft.mapper.SysUserMapper用户基础信息表对应的数据访问类
com.qrsoft.mapper.SysAuthMapper权限表对应的数据访问类
com.qrsoft.common.R通用类,定义了返回结果
com.qrsoft.common.WrappedResult通用类,定义了返回结果的接口规范
com.qrsoft.common.TokenVO通用类,定义了Token的内容
com.qrsoft.common.AuthAndMenu通用类,定义了权限和菜单
com.qrsoft.entity.SysUser用户基础信息表对应的数据实体类
com.qrsoft.entity.SysAuth权限表对应的数据实体类
com.qrsoft.util.TokenUtilToken工具类,用于生成Token、验证Token有效性等
com.qrsoft.config.MybatisPlusConfigMybatisPlus的配置类
  • com.qrsoft.mapper.SysUserMapper类:用户基础信息表对应的数据访问类
package com.qrsoft.mapper;

import com.qrsoft.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface SysUserMapper {
	@Select("select id,account,password,name,is_enable,type,user_type_id " +
			"from sys_user where account = #{account} and is_del = 0")
	SysUser getByAccount(String account);
}
  • com.qrsoft.mapper.SysAuthMapper类:权限表对应的数据访问类
package com.qrsoft.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qrsoft.entity.SysAuth;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@Mapper
public interface SysAuthMapper extends BaseMapper<SysAuth> {

	/**
	 * 根据用户id获取按钮权限
	 */
	@Select("select sa.id,sa.auth_name,sa.auth_code,sa.parent_id from sys_auth sa left join role_auth ra on sa.id = ra.auth_id where " +
			"sa.is_del = 0 and sa.type = 0 and ra.role_id in " +
			"(select sr.id from sys_role sr join user_role ur on sr.id = ur.role_id where sr.is_del = 0 and sr.is_enable = 0 and ur.user_id = #{userId})" +
			" order by sa.parent_id")
	List<SysAuth> getAuthByUserId(Integer userId);

	/**
	 * 根据用户id获取菜单权限
	 */
	@Select("select sa.id,sa.auth_name,sa.auth_code,sa.parent_id,sa.menu_url,sa.menu_icon,sa.menu_order from sys_auth sa left join role_auth ra on sa.id = ra.auth_id where " +
			"sa.is_del = 0 and sa.type = 1 and ra.role_id in " +
			"(select sr.id from sys_role sr join user_role ur on sr.id = ur.role_id where sr.is_del = 0 and sr.is_enable = 0 and ur.user_id = #{userId})" +
			" group by sa.id order by sa.menu_order")
	List<SysAuth> getMenuByUserId(Integer userId);

	/**
	 * 根据权限id获取权限详情
	 */
	@Select("select id,auth_name,auth_code,type,menu_url,parent_id,menu_icon,menu_order from sys_auth where id = #{id} and is_del=0")
	SysAuth getOneAuth(Integer id);

	/**
	 * 根据权限标识统计权限数量
	 */
	@Select("select count(1) from sys_auth where auth_code = #{authCode} and is_del=0")
	Integer countAuthCode(String authCode);

	/**
	 * 权限取消全部角色
	 */
	@Delete("delete from role_auth where auth_id = #{authId}")
	void deleteRole(Integer authId);

	/**
	 * 删除权限
	 */
	@Update("update sys_auth set is_del = 1 where id = #{id} and is_del = 0")
	Boolean delAuth(Integer id);

	@Select("select DISTINCT sa.id,sa.auth_name,sa.auth_code,sa.parent_id,sa.type,sa.menu_icon,sa.menu_order from sys_auth sa left join role_auth ra on sa.id = ra.auth_id where " +
			"sa.is_del = 0 and ra.role_id = #{roleId} order by sa.parent_id")
	List<SysAuth> getAuthByRole(Integer roleId);

	/**
	 * 根据角色获取权限id
	 */
	@Select("select auth_id from role_auth where role_id in (${roleId})")
	List<Integer> getIdByRole(String roleId);
}
  • com.qrsoft.common.R类:通用类,定义了返回结果
package com.qrsoft.common;

import java.util.HashMap;
import java.util.Map;

public class R {
	private final boolean success;
	private final String msg;
	private final Map<String, Object> payloadMap;

	private R(boolean success, String msg, Map<String, Object> payloadMap) {
		this.success = success;
		this.msg = msg;
		this.payloadMap = payloadMap;
	}
	public static R ok(Map<String, Object> payloadMap) {
		return new R(true, "", payloadMap);
	}
	public static R error(String msg) {
		return new R(false, msg, new HashMap<>());
	}
	public boolean isSuccess() {
		return success;
	}
	public Map<String, Object> getPayloadMap() {
		return payloadMap;
	}
	@Override
	public String toString() {
		return "R{" +
				"success=" + success +
				", msg='" + msg + '\'' +
				", payloadMap=" + payloadMap +
				'}';
	}
}
  • com.qrsoft.common.WrappedResult类:通用类,定义了返回结果的接口规范
package com.qrsoft.common;

import io.swagger.annotations.ApiModelProperty;
import org.springframework.security.access.AccessDeniedException;
import java.io.Serializable;

public class WrappedResult<T> implements Serializable {

	private static final long serialVersionUID = 1L;
	private static final String ERROR = "error";
	private static final String SUCCESS = "success";
	/**
	 * 接口状态
	 */
	@ApiModelProperty("接口状态,true成功,false失败")
	private final boolean successful;
	/**
	 * 接口返回数据
	 */
	@ApiModelProperty("接口成功返回数据主体")
	private final T resultValue;
	/**
	 * 接口错误信息
	 */
	@ApiModelProperty("接口错误信息")
	private final String resultHint;
	/**
	 * 接口返回状态
	 * 失败:401,403,error
	 * 成功:success
	 */
	@ApiModelProperty("接口返回状态,成功:success,失败:401,403,error")
	private final String type;

	private WrappedResult(boolean isSuccess, T data, String resultHint, String type) {
		this.successful = isSuccess;
		this.resultValue = data;
		this.resultHint = resultHint;
		this.type = type;
	}
	public static <T> WrappedResult<T> successWrapedResult(T data) {
		return new WrappedResult<>(true, data, "", SUCCESS);
	}
	public static <T> WrappedResult<T> failedWrappedResult(String exMessage) {
		return new WrappedResult<>(false, null, exMessage, ERROR);
	}
	public static WrappedResult<Boolean> failedWrappedResult(Exception e, Boolean data) {
		if ("java.lang.RuntimeException".equals(e.getClass().getName())) {
			return new WrappedResult<>(false, data, e.getMessage(), ERROR);
		} else if ("org.springframework.security.access.AccessDeniedException".equals(e.getClass().getName())) {
			throw new AccessDeniedException(e.getMessage());
		} else {
			return new WrappedResult<>(false, data, "操作失败", ERROR);
		}
	}
	public static <T> WrappedResult<T> failedWrappedResult(Exception e) {
		if ("java.lang.RuntimeException".equals(e.getClass().getName())) {
			return new WrappedResult<>(false, null, e.getMessage(), ERROR);
		} else if ("org.springframework.security.access.AccessDeniedException".equals(e.getClass().getName())) {
			throw new AccessDeniedException(e.getMessage());
		} else {
			return new WrappedResult<>(false, null, "操作失败", ERROR);
		}
	}
	public static <T> WrappedResult<T> failedWrappedResult(Exception e, String exMessage) {
		if ("java.lang.RuntimeException".equals(e.getClass().getName())) {
			return new WrappedResult<>(false, null, e.getMessage(), ERROR);
		} else if ("org.springframework.security.access.AccessDeniedException".equals(e.getClass().getName())) {
			throw new AccessDeniedException(e.getMessage());
		} else {
			return new WrappedResult<>(false, null, exMessage, ERROR);
		}
	}
	public static <T> WrappedResult<T> failedWrappedResult(String exMessage, String type) {
		return new WrappedResult<>(false, null, exMessage, type);
	}
	public boolean isSuccessful() {
		return this.successful;
	}
	public T getResultValue() {
		return this.resultValue;
	}
	public String getType() {
		return this.type;
	}
	public String getResultHint() {
		return resultHint;
	}
}
  • com.qrsoft.common.TokenVO类:通用类,定义了Token的内容
package com.qrsoft.common;

import lombok.Data;

import java.io.Serializable;
import java.util.List;

@Data
public class TokenVO implements Serializable {

	private static final long serialVersionUID = -5501706435587205188L;

	/**
	 * token
	 */
	private String token;

	/**
	 * 用户ID
	 */
	private String id;

	/**
	 * 姓名
	 */
	private String name;

	/**
	 * 用户名
	 */
	private String account;

	/**
	 * 用户类型
	 */
	private Integer type;

	/**
	 * 按钮权限列表
	 */
	private List<AuthAndMenu> authList;

	/**
	 * 菜单权限列表
	 */
	private List<AuthAndMenu> menuList;

	private String Authorization;

	private String userAuth;
}
  • com.qrsoft.common.AuthAndMenu类: 通用类,定义了权限和菜单
package com.qrsoft.common;

import com.qrsoft.entity.SysAuth;
import lombok.Data;

import java.util.List;

@Data
public class AuthAndMenu {

	/**
	 * id
	 */
	private Integer id;

	/**
	 * 权限名称
	 */
	private String authName;

	/**
	 * 权限编码
	 */
	private String authCode;

	/**
	 * 权限类型(0:按钮;1,菜单)
	 */
	private Integer type;

	/**
	 * 菜单Url
	 */
	private String menuUrl;

	/**
	 * 菜单Icon
	 */
	private String menuIcon;

	/**
	 * 菜单order
	 */
	private Integer menuOrder;

	/**
	 *  子菜单
	 */
	private List<AuthAndMenu> childs;

	/**
	 * 父级id
	 */
	private Integer parentId;

	public static AuthAndMenu authTOAuth(SysAuth auth) {
		AuthAndMenu output = new AuthAndMenu();
		output.setId(auth.getId());
		output.setAuthName(auth.getAuthName());
		output.setAuthCode(auth.getAuthCode());
		output.setType(0);
		output.setParentId(auth.getParentId());
		output.setMenuUrl(auth.getMenuUrl());
		output.setMenuIcon(auth.getMenuIcon());
		output.setMenuOrder(auth.getMenuOrder());
		return output;
	}

	public static AuthAndMenu authTOMenu(SysAuth auth) {
		AuthAndMenu output = new AuthAndMenu();
		output.setId(auth.getId());
		output.setAuthName(auth.getAuthName());
		output.setType(1);
		output.setParentId(auth.getParentId());
		output.setMenuUrl(auth.getMenuUrl());
		output.setMenuIcon(auth.getMenuIcon());
		output.setMenuOrder(auth.getMenuOrder());
		return output;
	}
}
  • com.qrsoft.entity.SysUser类:用户基础信息表对应的数据实体类
package com.qrsoft.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 用户基础
 */
@Data
@TableName("sys_user")
public class SysUser implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * 主键id
	 */
	@TableId(value = "id", type = IdType.AUTO)
	private Integer id;

	/**
	 * 用户账号
	 */
	@TableField("account")
	private String account;

	/**
	 * 用户密码
	 */
	@TableField("password")
	private String password;

	/**
	 * 用户姓名
	 */
	@TableField("name")
	private String name;

	/**
	 * 联系方式
	 */
	@TableField("contact")
	private String contact;

	/**
	 * 用户类型(0:质检员(管理员);1:司机;2:客户)
	 */
	@TableField("type")
	private Integer type = 0;

	/**
	 * 司机id/客户id
	 */
	@TableField("user_type_id")
	private Integer userTypeId;

	/**
	 * 是否启用(启用:0,未启用:1)
	 */
	@TableField("is_enable")
	private Integer isEnable = 0;

	/**
	 * 是否删除(正常:0,删除:1)
	 */
	@TableField("is_del")
	private Integer isDel = 0;

	/**
	 * 创建人
	 */
	@TableField(value = "create_user", select = false)
	private Integer createUser;

	/**
	 * 创建时间
	 */
	@TableField(value = "create_time", select = false)
	private Date createTime;
}
  • com.qrsoft.entity.SysAuth类:权限表对应的数据实体类
package com.qrsoft.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 权限表
 */
@Data
@TableName("sys_auth")
public class SysAuth implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * 主键id
	 */
	@TableId(value = "id", type = IdType.AUTO)
	private Integer id;

	/**
	 * 权限名称
	 */
	@TableField("auth_name")
	private String authName;

	/**
	 * 权限编码
	 */
	@TableField("auth_code")
	private String authCode;

	/**
	 * 权限类型(0:按钮;1,菜单)
	 */
	@TableField("type")
	private Integer type = 0;

	/**
	 * 菜单Url
	 */
	@TableField("menu_url")
	private String menuUrl;

	/**
	 * 父级id
	 */
	@TableField("parent_id")
	private Integer parentId;

	/**
	 * 菜单图标
	 */
	@TableField("menu_icon")
	private String menuIcon;

	/**
	 * 菜单顺序
	 */
	@TableField("menu_order")
	private Integer menuOrder;

	/**
	 * 删除状态,0正常,1删除
	 */
	@TableField("is_del")
	private Integer isDel = 0;

	/**
	 * 创建人
	 */
	@TableField(value = "create_user", select = false)
	private Integer createUser;

	/**
	 * 创建时间
	 */
	@TableField(value = "create_time", select = false)
	private Date createTime;
}
  • com.qrsoft.util.TokenUtil类:Token工具类,用于生成Token、验证Token有效性等
package com.qrsoft.util;

import com.qrsoft.common.R;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import java.util.Calendar;
import java.util.Map;

/**
 * token工具类
 */
public class TokenUtil {

	private TokenUtil() {
	}

	/**
	 * 密钥(至少32字节,后续可根据需求换成RSA加密)
	 */
	private static final byte[] SECRET = "sarnath-sarnath-sarnath-sarnath.".getBytes();
	/**
	 * token失效时间(分)
	 */
	public static final int EXP_TIME = 24 * 60;

	/**
	 * 生成token
	 */
	public static String genToken(Map<String, Object> payloadMap) {
		// JwtBuilder的base64UrlEncoder默认Encoders.BASE64URL
		JwtBuilder jwtBuilder = Jwts.builder();
		// 设置载荷
		jwtBuilder.setClaims(payloadMap);
		// 设置失效时间
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.MINUTE, EXP_TIME);
		jwtBuilder.setExpiration(calendar.getTime());
		// 设置签名
		jwtBuilder.signWith(Keys.hmacShaKeyFor(SECRET));
		return jwtBuilder.compact();
	}

	/**
	 * 验证token有效性
	 */
	public static R valid(String token) {
		try {
			JwtParser jwtParser = Jwts.parser();
			// 设置时钟偏移
			jwtParser.setAllowedClockSkewSeconds(3 * 60);
			// 设置签名
			jwtParser.setSigningKey(SECRET);
			Map<String, Object> payload = jwtParser.parseClaimsJws(token).getBody();
			return R.ok(payload);
		} catch (Exception e) {
			return R.error(e.getMessage());
		}
	}
}
  • com.qrsoft.config.MyBatisPlusConfig类:MyBatisPlus的配置类
package com.qrsoft.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {

	@Bean
	public PaginationInterceptor paginationInterceptor() {
		PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
		paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
		return paginationInterceptor;
	}
}

5、因为需要访问MySQL数据库,所以在resources目录下打开application.yml文件(在前面步骤中已经创建),并填写如下配置:

server:
  port: 8848
spring:
  datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root    
      password: 123456    
      url: jdbc:mysql://node3:3306/kongguan?autoReconnect=true&autoReconnectForPools=true&failOverReadOnly=false&serverTimezone=UTC    
  redis:
      host: node3    
      port: 6379    
      database: 15

注意:需要确保node3节点上的Redis和MySQL都已经正常启动,可以参照前面安装部署的任务中的步骤进行验证。

6、项目启动类BigDataKongGuanApplication 的内容如下:

package com.qrsoft;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class BigDataKongGuanApplication {
    public static ConfigurableApplicationContext appConfig;
    public static void main(String[] args) {
        appConfig=SpringApplication.run(BigDataKongGuanApplication.class, args);
    }
}

三、测试登录功能

1、启动后端Spring Boot程序。

2、启动前端VUE程序。

3、输入用户名和密码(例如:admin/admin)进行登录。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/780445.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前端面试题17(js快速检索方法详解)

在前端JavaScript中&#xff0c;快速检索数据通常涉及到数组或对象的搜索。这里我会介绍几种常见的快速检索方法&#xff0c;并提供相应的代码示例。 1. 数组的find和findIndex方法 find: 返回数组中满足条件的第一个元素的值。findIndex: 返回数组中满足条件的第一个元素的索…

基于LSTM的股票价格预测

摘要 本课设旨在利用LSTM&#xff08;长短期记忆&#xff09;网络实现股票价格预测&#xff0c;通过收集、预处理股票数据集&#xff0c;并构建预测模型进行训练与优化。实验结果显示&#xff0c;经过优化调整模型参数&#xff0c;模型在测试集上取得了较为理想的预测效果。尽…

《征服数据结构》SparseArray

摘要&#xff1a; 1&#xff0c;SparseArray的介绍 2&#xff0c;SparseArray的代码实现 1&#xff0c;SparseArray的介绍 前面我们讲过《ArrayMap》&#xff0c;用它来实现哈希表&#xff0c;其中存放key和value的数组长度是存放散列表数组长度的二倍。 在哈希表中如果key值是…

SwiftData 模型对象的多个实例在 SwiftUI 中不能及时同步的解决

概览 我们已经知道,用 CoreData 在背后默默支持的 SwiftUI 视图在使用 @FetchRequest 来查询托管对象集合时,若查询结果中的托管对象在别处被改变将不会在 FetchedResults 中得到及时的刷新。 那么这一“囧境”在 SwiftData 里是否也会“卷土重来”呢?空说无益,就让我们在…

【项目设计】负载均衡式——Online Judge

负载均衡式——Online Judge&#x1f60e; 前言&#x1f64c;Online Judge 项目一、项目介绍二、项目技术栈三、项目使用环境四、项目宏观框架五、项目后端服务实现过程1、comm模块设计1.1 Log.hpp实现1.2 Util.hpp实现 2、compiler_server 模块设计2.1compile.hpp文件代码编写…

vb.netcad二开自学笔记2:认识vs编辑器

认识一下宇宙第一编辑器的界面图标含义还是很重要的&#xff0c;否则都不知道面对的是什么还怎么继续&#xff1f; 一、VS编辑器中常见的图标的含义 变量 长方体&#xff1a;变量 局部变量 两个矩形块&#xff1a;枚举 预定义的枚举 紫色立方体&#xff1a;方法 橙色树状结构…

通过AIS实现船舶追踪与照射

前些天突然接到个紧急的项目&#xff1a;某处需要实现对夜航船只进行追踪并用激光灯照射以保障夜航安全。这个项目紧急到什么程度呢&#xff1f;&#xff01;现场激光灯都安装好了&#xff0c;还有三个星期就要验收了&#xff0c;但上家没搞定就甩给我们了:( 从技术上看&#…

Java -- 实现MD5加密/加盐

目录 1. 加密的引出2. MD5介绍3. 解决MD5不可解密方法4. 实现加密解密4.1 加密4.2 验证密码 1. 加密的引出 在MySQL数据库中&#xff0c;一般都需要把密码、身份证、电话号码等信息进行加密&#xff0c;以确保数据的安全性。如果使用明文来存储&#xff0c;当数据库被入侵的时…

力扣考研经典题 反转链表

核心思想 头插法&#xff1a; 不断的将cur指针所指向的节点放到头节点之前&#xff0c;然后头节点指向cur节点&#xff0c;因为最后返回的是head.next 。 解题思路 1.如果头节点是空的&#xff0c;或者是只有一个节点&#xff0c;只需要返回head节点即可。 if (head null …

Vatee万腾平台:创新科技,驱动未来

在科技日新月异的今天&#xff0c;每一个创新的火花都可能成为推动社会进步的重要力量。Vatee万腾平台&#xff0c;作为科技创新领域的佼佼者&#xff0c;正以其卓越的技术实力、前瞻性的战略眼光和不懈的探索精神&#xff0c;驱动着未来的车轮滚滚向前。 Vatee万腾平台深知&am…

公有链、私有链与联盟链:区块链技术的多元化应用与比较

引言 区块链技术自2008年比特币白皮书发布以来&#xff0c;迅速发展成为一项具有颠覆性潜力的技术。区块链通过去中心化、不可篡改和透明的方式&#xff0c;提供了一种全新的数据存储和管理方式。起初&#xff0c;区块链主要应用于加密货币&#xff0c;如比特币和以太坊。然而&…

RUST 编程语言 绘制随机颜色图片 画圆形 画矩形 画直线

什么是Rust Rust是一种系统编程语言&#xff0c;旨在提供高性能和安全性。它是由Mozilla和其开发社区创建的开源语言&#xff0c;设计目标是在C的应用场景中提供一种现代、可靠和高效的选择。Rust的目标是成为一种通用编程语言&#xff0c;能够处理各种计算任务&#xff0c;包…

STM32-OC输出比较和PWM

本内容基于江协科技STM32视频内容&#xff0c;整理而得。 文章目录 1. OC输出比较和PWM1.1 OC输出比较1.2 PWM&#xff08;脉冲宽度调制&#xff09;1.3 输出比较通道&#xff08;高级&#xff09;1.4 输出比较通道&#xff08;通用&#xff09;1.5 输出比较模式1.6 PWM基本结…

数据库系统原理 | 查询作业2

整理自博主本科《数据库系统原理》专业课自己完成的实验课查询作业&#xff0c;以便各位学习数据库系统概论的小伙伴们参考、学习。 *文中若存在书写不合理的地方&#xff0c;欢迎各位斧正。 专业课本&#xff1a; ​ ​ ———— 本次实验使用到的图形化工具&#xff1a;Heidi…

ThreadPoolExecutor - 管理线程池的核心类

下面是使用给定的初始参数创建一个新的 ThreadPoolExecutor &#xff08;构造方法&#xff09;。 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,…

【SVN的使用-源代码管理工具-SVN介绍-服务器的搭建 Objective-C语言】

一、首先,我们来介绍一下源代码管理工具 1.源代码管理工具的起源 为什么会出现源代码管理工具,是为了解决源代码开发的过程中出现的很多问题: 1)无法后悔:把项目关了,无法Command + Z后悔, 2)版本备份:非空间、费时间、写的名称最后自己都忘了干什么的了, 3)版本…

中英双语介绍加拿大(Canada)

加拿大国家简介 中文版 加拿大简介 加拿大是位于北美洲北部的一个国家&#xff0c;以其广袤的土地、多样的文化和自然美景著称。以下是对加拿大的详细介绍&#xff0c;包括其地理位置、人口、经济、特色、高等教育、著名景点、国家历史和交通条件。 地理位置 加拿大是世界…

LeetCode 189.轮转数组 三段逆置 C写法

LeetCode 189.轮转数组 C写法 三段逆置 思路: 三段逆置方法:先逆置前n-k个 再逆置后k个 最后整体逆置 由示例1得&#xff0c;需要先逆置1,2,3,4 再逆置5,6,7&#xff0c;最后前n-k个与后k个逆置 代码 void reverse(int*num, int left, int right) //逆置函数 { while(left …

【工具】豆瓣自动回贴软件

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 相比于之前粗糙丑陋的黑命令框版本&#xff0c;这个版本新增了UI界面&#xff0c;从此可以不需要再挨个去翻配置文件了。 另外&#xff0c;升级了隐藏浏…

深入理解并发、线程与等待通知机制

目录 一、基础概念 进程和线程 进程 线程 Java 线程的无处不在 进程间的通信 进程间通信有几种方式&#xff1f; CPU 核心数和线程数的关系 上下文切换&#xff08;Context switch&#xff09; 并行和并发 二、认识 Java 里的线程 Java 程序天生就是多线程的 线程的…