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

news2024/11/16 5:18:22

任务描述

任务名称:

实现基本的登录功能

知识点

了解前端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)进行登录。 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【机器学习】属性降维:揭示数据的简化之美

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 属性降维&#xff1a;揭示数据的简化之美引言什么是属性降维&#xff1f;为何降…

(二)前端javascript中的数据结构之栈

栈是一种遵从后进先出&#xff08;LIFO&#xff09;原则的有序集合。新添加的或待删除的元素都保存在栈的 同一端&#xff0c;称作栈顶&#xff0c;另一端就叫栈底。在栈里&#xff0c;新元素都靠近栈顶&#xff0c;旧元素都接近栈底。 栈是限定仅在表的一端进行插入和删除操作…

SpringBoot源码阅读(3)——监听器

ApplicationListener类初始化位置 在类SpringApplication的构造方法&#xff0c;第267行 在META-INFO/spring.factories中配置的实现类 spring-boot # Application Listeners org.springframework.context.ApplicationListener\ org.springframework.boot.ClearCachesApplic…

一分钟教会你,iPhone和安卓手机虚拟摄像头如何连接电脑?手机连接电脑虚拟摄像头!

在当今的远程工作和在线学习时代&#xff0c;摄像头的需求日益增长。然而&#xff0c;有时我们可能会遇到电脑摄像头画质不佳或无法使用的情况。此时&#xff0c;利用手机的高清摄像头作为电脑的虚拟摄像头&#xff0c;便成了一个理想的选择。本文将为您介绍如何将iPhone和安卓…

SpringMVC系列十三: SpringMVC执行流程 - 源码分析

源码分析 执行流程图实验设计前端控制器分发请求处理器映射器处理器适配器调用目标方法调用视图解析器渲染视图作业布置 执行流程图 实验设计 1.新建com.zzw.web.debug.HelloHandler Controller public class HelloHandler {//编写方法, 响应请求, 返回ModelAndViewRequestMa…

拥抱 AGI:PieDataCS 引领云原生数据计算系统新范式

自2023年后&#xff0c;人工智能技术进入了一个更为成熟和广泛应用的阶段&#xff0c;人工通用智能&#xff08;AGI&#xff09;这一概念也成为了科技界和产业界热议的焦点。本文将结合 AGI 时代背景&#xff0c;从架构设计到落地实践&#xff0c;详细介绍拓数派云原生数据计算…

掌握全球通用的建模语言,从《Archimate®3.2规范参考卡》开始!

作为企业架构设计的通用语言&#xff0c;Archimate更是一种愿景的体现&#xff0c;一个变革的引擎&#xff0c;它不仅可以与TOGAF标准高效结合&#xff0c;还可助力全球企业无边界协作使用&#xff08;下滑可了解更多Archimate建模语言的发展和演进&#xff09;&#xff01; 本…

PMP 认证权威吗?对项目…业生涯的发展有帮助?

PMP认证到底权威吗&#xff1f; 首先在我看来&#xff0c;PMP认证是否权威要从各个角度进行综合考虑。入行这么多年个人也有不少的体会&#xff0c;那么我们就从多个角度进行分析一下&#xff0c;PMP认证的权威性与促进方面。 在深入探讨这个话题前&#xff0c;我分享一下近期…

双向链表 -- 详细理解和实现

欢迎光顾我的homepage 前言 双向链表是一种带头双向循环的链表。在双向链表中&#xff0c;首先存在着一个头结点&#xff1b;其次每个节点有指向下一个节点的指针next 和指向上一个节点的指针prev &#xff1b…

《网络安全和信息化》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答 问&#xff1a;《网络安全和信息化》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的正规学术期刊。 问&#xff1a;《网络安全和信息化》级别&#xff1f; 答&#xff1a;国家级。主管单位&#xff1a;工业和信息化部 主办单位&#…

脑干出血:揭秘其成因、症状与治疗方法

脑干出血&#xff0c;作为脑出血中尤为凶险的一种&#xff0c;主要袭击脑部的核心——脑干区域。脑干&#xff0c;作为连接大脑与脊髓的桥梁&#xff0c;掌管着呼吸、心跳、意识等至关重要的生命功能&#xff0c;一旦受损&#xff0c;后果不堪设想。 脑干出血的成因 首要元凶…

python爬虫入门(四)之Beautiful Soup库

一、什么是Beautiful Soup库 1、Beautiful Soup库是用来做HTML解析的库 Beautiful Soup把看起来复杂的HTML内容&#xff0c;解析成树状结构&#xff0c;让搜索和修改HTML结构变得更容易 2、第三方库&#xff0c;先安装 终端输入pip install bs4 from bs4 import Beautiful…

AI工具杂谈

AI是在帮助开发者还是取代他们&#xff1f; 在软件开发领域&#xff0c;生成式人工智能&#xff08;AIGC&#xff09;正在改变开发者的工作方式。无论是代码生成、错误检测还是自动化测试&#xff0c;AI工具正在成为开发者的得力助手。然而&#xff0c;这也引发了对开发者职业…

Pytorch的ImageFolder数据加载器

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️如遇文章付费&#xff0c;可先看…

burpsuite官方靶场之逻辑漏洞篇

*1.过于信任客户端控件* *1.1 达成目标* 用wiener买一件Lightweight l33t leather jacket。 提供的账户&#xff1a;wiener/peter *1.2攻击步骤* 第一步&#xff0c;登录wienr&#xff0c;并且来到主界面&#xff0c;发现Lightweight l33t leather jacket要$1337&#xff…

面试经典 106. 从中序与后序遍历序列构造二叉树

最近小胖开始找工作了&#xff0c;又来刷苦逼的算法了 555 废话不多说&#xff0c;看这一题&#xff0c;上链接&#xff1a;https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/?envTypestudy-plan-v2&envIdtop-inte…

【Unity】简单举例UI合批,优化draw call

1. UGUI-Editor 首先引入UGUI-Editor插件 链接: https://pan.baidu.com/s/1PpxStvgRCDi9xjUr6j6nCQ?pwdm5ju 提取码: m5ju 或者直接去Github搜索UGUI-Editor 2. 没有UI时 3. 放几个UI看看效果 4. 选中Canvas&#xff0c;右键优化Batch 发现减少了3个&#xff0c;这是因为&…

STM32智能无人机控制系统教程

目录 引言环境准备智能无人机控制系统基础代码实现&#xff1a;实现智能无人机控制系统 4.1 数据采集模块 4.2 数据处理与飞行控制 4.3 通信与导航系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;无人机应用与优化问题解决方案与优化收尾与总结 1. 引言 智能无人机控…

vue3创建项目

1. 安装node.js&#xff0c;添加环境变量&#xff0c;确保cmd里能使用node命令以及npm命令&#xff1a;node --version npm --version 本人安装的版本如下&#xff1a; 2. 安装vue的脚手架 npm install -g vue/cli 3. 创建vue项目&#xff1a;1&#xff09;使用ui&#xff1…

#数据结构 链表

单向链表 1. 概念 单向链表 单向循环链表 双向链表 双向循环链表 解决&#xff1a;长度固定的问题&#xff0c;插入和删除麻烦的问题 1、逻辑结构&#xff1a; 线性结构 2、存储结构&#xff1a; 链式存储 链表就是将 结点 用链串起来的线性表&#xff0c;链就是 结点 中的…