阶段三:项目开发---搭建项目前后端系统基础架构:任务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.xdnf.cn/news/1474316.html

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

相关文章

firewalld(8) policies

简介 前面的文章中我们介绍了firewalld的一些基本配置以及NAT的相关配置。在前面的配置中&#xff0c;我们所有的策略都是与zone相关的&#xff0c;例如配置的rich rule&#xff0c;--direct,以及NAT,并且这些配置都是数据包进入zone或者从zone发出时设置的策略。 我们在介绍…

昇思25天学习打卡营第15天 | Vision Transformer图像分类

内容介绍&#xff1a; 近些年&#xff0c;随着基于自注意&#xff08;Self-Attention&#xff09;结构的模型的发展&#xff0c;特别是Transformer模型的提出&#xff0c;极大地促进了自然语言处理模型的发展。由于Transformers的计算效率和可扩展性&#xff0c;它已经能够训练…

在VMware虚拟机的创建以及安装linux操作系统

一、创建虚拟机 1.双击打开下载好的VMware Workstation软件 2.点击“创建新的虚拟机” 3.根据个人选择需要创建的虚拟机&#xff0c;点击下一步 4.直接点击下一步 5.选择稍后安装操作系统&#xff0c;点击下一步 、 6.选择需要的操作系统&#xff0c;点击下一步 7.根据…

YOLOv8改进---BiFPN特征融合

一、BiFPN原理 1.1 基本原理 BiFPN&#xff08;Bidirectional Feature Pyramid Network&#xff09;&#xff0c;双向特征金字塔网络是一种高效的多尺度特征融合网络&#xff0c;其基本原理概括分为以下几点&#xff1a; 双向特征融合&#xff1a;BiFPN允许特征在自顶向下和自…

【踩坑】修复pyinstaller报错 No module named pkg_resources.extern

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 报错如下&#xff1a; 修复方法&#xff1a; pip install --upgrade setuptools pippyinstaller -F -w main.py --hidden-importpkg_resources.py2_wa…

算法系列--分治排序|归并排序|逆序对的求解

一.基本概念与实现 归并排序(mergeSort)也是基于分治思想的一种排序方式,思路如下: 分解:根据中间下标mid将数组分解为两部分解决:不断执行上述分解过程,当分解到只有一个元素时,停止分解,此时就是有序的合并:合并两个有序的子区间,所有子区间合并的结果就是原问题的解 归并…

【TB作品】51单片机 Proteus仿真 00002仿真-智能台灯色调倒计时光强

实验报告&#xff1a;基于51单片机的智能台灯控制系统 背景 本实验旨在设计一个基于51单片机的智能台灯控制系统&#xff0c;该系统可以通过按键进行手动控制&#xff0c;并能根据环境光强度自动调节台灯亮度。此外&#xff0c;系统还具备倒计时关灯功能。 器件连接 51单片…

Xilinx FPGA:vivado关于真双端口的串口传输数据的实验

一、实验内容 用一个真双端RAM&#xff0c;端口A和端口B同时向RAM里写入数据0-99&#xff0c;A端口读出单数并存入单端口RAM1中&#xff0c;B端口读出双数并存入但端口RAM2中&#xff0c;当检测到按键1到来时将RAM1中的单数读出显示到PC端&#xff0c;当检测到按键2到来时&…

强化学习的数学原理:时序差分算法

概述 之前第五次课时学习的 蒙特卡洛 的方法是全课程当中第一次介绍的第一种 model-free 的方法&#xff0c;而本次课的 Temporal-Difference Learning 简称 TD learning &#xff08;时序差分算法&#xff09;就是第二种 model-free 的方法。而对于 蒙特卡洛方法其是一种 non…

QWidget窗口抗锯齿圆角的一个实现方案(支持子控件)2

QWidget窗口抗锯齿圆角的一个实现方案&#xff08;支持子控件&#xff09;2 本方案使用了QGraphicsEffect&#xff0c;由于QGraphicsEffect对一些控件会有渲染问题&#xff0c;比如列表、表格等&#xff0c;所以暂时仅作为研究&#xff0c;优先其他方案 在之前的文章中&#…

论文辅助笔记:ST-LLM

1 时间嵌入 2 PFA&#xff08;Partial Frozen Architecture&#xff09; 3 ST_LLM 3.1 初始化 3.2 forward

Idea新增Module报错:sdk ‘1.8‘ type ‘JavaSDK‘ is not registered in ProjectJdkTable

文章目录 一&#xff0c;创建Module报错二&#xff0c;原因分析三&#xff0c;解决方案1&#xff0c;点击上图的加号&#xff0c;把JDK8添加进来即可2&#xff0c;点击左侧[Project]&#xff0c;直接设置SDK为JDK8 四&#xff0c;配置检查与验证 一&#xff0c;创建Module报错 …

【Linux】:进程创建与终止

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关Linux程序地址空间的相关知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从…

dell Vostro 3690安装win11 23h2 方法

下载rufus-4.5.exe刻U盘去除限制 https://www.dell.com/support/home/zh-cn/product-support/product/vostro-3690-desktop/drivers dell官网下载驱动解压到U盘 https://dl.dell.com/FOLDER09572293M/2/Intel-Rapid-Storage-Technology-Driver_88DM9_WIN64_18.7.6.1010_A00_01…

【鸿蒙学习笔记】创建自定义组件

官方文档&#xff1a;创建自定义组件 目录标题 [Q&A] 如何自定义组件&#xff1f;&#xff11;・struct 自定义组件名 {...}&#xff12;・build()函数&#xff1a;&#xff13;・&#xff20;Component&#xff14;・Entry&#xff15;・Reusable 自定义组件的参数 buil…

GD32 MCU ADC采样率如何计算?

大家在使用ADC采样的时候是否计算过ADC的采样率&#xff0c;这个问题非常关键&#xff01; 以下为GD32F303系列MCU中有关ADC的参数&#xff0c;其中ADC时钟最大值为40MHz&#xff0c;12位分辨率下最大采样率为2.86MSPS.如果ADC时钟超频的话&#xff0c;可能会造成ADC采样异常&…

SAP_MM模块-采购信息记录变更文档的三种查询方式

最近有用户在问采购信息记录变更的信息怎么去查找&#xff0c;想要看看是谁更改了价格&#xff0c;于是就给她查了一下&#xff0c;顺便做个记录&#xff0c;SAP中的业务数据或者主数据的变更信息查询方法&#xff0c;都是比较类似的&#xff0c;学会了这三个方法&#xff0c;其…

商家店铺电商小程序模板源码

橙色通用的商家入驻&#xff0c;商户商家&#xff0c;商家店铺&#xff0c;购物商城&#xff0c;商家购物平台app小程序网页模板。包含&#xff1a;商家主页、优先商家、商品详情、购物车、结算订单、个人中心、优惠券、会员卡、地址管理等功能页面。 商家店铺电商小程序模板源…

SSM高校教师教学质量评估系统-计算机毕业设计源码03344

摘要 在高等教育中&#xff0c;教学质量是培养优秀人才的关键。为了提高教学质量&#xff0c;高校需要建立一套科学、有效的教师教学质量评估系统。本研究采用 SSM技术框架&#xff0c;旨在开发一款高校教师教学质量评估系统。 SSM框架作为一种成熟的Java开发框架&#xff0c;具…

Centos新手问题——yum无法下载软件

起因&#xff1a;最近在学习centos7&#xff0c;在VM上成功安装后&#xff0c;用Secure进行远程登陆。然后准备下载一个C编译器&#xff0c;看网络上的教程&#xff0c;都是用yum来下载&#xff0c;于是我也输入了命令&#xff1a; yum -y install gcc* 本以为会自动下载&…