在home页面加载完成后,获取当前用户。
<template>
<div class="home">
<el-container>
<el-menu class="el-menu-vertical-demo" background-color="#060037" text-color="#ffffff" :unique-opened="true" :router="true" :collapse="isCollapse">
<el-menu-item>
<img src="../assets/logo.png" width="24" height="24"/>
<span style="font-family: 楷体; font-size: 18px; color: #E3E63C; ">旭锋信息管理系统span>
el-menu-item>
el-menu>
<el-container>
<el-header>
<el-row type="flex" justify="space-between">
<el-row :span="2" align="center">
<i v-if="isCollapse" class="fas fa-indent" style="font-size: 36px; margin-top: 8px" @click="isCollapse=!isCollapse">i>
<i v-else class="fas fa-outdent" style="font-size: 36px; margin-top: 8px" @click="isCollapse=!isCollapse">i>
el-row>
<el-row :span="12" align="end">
<el-dropdown trigger="click" @command="handleCommand" size="small">
<div style="margin-top: 10px">
<a-avatar icon="user" shape="square" />
<span class="el-dropdown-link" style="color: #ffffff">
{{username}}<i class="el-icon-arrow-down el-icon--right">i>
span>
div>
<el-dropdown-menu slot="dropdown" style="width: 120px">
<el-dropdown-item command="psnCenter">个人中心el-dropdown-item>
<el-dropdown-item divided command="logout">安全退出el-dropdown-item>
el-dropdown-menu>
el-dropdown>
el-row>
el-row>
el-header>
<div style="height: 25px; background-color: #7dbcea; box-shadow: 0 4px 15px #888888;">
<el-row type="flex" justify="end">
<el-breadcrumb separator-class="el-icon-arrow-right" style="margin-top:6px; margin-right: 20px">
<el-breadcrumb-item :to="{ path: '/' }">首页el-breadcrumb-item>
<el-breadcrumb-item>活动管理el-breadcrumb-item>
<el-breadcrumb-item>活动列表el-breadcrumb-item>
<el-breadcrumb-item>活动详情el-breadcrumb-item>
el-breadcrumb>
el-row>
div>
<el-main>
<router-view>router-view>
el-main>
<el-footer>Footerel-footer>
el-container>
el-container>
div>
template>
<script>
// @ is an alias to /src
export default {
name: 'Home',
data() {
return {
isCollapse: false,
username: "[email protected]"
};
},
methods: {
handleCommand(command)
{
this.$message('click on item ' + command);
}
}
}
</script>
3.2.1.1 声明变量用于保存菜单数据
data() {
return {
isCollapse: false,
username: "",
/*保存菜单数据*/
menuData: []
};
},
3.2.1.2 定义请求用户信息及授权菜单数据方法
/*请求当前用户所拥有权限的未禁用的菜单类型的菜单*/
async getCurrentUserMenus()
{
const {data : res} = await this.$axios.get(`/menu/usermenu/${this.username}`);
if(res.status == 2000 && res.data && res.data.length > 0)
{
this.menuData = res.data;
}
},
/*重新获取当前用户信息包含用户拥有的授权标识*/
async reloadCurrentUser()
{
let username;
if(sessionStorage.getItem("username"))
{
username = sessionStorage.getItem("username")
}
const { data: res } = await this.$axios.get(`/relogin/${username}`);
if(2000 === res.status && res.data)
{
sessionStorage.setItem("currentUser", JSON.stringify(res.data));
}
else
{
this.$message.error(res.message);
}
},
3.2.1.3 在Vue对象创建完成后,自动请求菜单
created(){
this.username = sessionStorage.getItem("username");
/*获取当前登陆用户信息*/
this.reloadCurrentUser();
/*获取构建当前用户左侧菜单栏的数据*/
this.getCurrentUserMenus();
/*当页面重新构建时即设置当前激活菜单项及获取面包屑相关数据*/
this.setDefaultsAliveMenuAndBreadData();
},
3.2.2.1 三大JAVAEE组件
过滤器
概述
作用
在javax.servlet.Filter接口中定义了3个方法:
void init(FilterConfig filterConfig) 用于完成过滤器的初始化
void destroy() 用于过滤器销毁前,完成某些资源的回收
void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) 实现过滤功能,该方法对每个请求增加额外的处理。
自定义Filter实现
第一步:自定义Filter类并实现javax.servlet.Filter接口。
package com.jt.filter;
import javax.servlet.*;
import java.io.IOException;
/**
* javaee规范中的过滤器,对请求和响应数据进行过滤
* 1)统一数据的编码
* 2)统一数据格式校验 (今日头条的灵犬系统)
* 3)统一身份认证
*/
public class DemoFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println("==doFilter==");
servletRequest.setCharacterEncoding("UTF-8");
String id= servletRequest.getParameter("id");
System.out.println("id="+id);
filterChain.doFilter(servletRequest,servletResponse);
}
}
第二步:将自定义Filter类的对象注册到项目中
package com.jt.config;
/**
* 在这里配置javaee规范中的三大组件
*/
@Configuration
public class ComponentConfig {
....
//注册过滤器
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean bean=
new FilterRegistrationBean(new DemoFilter());
bean.addUrlPatterns("/hello");//对哪个请求进行处理
return bean;
}
}
OncePerRequestFilter
拦截器
概念
在springmvc中,定义拦截器要实现HandlerInterceptor接口,并实现该接口中提供的三个方法
监听器
概念
原理
在javax.servlet.ServletContextListener接口中定义了2种方法:
void contextInitialized(ServletContextEvent sce) 监听器的初始化
void contextDestroyed(ServletContextEvent sce) 监听器销毁
过滤器与拦截器的执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0XCCD48v-1638847005433)(./filepng/过滤器与拦截器.png)]
3.2.2.2 通过过滤器检验token
自定义token校验过滤器
@Component
@PropertySource("classpath:/jwt.properties")
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter
{
private final Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class);
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.head}")
private String tokenHead;
@Autowired
private JWTUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
/**
* 校验token
* */
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
{
/**执行流程:*/
/**
* 1.由于所有请求都会经过此过滤器,但有些url不需要进行token校验,需要直接进入过滤链的下一个过滤器。
* 放行/login与/captcha等url*/
String url = request.getRequestURI();
if("/login".equals(url) || "/captcha".equals(url))
{
/*该方法不能结束函数的后续语句的执行*/
filterChain.doFilter(request, response);
return;
}
/**2.通过request对象中获取请求携带的token.
* 如果token存在,则继续校验流程;则校验是否是以自定义的tokenHead开始。两者都检验成功则进行下一项校验。
* 如果不存在或者不是以自定义tokenHead,则响应前端,无效token,请重新登录。*/
String token = request.getHeader(tokenHeader);
if(null == token || !token.startsWith(tokenHead))
{
JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
return;
}
/**3.校验token的主体
* 如果token主体长度为0,则响应前端无效token
* 如果长度不为0,则继续校验*/
String tokenBody = token.substring(tokenHead.length());
if(tokenBody.length() <= 0)
{
JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
return;
}
String username = null;
try
{
username = jwtUtils.getUserNameFromToken(tokenBody);
}catch (Exception e)
{
logger.error(e.getMessage());
JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.EXPIRED_TOKEN));
return;
}
/**4.校验通过token获取的用户名是否为空。
* 如果为空则说明token被篡改,为无效token
* 如果不为空则进行下一步校验
* */
if (null == username)
{
JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
return;
}
/**5.校验用户信息是否有效,即userDetailsService.loadUserByUsername()是否抛出异常,
* 如果抛出异常说明token无效。
* 如果未抛出异常,进行下一步校验。
* */
SysUserPojo userDetails;
try{
userDetails = (SysUserPojo) userDetailsService.loadUserByUsername(username);
}catch (Exception e)
{
logger.error(e.getMessage());
JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
return;
}
/**6.所有校验都通过后,构建 UsernamePasswordAuthenticationToken 对象
* 该对象有两个构造函数:
* 一个两个参数的,public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
* 一个三个参数的。public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities)
* 当使用两参的构造,则该用户会自动设置为未登录,
* 当使用三参的构造,则该用户会自动设置为已登录。
* */
UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());
/*将被设置为已登录的UsernamePasswordAuthenticationToken 对象放到安全上下文中。此时该用户才会被认为是登录用户。*/
SecurityContextHolder.getContext().setAuthentication(userToken);
filterChain.doFilter(request, response);
}
}
将自定义的token校验过滤器配置到SpringSecurity中
修改SpringSecurity配置类如下:
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private MyUserDetailService myUserDetailService;
@Autowired
private JwtTokenFilter jwtTokenFilter;
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(myUserDetailService)
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception
{
web.ignoring().antMatchers("/login", "/captcha");
}
/*对http请求认证授权配置*/
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
/*配置所有请求都需要登录认证*/
.anyRequest().authenticated()
.and()
/*关闭跨站请求伪造*/
.csrf().disable()
/*配置session管理器*/
.sessionManagement()
/*由于使用jwt令牌来保存用户登录信息,因此不需要使用创建session来保存用户登录信息*/
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
/*将token校验过滤器添加在认证过滤器前*/
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
;
}
}
前响应信息构建成Json响应给前端
由于过滤器执行在SpringMVC之前,因此需要将响应给前端信息自行构建成json串。
方法:通过SpringMVC依赖的Jackson的ObjectMapper
对象完成
具体实现:
package org.wjk.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.wjk.vo.ResPaging;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class JsonResUtils
{
public static void response(HttpServletRequest request, HttpServletResponse response, ResPaging res) throws IOException
{
/*设置响应头*/
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
/*获取对象到JSON的映射器*/
ObjectMapper mapper = new ObjectMapper();
/*ServletOutputStream outputStream = response.getOutputStream();
outputStream.print(mapper.writeValueAsString(res));
outputStream.flush();
outputStream.close();*/
PrintWriter writer = response.getWriter();
/*通过映射器对象将响应封装对象映射为JSON*/
writer.print(mapper.writeValueAsString(res));
writer.flush();
writer.close();
}
}
3.2.3.1 业务需求:
userDetailsService.loadUserByUsername(username)
方法查询数据获取用户信息。因此为提高程序运行效率,减小数据库访问压力,可以将数据库的查询结果保存到redis中。3.2.3.2实现方法:
3.2.3.3实现细节:
/** * redis中保存的数据的数据类型在5.0时只有String,List,Hash,Set,zSet五种类型的数据。 * 当保存到redis中的数据为java对象时,则需要将对象转换成以上类型的一种。常用的是String类型。 * 而Redis的String类型:是二进制安全的字符串。也就是说,Redis中即可以保存普通的String对象,也可以是字节数组。因此redis的string可以包含任何数据。 * 比如jpg图片或者序列化的对象。 * 将java对象序列化成字符串,有两种方法: * 1.通过Serialize方式: * 该方式要求需要待序列化的Java对象的类型必须实现Serialize接口。 * 并且需要自定义序列化与反序列化方法。 * 2.通过JSON方式: * 该方式要求当实现反序列化时要求必须是实体类的对象,并且该对象必须有无参构造函数。 * 如果使用接口或抽像类的引用对象去接收反序列化时将会出错。 * */
3.2.3.4业务实现
userDetailsService.loadUserByUsername(username)
返回的UserDetails接口的某一实现类的对象。因此要实现缓存,则需要通过Serialize方式实现序列化。
自定义序列化与反序列化工具类。具体实现如下:
package org.wjk.utils;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
@Slf4j
public class SerializeUtils
{
private static final Logger LOGGER = LoggerFactory.getLogger(SerializeUtils.class);
public static byte[] serialize(Object resource)
{
/**
* ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。
* 可以使用 ObjectInputStream 读取(重构)对象。通过在流中使用文件可以实现对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或 另一个进程中重构对象。
*
* 只能将支持 java.io.Serializable 接口的对象写入流中。每个 serializable 对象的类都被编码,编码内容包括类名和类签名、对象的字段值和 数组值,以及从初始对象中引用的其他所有对象的闭包。
*
* writeObject 方法用于将对象写入流中。所有对象(包括 String 和数组)都可以通过 writeObject 写入。可将多个对象或基元写入流中。
* 必须使用与写入对象时相同的类型和顺序从相应 ObjectInputstream 中读回对象。
*
* 还可以使用 DataOutput 中的适当方法将基本数据类型写入流中。还可以使用 writeUTF 方法写入字符串。
*
* 对象的默认序列化机制写入的内容是:
* 对象的类,类签名,以及非瞬态和非静态字段的值。其
* 他对象的引用(瞬态和静态字段除外)也会导致写入那些对象。
* 可使用引用共享机制对单个对象的多个引用进行编码,这样即可将对象的图形恢复为最初写入它们时的形状。
* */
ObjectOutputStream oos = null;
/**
* 此类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray() 和 toString() 获 取数据。
*
* 关闭 ByteArrayOutputStream 无效。此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException。
* */
ByteArrayOutputStream baos = null;
try
{
baos = new ByteArrayOutputStream();
// 创建写入指定 OutputStream 的 ObjectOutputStream。此构造方法将序列化流部分写入底层流;调用者可以通过立即刷新流,确保在读取头 部时,用于接收 ObjectInputStreams 构造方法不会阻塞。
oos = new ObjectOutputStream(baos);
// 将指定的对象写入 ObjectOutputStream。对象的类、类的签名,以及类及其所有超类型的非瞬态和非静态字段的值都将被写入。可以使用 writeObject 和 readObject 方法重写类的默认序列化。
// 由此对象引用的对象是以可变迁的方式写入的,这样,可以通过ObjectInputStream 重新构造这些对象的完全等价的图形。
oos.writeObject(resource);
return baos.toByteArray();
}
catch (Exception e)
{
e.printStackTrace();
LOGGER.error("序列化失败,原因是:"+ e.getMessage());
}
return null;
}
public static Object unserialize(byte [] resource)
{
// ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化
ObjectInputStream ois = null;
ByteArrayInputStream bais = null;
try
{
bais = new ByteArrayInputStream(resource);
/*创建从指定 InputStream 读取的 ObjectInputStream。从流读取序列化头部并予以验证。在对应的 ObjectOutputStream 写入并刷新头部 之前,此构造方法将阻塞。*/
ois = new ObjectInputStream(bais);
/*readObject 方法负责使用通过对应的 writeObject 方法写入流的数据,为特定类读取和恢复对象的状态。该方法本身的状态,不管是属于其超类 还是属于其子类,都没有关系。恢复状态的方法是,从个别字段的 ObjectInputStream 读取数据并将其分配给对象的适当字段。DataInput 支持读取基本数据类型。 */
return ois.readObject();
}catch (Exception e)
{
e.printStackTrace();
LOGGER.error("反序列化失败,原因是:" + e.getMessage());
}
return null;
}
}
通过AOP实现数据缓存到redis
添加AOP依赖及Jedis 连接依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
完成Jedis配置文件:新建redis.properties配置文件
#redis所在主机配置
redis.host=192.168.75.55
redis.port=6379
# redis连接池配置
redis.pool.maxTotal=32
redis.pool.maxIdle=32
redis.pool.minIdle=16
redis.pool.maxWait=5
redis.pool.testOnBorrow=false
完成Jedis连接池创建:新建RedisConfig配置类
package org.wjk.config.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.time.Duration;
@PropertySource("classpath:/redis.properties")
@Configuration
public class RedisConfig
{
private final Logger logger = LoggerFactory.getLogger(RedisConfig.class);
@Value("${redis.host}")
private String jedisHost;
@Value("${redis.port}")
private Integer jedisPort;
@Value("${redis.pool.maxTotal}")
private Integer maxTotal;
@Value("${redis.pool.maxIdle}")
private Integer maxIdle;
@Value("${redis.pool.minIdle}")
private Integer minIdle;
@Value("${redis.pool.maxWait}")
private Integer maxWait;
@Value("${redis.pool.testOnBorrow}")
private Boolean testOnBorrow;
@Bean
public JedisPool jedisPool()
{
logger.debug("Redis's host is {} and port is {}", jedisHost, jedisPort);
JedisPoolConfig poolConfig = new JedisPoolConfig();
/*设置redis连接池中最大连接数*/
poolConfig.setMaxTotal(maxTotal);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setMaxWait(Duration.ofSeconds(maxWait));
poolConfig.setTestOnBorrow(testOnBorrow);
return new JedisPool(poolConfig, jedisHost, jedisPort);
}
}
创建用于AOP缓存读取与刷新的自定义注解
缓存读取注解
package org.wjk.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheReader
{
String value() default "";
}
缓存刷新注解
package org.wjk.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheWriter
{
String value() default "";
}
实现缓存
package org.wjk.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.wjk.annotation.CacheReader;
import org.wjk.exception.ExcInfo;
import org.wjk.exception.ServiceException;
import org.wjk.utils.SerializeUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Arrays;
@Aspect
@Component
@Slf4j
public class CacheOperation
{
@Autowired
private JedisPool jedisPool;
private final Logger logger = LoggerFactory.getLogger(CacheOperation.class);
@Around("@annotation(anCacheReader)")
public Object getDataFromCache(ProceedingJoinPoint joinPoint,CacheReader anCacheReader) throws Throwable
{
/*构建Redis中的key*/
String jdsKey = anCacheReader.value();
jdsKey = jdsKey + Arrays.toString(joinPoint.getArgs());
Jedis jedis = jedisPool.getResource();
Object result = null;
/*如果Redis中存在jdsKey,则获取该Key对应的数据。*/
if(jedis.exists(jdsKey))
{
try
{
logger.info("从redis中获取数据");
result = SerializeUtils.unserialize(jedis.get(jdsKey.getBytes()));
}
catch(Exception e)
{
logger.error("从redis中获取数据失败,原因是:" + e.getMessage());
throw new ServiceException(ExcInfo.UNKNOW_EXCEPTION);
}
finally
{
jedis.close();
}
}
/*如果Redis中不存在jdsKey,则从数据库中获取数据,并将数据以jdsKey为Key保存至Redis中*/
else
{
logger.info("从mysql中获取数据");
result = joinPoint.proceed();
storeToRedis(jedis, jdsKey, result);
}
return result;
}
/*异步实现*/
@Async
protected void storeToRedis(Jedis jedis, String key, Object object)
{
if(jedis == null)
{
jedis = jedisPool.getResource();
}
try
{
logger.info("以{}为key保存数据", key);
jedis.set(key.getBytes(), SerializeUtils.serialize(object));
}
catch (Exception e)
{
logger.error("保存数据到redis失败,原因是:" + e.getMessage());
}
finally
{
jedis.close();
}
}
}
关于SpringBoot项目的多线程
SpringBoot项目默认是支持多线程的。
多线程的实现
核心配置文件中,完成线程池的配置
spring:
#关闭SpringBoot图标
main:
banner-mode: off
#配置数据源信息
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mysql://192.168.75.55:3306/xfsy?severTimezone=GMT%2B8&characterEncoding=utf8
username: root
password: root
#配置线程池信息
task:
execution:
pool:
#核心线程数,当池中线程数没达到core-size的值时,每接收一个新的任务都会创建一个新线程,然后存储到池。假如池中线程数已经达到 core-size设置的值,再接收新的任务时,要检测是否有空闲的核心线程,假如有,则使用空闲的核心线程执行新的任务。
core-size: 16
#最大线程数,当任务队列已满,核心线程也都在忙,再来新的任务则会创建新的线程,但所有线程数不能超过max-size设置的值,否则可能会 出现异常(拒绝执行)
max-size: 16
#队列容量,假如核心线程数已达到core-size设置的值,并且所有的核心线程都在忙,再来新的任务,会将任务存储到任务队列。
queue-capacity: 32
#线程空闲时间,假如池中的线程数多余core-size设置的值,此时又没有新的任务,则一旦空闲线程空闲时间超过keep-alive设置的时间 值,则会被释放。
keep-alive: 60s
thread-name-prefix: cims-task-
开启多线程,在启动类上使用@EnableAsync
注解
@SpringBootApplication
@EnableAsync
public class BsServerApplication
{
public static void main(String[] args)
{
SpringApplication.run(BsServerApplication.class, args);
}
}
使用@Async
注解描述要异步执行的方法。如写入缓存的方法
/*异步实现*/
@Async
protected void storeToRedis(Jedis jedis, String key, Object object)
{
if(jedis == null)
{
jedis = jedisPool.getResource();
}
try
{
logger.info("以{}为key保存数据", key);
jedis.set(key.getBytes(), SerializeUtils.serialize(object));
}
catch (Exception e)
{
logger.error("保存数据到redis失败,原因是:" + e.getMessage());
}
finally
{
jedis.close();
}
}
3.2.4.1 定义封装数据库菜单表记录的实体类
package org.wjk.pojo;
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 lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@Accessors(chain = true)
@TableName("sys_menu")
public class SysMenuPojo extends BasePojo
{
private static final long serialVersionUID = -514867408544331729L;
@TableId(type = IdType.AUTO)
@NotNull(groups = Update.class)
private Integer id;
@NotNull
private Integer type;
private String path;
private String component;
@NotEmpty
private String name;
@NotEmpty
private String iconCls;
private String permission;
private String permissionNote;
private Integer parentId=0;
private Integer enabled = 1;
@TableField(exist = false)
private List<SysMenuPojo> children;
public interface Insert {}
public interface Update {}
public static List<SysMenuPojo> listConvertToNode(List<SysMenuPojo> source)
{
List<SysMenuPojo> target = new ArrayList<>();
Map<Integer, SysMenuPojo> map = new HashMap<>();
for(SysMenuPojo item : source)
map.put(item.id, item);
for(SysMenuPojo it : source)
{
if(it.id == 0 || null == map.get(it.parentId))
{
target.add(it);
}
else
{
SysMenuPojo parent = map.get(it.parentId);
if (null == parent.children)
parent.children = new ArrayList<>();
parent.children.add(it);
}
}
return target;
}
}
3.2.4.2 定义接收的Controller类SysMenuCtrller
package org.wjk.controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wjk.service.SysMenuSvc;
import org.wjk.vo.ResPaging;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/menu")
public class SysMenuCtrller
{
private SysMenuSvc sysMenuSvc;
// 使用构造方法方式完成依赖注入。当交由Spring管理的类中,只有一个构造方法时,该构造方法默认是使用@Autowired注解描述,即使不显示使用该注解。
public SysMenuCtrller(SysMenuSvc sysMenuSvc)
{
this.sysMenuSvc = sysMenuSvc;
}
/*处理获取当前用户授权访问的菜单数据的请求。*/
@GetMapping("/usermenu/{username}")
public ResPaging getCurrentUserMenus(@PathVariable @Validated @NotEmpty String username)
{
return ResPaging.success("OK", sysMenuSvc.getCurrentUserMenus(username));
}
}
3.2.4.3 定义Service与Serivce实现类:SysMenuSvc
和SysMenuSvcImpl
package org.wjk.service;
import org.wjk.pojo.SysMenuPojo;
import java.util.List;
public interface SysMenuSvc
{
List<SysMenuPojo> getCurrentUserMenus(String username);
}
package org.wjk.service.impl;
import org.springframework.stereotype.Service;
import org.wjk.annotation.CacheReader;
import org.wjk.dao.SysMenuDao;
import org.wjk.exception.ExcInfo;
import org.wjk.exception.ServiceException;
import org.wjk.pojo.SysMenuPojo;
import org.wjk.service.SysMenuSvc;
import java.util.List;
@Service
public class SysMenuSvcImpl implements SysMenuSvc
{
private SysMenuDao sysMenuDao;
public SysMenuSvcImpl(SysMenuDao sysMenuDao)
{
this.sysMenuDao = sysMenuDao;
}
/*获取当前用户授权的菜单数据*/
@Override
@CacheReader("MENU_USER::")
public List<SysMenuPojo> getCurrentUserMenus(String username)
{
List<SysMenuPojo> source = sysMenuDao.getCurrentUserMenus(username);
if(source.isEmpty())
throw new ServiceException(ExcInfo.ILLEGAL_DB_CONNECT);
List<SysMenuPojo> target = SysMenuPojo.listConvertToNode(source);
if(target.isEmpty())
throw new ServiceException(ExcInfo.DATA_CAST_ERROR);
return target;
}
}
3.2.4.4 定义Dao层:SysMenuDao
package org.wjk.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.wjk.pojo.SysMenuPojo;
import java.util.List;
@Mapper
public interface SysMenuDao extends BaseMapper<SysMenuPojo>
{
List<SysMenuPojo> getCurrentUserMenus(String username);
}
3.2.4.5 定义映射文件: SysMenu
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wjk.dao.SysMenuDao">
<select id="getCurrentUserMenus" resultType="org.wjk.pojo.SysMenuPojo">
select mn.id id, mn.type type, mn.path path, mn.component component, mn.name name, mn.icon_cls iconCls, mn.permission permission,
mn.permission_note permissionNote, mn.parent_id parentId, mn.enabled enabled
from sys_menu mn
left join sys_pstn_prmsn spm on mn.id=spm.menu_id
left join sys_user su on spm.pstn_id=su.pstn_id
where su.username=#{username} and mn.type=0 and mn.enabled=1
select>
mapper>
3.2.4.6前端动态菜单构建
<el-menu class="el-menu-vertical-demo" background-color="#060037" text-color="#ffffff" :unique-opened="true" :router="true" :collapse="isCollapse">
<el-menu-item>
<img src="../assets/logo.png" width="24" height="24"/>
<span style="font-family: 楷体; font-size: 18px; color: #E3E63C; ">旭锋信息管理系统span>
el-menu-item>
<template v-for="(item, index) in menuData">
<el-menu-item v-if="!item.children" :index="item.path">
<i :class="item.iconCls">i>
<span slot="title" class="myAwe">{{item.name}}span>
el-menu-item>
<el-submenu v-else :index="'second'+index">
<template slot="title">
<i :class="item.iconCls">i>
<span slot="title" class="myAwe">{{item.name}}span>
template>
<template v-for="(subitem, subIndex) in item.children">
<el-menu-item v-if="!subitem.children" :index="subitem.path">
<i :class="subitem.iconCls">i>
<span slot="title" class="myAwe">{{subitem.name}}span>
el-menu-item>
<el-submenu v-if="subitem.children" :index="'third'+subIndex">
<template slot="title">
<i :class="subitem.iconCls">i>
<span slot="title" class="myAwe">{{subitem.name}}span>
template>
<template v-for="(thirdItem) in subitem.children">
<el-menu-item :index="thirdItem.path">
<i :class="thirdItem.iconCls">i>
<span slot="title" class="myAwe">{{thirdItem.name}}span>
el-menu-item>
template>
el-submenu>
template>
el-submenu>
template>
sessionStorage
中3.2.5.1 定义获取当前登陆用户的函数:在Home组件的methods属性中,获取成功后的响应后,将当前用户信息保存到sessionStorage
中。
/*重新获取当前用户信息包含用户拥有的授权标识*/
async reloadCurrentUser()
{
let username;
if(sessionStorage.getItem("username"))
{
username = sessionStorage.getItem("username")
}
const { data: res } = await this.$axios.get(`/relogin/${username}`);
if(2000 === res.status && res.data)
{
sessionStorage.setItem("currentUser", JSON.stringify(res.data));
}
else
{
this.$message.error(res.message);
}
},
3.2.5.2 在生命周期钩子created()
中发送该请求。
created(){
this.username = sessionStorage.getItem("username");
/*获取当前登陆用户信息*/
this.reloadCurrentUser();
/*获取构建当前用户左侧菜单栏的数据*/
this.getCurrentUserMenus();
/*当页面重新构建时即设置当前激活菜单项及获取面包屑相关数据*/
this.setDefaultsAliveMenuAndBreadData();
},
3.2.5.3 在LoginCtrller
中定义响应方法
@GetMapping("/relogin/{username}")
public ResPaging reloadCurrentUser(@PathVariable @Validated @NotEmpty String username)
{
SysUserPojo userDetails = loginService.reloadCurrentUser(username);
/*将当前用户信息也返回给前端*/
userDetails.setPassword(null);
return ResPaging.success("登陆成功!", userDetails);
}
3.2.5.4 在LoginSvsImpl中定义处理逻辑
@Override
public SysUserPojo reloadCurrentUser(String username)
{
return (SysUserPojo) userDetailService.loadUserByUsername(username);
}
定义保存默认激活菜单项和面包屑变量
data() {
return {
isCollapse: false,
username: "",
/*保存菜单数据*/
menuData: [],
/*保存默认选中菜单的index值*/
defaultMenu: "",
breadCrumbData: [],
};
定义设置默认激活菜单项和面包屑所需数据: homeVue的method属性中
/*设置默认激活菜单和面包屑数据*/
setDefaultsAliveMenuAndBreadData()
{
if(this.$route.meta.length >= 0)
{
this.$route.meta.forEach(item => {
if(item.type==0)
{
this.defaultMenu = item.path;
}
this.breadCrumbData.push(item.title);
})
}
}
监听路由组件变化,创建组件变化时设置面包屑及默认激活菜单数据。
/*Vue的监听器,当监听对象发生变化时,则自动调用回调函数*/
watch:{
/*监听路由组件变化*/
'$route'(to, from){
this.breadCrumbData = [];
this.setDefaultsAliveMenuAndBreadData();
}
}
created(){
let user = JSON.parse(sessionStorage.getItem("currentUser"));
this.username = user.username;
this.getCurrentUserMenus();
/*当页面重新构建时即设置当前激活菜单项及获取面包屑相关数据*/
this.setDefaultsAliveMenuAndBreadData();
},
<div style="height: 25px; background-color: #7dbcea; box-shadow: 0 4px 15px 5px #7dbcea;">
<el-row type="flex" justify="end">
<el-breadcrumb separator-class="el-icon-arrow-right" style="margin-top:6px; margin-right: 20px">
<el-breadcrumb-item v-for="(item, index) in breadCrumbData">{{item}}el-breadcrumb-item>
el-breadcrumb>
el-row>
div>
用户点击安全退出下拉菜单后
<el-dropdown trigger="click" @command="handleCommand" size="small">
/*当前用户下拉菜单选中处理方法*/
handleCommand(command)
{
//this.$message('click on item ' + command);
switch(command)
{
case "logout":
this.currentUserLogout();
break;
case "psnCenter":
break;
}
},
currentUserLogout()
业务流程
sessionStorage
退出成功
信息给用户。失败信息
给用户。具体实现
/*自定义退出方法*/
async currentUserLogout()
{
const {data : res} = await this.$axios.get("/logout");
if(2000 == res.status)
{
sessionStorage.clear();
this.$message.success(res.message);
await this.$router.push("/login");
}
else
{
this.$message.error(res.message);
}
}
5.2.4.1 定义自定义退出逻辑处理器
该处理器需要实现SpringSecurity
提供的LogoutHandler
接口
LogoutHandler
接口的public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
方法。
SpringSecurity
安全上下文。具体实现
package org.wjk.config.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*用户退出的业务处理方法*/
@Component
public class MyLogoutHandler implements LogoutHandler
{
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
{
SecurityContextHolder.clearContext();
}
}
5.2.4.2 定义自定义退出成功处理器
该处理器需要实现SpringSecurity
提供的LogoutSuccessHandler
接口
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
方法。
具体实现
package org.wjk.config.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import org.wjk.utils.JsonResUtils;
import org.wjk.vo.ResPaging;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler
{
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
{
JsonResUtils.response(request, response, ResPaging.success("退出成功!"));
}
}
5.2.4.3 配置SpringSecurity接收退出请求并设置自定义退出处理和退出成功处理供SpringSecurity退出调用。
配置SpringSecurity在自定义MySecurityConfig
配置类
protected void configure(HttpSecurity http) throws Exception
方法具体实现
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
/*配置所有请求都需要登录认证*/
.anyRequest().authenticated()
.and()
/*配置自定义退出逻辑*/
.logout().permitAll()
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler(logoutSuccessHandler)
.and()
/*关闭跨站请求伪造*/
.csrf().disable()
/*配置session管理器*/
.sessionManagement()
/*由于使用jwt令牌来保存用户登录信息,因此不需要使用创建session来保存用户登录信息*/
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
/*将token校验过滤器添加在认证过滤器前*/
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
;
}
客户端在页面跳转前,校验用户是否有跳转页面的访问权限。
如果有,则页面跳转。
如果没有,则页面不跳转。
当客户端请求服务器资源时,校验登录用户是否有访问该资源的权限。
6.2.1.1 在路由管理器组件中,为需要权限的路由组件配置meta属性,属性中包含所需的权限标识。
具体实现如下:
const routes = [
{
path: '/',
redirect: "/login",
},
{
path: "/login",
name: "Login",
component: Login
},
{
path: "/home",
name: "Home",
component: Home,
children: [
{
path: "/menu",
component: _ => import("../views/menu/menumngr"),
meta: [{title: "系统设置"},{title: "菜单管理", permission: "sys:menu:mngr" /*配置权限标识*/, type: 0, path: "/menu"}]
}
]
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
6.2.1.2 在前置路由导航守卫中校验权限
具体实现如下:
router.beforeEach((to, from, next) => {
let token = sessionStorage.getItem("token");
let permissions;
if(JSON.parse(sessionStorage.getItem("currentUser")))
{
permissions = JSON.parse(sessionStorage.getItem("currentUser")).permissions;
}
let toPermission;
if(Object.keys(to.meta).length !== 0)
{
toPermission = to.meta[to.meta.length - 1].permission;
}
if("/login" === to.path || (token && token.trim().length > 0))
{
if(to.path !== "/login" && permissions && toPermission && !permissions.includes(toPermission))
{
Message.error("您无访问该资源的权限,请联系管理员!");
next(from.path);
}
next();
}
else
next("/login");
})
解决vue-router连接两次跳转到相同路径时,报错的问题。
当vue-router连接两次跳转到相同路径时,将报错如下错误
Error: Redirected when going from "/home" to "/menu" via a navigation guard.
Error: Navigation cancelled from "/home" to "/menu" with a new navigation.
解决方式,在路由管理器组件的index.js文件中,添加如下代码:
Vue.use(VueRouter)
//解决编程式路由往同一地址跳转时会报错的情况
const originalPush = VueRouter.prototype.push;
const originalReplace = VueRouter.prototype.replace;
//push
VueRouter.prototype.push = function push (location, onResolve, onReject) {
// if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => {return;})
}
// replace
VueRouter.prototype.replace = function replace (location, onResolve, onReject) {
// if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)
return originalReplace.call(this, location).catch(err => {return;})
}
6.2.2.1 开启SpringSecurity权限
使用@EnableWebSecurity
及@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)
描述自定义SpringSecurity配置类。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter
{
......
}
6.2.2.2 标识方法所需权限
使用@PreAuthorize("hasAuthority('sys:menu:mngr')")
注解描述需要权限访问的controller方法。如:
@GetMapping("/usermenu/{userId}")
@PreAuthorize("hasAuthority('sys:menu:mngr')")// 该注解说明,要访问此方法的用户必须有sys:menu:mngr标识的权限
public ResPaging getCurrentUserMenus(@PathVariable @Validated @NotNull Integer userId)
{
return ResPaging.success("获取成功", sysMenuSvc.getCurrentUserMenus(userId));
}
6.2.2.3 处理权限不足
当用户无权访问,SpringSecurity将抛出AccessDeniedException
异常。
全局异常处理该异常。
@ExceptionHandler(RuntimeException.class)
public ResPaging MyRuntimeException(RuntimeException exception)
{
exception.printStackTrace();
if(exception instanceof UsernameNotFoundException)
return ResPaging.failture(ExcInfo.ILLEGAL_PASSWORD);
else if(exception instanceof LockedException)
return ResPaging.failture(ExcInfo.ACCOUNT_LOCKED);
else if(exception instanceof DisabledException)
return ResPaging.failture(ExcInfo.ACCOUNT_NOT_ENABLED);
/*处理权限不足异常*/
else if(exception instanceof AccessDeniedException)
return ResPaging.failture(ExcInfo.PERMISSION_DENIED);
else
return ResPaging.failture(ExcInfo.UNKNOW_EXCEPTION);
}
6.2.2.4 原始 定义权限不足时的处理逻辑即配置
定义时机:在使用SpringSecurity完成登录时,而非自定义登录逻辑时,需要自定义权限不足时的处理逻辑
当用户没有权限访问资源时,SpringSecurity则会回调该类的方法。
自定义权限不足的处理逻辑必须实现AccessDeniedHandler
,并重写public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException
方法。
主要作用:响应前端用户权限不足。
权限不足的处理逻辑
package org.wjk.config.security;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.wjk.exception.ExcInfo;
import org.wjk.utils.JsonResUtils;
import org.wjk.vo.ResPaging;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler
{
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException
{
JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.PERMISSION_DENIED));
}
}
配置权限不足的处理逻辑
/*对http请求认证授权配置*/
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
/*配置所有请求都需要登录认证*/
.anyRequest().authenticated()
.and()
/*配置自定义退出逻辑*/
.logout().permitAll()
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler(logoutSuccessHandler)
.and()
/*关闭跨站请求伪造*/
.csrf().disable()
/*配置session管理器*/
.sessionManagement()
/*由于使用jwt令牌来保存用户登录信息,因此不需要使用创建session来保存用户登录信息*/
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
/*处理各类SpringSecurity抛出的异常*/
.exceptionHandling()
/*处理权限不足的异常*/
.accessDeniedHandler(accessDeniedHandler)
.and()
/*将token校验过滤器添加在认证过滤器前*/
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
;
}
处理方法,通过axios响应拦截器处理。
具体实现
axios.interceptors.response.use(res => {
/*处理token过期的处理逻辑*/
if(5005 === res.data.status || 5006 === res.data.status)
{
Modal.error({
title: "旭峰科技信息管理系统",
content: res.data.message,
onOk(){
sessionStorage.clear();
router.push("/login").then(r => res);
}
})
}
/*拦截权限不足处理逻辑*/
if(5014 === res.data.status)
{
Message.error(res.data.message);
}
return res;
})