阶段三:项目开发---搭建项目前后端系统基础架构:任务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 = axiosaxios.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: {// 存储tokenAuthorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : '',},mutations: {// 修改token,并将token存入localStoragechangeLogin (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.queryif (query) {this.redirect = query.redirectthis.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%A8config.cancelToken = new CancelToken((c) => {pending[requestData] = c})} else { // 允许重复请求,不进行拦截delete config.data.allowedRepeat // 把自定义的请求参数给删掉,不发送给服务端}return config
}, error => {// Do something with request errorconsole.log(error) // for debugPromise.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.paramsif (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
    // ... ...// ... 略 ...@Autowiredprivate TokenAuthenticationProvider tokenAuthenticationProvider;@Overrideprotected 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 {@Autowiredprivate SysUserMapper userMapper;@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate TokenAuthenticationProvider tokenAuthenticationProvider;@Overrideprotected void configure(AuthenticationManagerBuilder auth) {// 使用自定义身份验证组件auth.authenticationProvider(tokenAuthenticationProvider);}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 配置 CSRF 关闭,允许跨域访问http.csrf().disable();// 开启Spring Security cors支持,允许跨域访问http.cors();// 关闭 Sessionhttp.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")));});}@Overridepublic 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中取用户名和密码,生成认证实体)*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {return null;//① ...}/*** 认证成功返回统一格式的JSON*/@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChainchain, Authentication auth) throws IOException {//④...}/*** 认证失败返回统一格式的JSON*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponseresponse, 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();@Autowiredprivate SysUserMapper userMapper;@Autowiredprivate SysAuthMapper authMapper;/*** 身份认证*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {return null;//③ ... }@Overridepublic 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中取用户名和密码,生成认证实体)*/@Overridepublic 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类中的相应位置替换为如下代码):
   @Overridepublic boolean supports(Class<?> authentication) {//②return authentication.equals(UsernamePasswordAuthenticationToken.class);}
  • 在TokenAuthenticationProvider类中的 ③ 部分代码如下(请在TokenAuthenticationProvider类中的相应位置替换为如下代码):
	/*** 身份认证*/@Overridepublic 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;//生成tokentry {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());}//存储用户对应tokenredisTemplate.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;}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {System.out.println("权限信息打印");// 从Http头中取tokenString token = ((HttpServletRequest) request).getHeader("Authorization");if (token != null) {// 校验token,并获取token中存储的用户名R r = TokenUtil.valid(token);// 验证redis是否存在tokenBoolean tokenBoo = redisTemplate.hasKey(token);if (tokenBoo != null && tokenBoo && r.isSuccess()) {// 从token检验结果获取用户名String account = MapUtils.getString(r.getPayloadMap(), "account", StringUtils.EMPTY);// 根据token获取redis中权限setSet<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;}@Overridepublic 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.BASE64URLJwtBuilder 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 {@Beanpublic 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.Driverusername: 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.mzph.cn/diannao/42372.shtml

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

相关文章

如何让代码兼容 Python 2 和 Python 3?Future 库助你一臂之力

目录 01Future 是什么? 为什么选择 Future? 安装与配置 02Future 的基本用法 1、兼容 print 函数 2、兼容整数除法 3、兼容 Unicode 字符串 03Future 的高级功能 1. 处理字符串与字节 2. 统一异常处理…

linux kthread任务管理

目录 一、linux 创建内核线程1.1 kthread_create1.2 kthread_create_worker kthread_queue_work 二、设置线程优先级和调度策略2.1 sched_setscheduler2.2 调度策略 一、linux 创建内核线程 1.1 kthread_create 在 linux 中&#xff0c;可以使用 kthread_create 接口创建内核…

移动校园(7)ii:uniapp路由响应拦截器处理token,以及微信小程序报错当前页面正在处于跳转状态,请稍后再进行跳转....

依据昨天的写完&#xff0c;在token过期之后&#xff0c;再次调用接口&#xff0c;会触发后端拦截&#xff0c;扔进全局错误处理中间件 前端说明提示都没有&#xff0c;只有一个这个&#xff0c;现在优化一下&#xff0c;再写一个类似全局后置守卫&#xff0c;当状态码是401的时…

MySQL——数据连接池

数据库连接 --- 执行完毕 --- 释放&#xff08;连接到释放的过程十分浪费系统资源&#xff09; 池化技术&#xff1a;准备一些预先的资源&#xff0c;过来就连接预先准备好的 编写连接池&#xff0c;实现一个接口 DataSource 开源数据源实现&#xff08;拿来即用&#xff09;…

增强安全防护,解读智慧校园系统的登录日志功能

在构建智慧校园系统时&#xff0c;登录日志功能扮演着不可或缺的角色&#xff0c;它不仅是系统安全的守护者&#xff0c;也是提升管理效率和确保合规性的有力工具。这一机制详细记录每次登录尝试的方方面面&#xff0c;涵盖了时间戳、用户身份、登录来源的IP地址乃至使用的设备…

phpcms 升级php8.3.8

windows 2008 server 不支持php8.3.8,需升级为windows 2012 1.下载php8.3.8 PHP8.3.9 For Windows: Binaries and sources Releases 2.配置php.ini (1.)在php目录下找到php.ini-development文件&#xff0c;把它复制一份&#xff0c;改名为php.ini (2.)修改php安装目录 根…

《昇思 25 天学习打卡营第 10 天 | ResNet50 迁移学习 》

《昇思 25 天学习打卡营第 10 天 | ResNet50 迁移学习 》 活动地址&#xff1a;https://xihe.mindspore.cn/events/mindspore-training-camp 签名&#xff1a;Sam9029 使用迁移学习进行狼狗图像分类 简介 在机器学习和深度学习中&#xff0c;我们经常面临数据不足的问题。 迁…

python【文件操作】

文件操作 一、创建文件夹二、文件操作模式1.覆盖写入2.读取3.追加 三、 Python脚本在文件中查找和替换文本四、 python清空文件夹 一、创建文件夹 判断文件或者文件夹是否存在 import ospathrD://测试文件夹 if not os.path.exists(path):os.mkdir(path)print(os.path.exists…

C++模板元编程(二)——完美转发

完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美&#xff0c;即不仅能准确地转发参数的值&#xff0c;还能保证被转发参数的左、右值属性不变。 文章目录 场景旧的方法新的方法内部实现参考文献 场景 思考下面的代码&#xff1a; templ…

高防服务器的重要性

在数字化时代&#xff0c;网络安全已成为企业和个人最为关注的问题之一。随着网络攻击的日益频繁和复杂&#xff0c;传统的服务器租用服务已难以满足高安全需求的市场。高防服务器租用应运而生&#xff0c;成为保护网络安全的重要解决方案。本文将探讨高防服务器租用的概念、重…

专业140+总分420+天津大学815信号与系统考研经验天大电子信息与通信工程,真题,大纲,参考书。

顺利上岸天津大学&#xff0c;专业课815信号与系统140&#xff0c;总分420&#xff0c;总结一些自己的复习经历&#xff0c;希望对于报考天大的同学有些许帮助&#xff0c;少走弯路&#xff0c;顺利上岸。专业课&#xff1a; 815信号与系统&#xff1a;指定教材吴大正&#xf…

2-26 基于matlab开发的制冷循环模型

基于matlab开发的制冷循环模型。Simscape两相流域中的制冷循环模型&#xff0c;在simulink中完成多循环温度控制。程序已调通&#xff0c;可直接运行。 2-26 制冷循环模型 Simscape两相流域 - 小红书 (xiaohongshu.com)

Arduino ESP8266 开发环境搭建

Arduino ESP8266 开发环境搭建 很久之前学嵌入式时&#xff0c;用过Arduino8266进行开发&#xff0c;开发成本低、难度小&#xff0c;体验很不错。 近期&#xff0c;又突然要用&#xff0c;遂再次搭建环境&#xff0c;但变动挺多&#xff0c;有些小波折&#xff0c;开贴记录。…

生成式AI应用实列和价值链

生成式AI应用实列和价值链 生成式AI应用实列ChatGPTGeminiGitHub CopilotSynthesia 价值链 生成式AI应用实列 ChatGPT ChatGPT 并不是生成式 AI 行业中唯一的公司。 Stability AI 的 Stable Diffusion 可以根据文本描述生成图像&#xff0c;发布后 90 天内&#xff0c;在 Git…

vue是如何进行监听数据变化的?vue2和vue3分别是什么,vue3为什么要更换

在 Vue 中&#xff0c;数据变化的监听是通过响应式系统来实现的。Vue 2.x 和 Vue 3 在这方面有一些区别。 Vue 2.x 的数据监听 Vue 2.x 使用的是 Object.defineProperty() 方法来实现数据的响应式。当你声明一个 Vue 实例的数据对象时&#xff0c;Vue 将遍历这个对象的属性&a…

清除屏幕上信息的命令clear

清除屏幕上信息的命令clear There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should leave quickly. 清…

高考志愿填报千万要注意这四点

在高考志愿填报过程中&#xff0c;确实有很多需要留心的点。我为你总结了四个关键点&#xff0c;希望能帮助你顺利完成志愿填报&#xff1a; 1、学校提供的支持 学校作为学生志愿填报咨询服务的主阵地&#xff0c;应提供体系化和制度化的支持。包括及时关注并传达政策动向和相…

行内元素、块级元素居中

行内元素居中 水平居中 {text-align&#xff1a;center;}垂直居中 单行——行高等于盒子高度 <head><style>.father {width: 400px;height: 200px;/* 行高等于盒子高度&#xff1a;line-height: 200px; */line-height: 200px;background-color: pink;}.son {}&…

如何做好IT类的技术面试?

我们在找工作时&#xff0c;需要结合自己的现状&#xff0c;针对意向企业做好充分准备。作为程序员&#xff0c;你有哪些面试IT技术岗的技巧&#xff1f; 方向一&#xff1a;分享你面试IT公司的小技巧 我分享一些基于广泛观察和用户反馈的面试IT公司的小技巧&#xff1a; 技术准…

孟德尔随机化-痛风

写在前面 昨天看到文献&#xff0c;称饮酒与痛风无关联&#xff0c;甚是疑惑&#xff0c;今天刚好看了一篇新文献&#xff0c;虽然不是主要讲饮酒与痛风的&#xff0c;但也有牵扯到这方面内容&#xff0c;而且是相反的内容&#xff0c;特记录一下。 孟德尔随机化-受教育程度与…