security底层
Web Security Configurer 适配器
import com.ruoyi.framework.config.properties.PermitAllUrlProperties; import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.filter.CorsFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; /** * spring security配置 * Web Security Configurer 适配器 * @author ruoyi */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** *token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 允许匿名访问的地址 */ @Autowired private PermitAllUrlProperties permitAllUrl; /** * 解决 无法直接煮热的问题 AuthenticationManager * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManager() throws Exception{ return super.authenticationManagerBean(); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ //允许匿名访问的url ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); httpSecurity //CSRF禁用,因为不支持session .csrf().disable() //禁用HTTP响应标头 .headers().cacheControl().disable().and() //认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() //基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //过滤请求 .authorizeRequests() //对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login","/register","/captchaImage").permitAll() //静态资源,可以匿名访问 .antMatchers(HttpMethod.GET,"/","/*.html","/**/*.html","/**/*.css","/**/*.js","/profile/**").permitAll() .antMatchers("/swagger-ui.html","/swagger-resources/**","/webjars/**","/*/api-docs","/druid/**").permitAll() //除了上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() //表示禁用HTTP响应标头。HTTP响应标头是HTTP响应中包含的信息。用于描述响应的内容。HTTP响应标头可以包含各种信息,列如内容类型、内容长度、缓存控制等 .headers().frameOptions().disable(); //添加Logout filter httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); //添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //添加CORS filter corsFilter跨域过滤器 httpSecurity.addFilterBefore(corsFilter,JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); } /** * 强散列哈希加密实现 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();} /** * 身份认证接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{ auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } }
security的流程
系统首选加载的时Security配置类,SecurityConfig配置类会继承,WebSecurityConfigureAdapter这个类,重写configure()方法,security框架是使用表单登录的,表单登录方法是formLogin(),表单登录的配置文件FormLoiginConfigurer里有一个这个UsernamePasswordAuthenticationFilter用户名密码认证过滤器,这个里面先执行身份验证的方法,这个方法先进行判断里面请求方式,如果不是POST请求,就会去报异常、是POST请求的话,我需要先获取这个用户名和密码,trim该方法用于去除字符串两端的空格。进行创建一个身份验证令牌这个令牌叫UsernamePasswordAuthenticationToken,里面封装了这个username和password,并且还设置了身份验证的详细信息setDetails,然后获取身份验证管理器去对这个令牌进行身份验证,最终调用的是接口AuthenticationManager中的authenticate方法,不支持token的认证,providers提供了匿名的各种认证器,然后调用 ProviderManager父类ProviderManager(父类和自己类型一样)的认证方法,继续调用providers又回到authenticate,获取这个用户的主体,然后继续调用authenticate认证方法,这里的provider用的是DaoAuthenticationProvider,最终调用的就是我们写的UserDetailsServiceImpl方法的loadUserByUsername(username)方法,这里只是查询到了用户的信息,三次输错的话,会报异常,和当前数据库里的密码进行比对,调用 check方法检查查询到的用户信息,用户是否可用,用户是否过期,比对成功(AuthenticationProvider)认证成功后封装Authentication并设置好用户的信息(用户名,密码,权限等)返回,最后返回到UsernamePasswordAuthenticationFilter,不匹配的话就是密码错误。
防止重复提交拦截器(前后端都有)
1.RepeatSubmitinterceptor
2.Controller上自己写的RepeatSubmit注解,叫自定义注解防止表单重复提交
(1)间隔时间,小于此时间为重复提交
(2)提示消息,返回一个不允许重复提交,请稍后再试
3.作用,干嘛的
前端也需要写个拦截器,要给后端传值,如果没传,你代码就报异常了,你得需要考虑代码的见状性,这个拦截器是抽象类
4.HandlerInterceptor :处理程序拦截
前置方法preHandle,里面我们可以请求这个静态资源、具体的接口和方法,做一个判断,不是接口,方法,静态资源直接放行,你访问的是HandlerMethod类型,意思是一个方法的话,Mothod整个方法对象,获取注解(RepeatSubmit.class),为什么拿这个注解呢?有一些接口需要的那就不是null,将这个接口抽取到拦截器中统一处理对业务代码是解耦,进入这个方法isRepeatSubmit方法(验证是否重复提交由子类实现具体的放重复提交的规则),封装所有的参数,获取一个请求地址,唯一标识拼接(指定的key + url + submitKey),这个是从这个redis中查询取值(也可以从数据库中)。不是空,进这个if判断,这里面的就是判断这个时间差是否小于你设定的时间,大于直接放行。最后有一些接口不需要的那是null,不需要不打这个注解就行了,
防止重复提交拦截器的主体
package com.ruoyi.framework.interceptor; import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import com.alibaba.fastjson2.JSON; import com.ruoyi.common.annotation.RepeatSubmit; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.ServletUtils; /** * 防止重复提交拦截器 * * @author */ /** * RepeatSubmitInterceptor.java 是一个拦截器,用于防止重复提交表单。 * 它在请求被处理之前拦截,并检查被调用的方法是否有 @RepeatSubmit 注解。 * 如果有,拦截器会验证请求是否为重复提交,并返回错误响应。 * 该拦截器仅适用于 HandlerMethod,并且在被调用的方法没有 @RepeatSubmit 注解或请求不是重复提交时返回 true。 * 否则,返回 false 并发送错误响应。 */ @Component public abstract class RepeatSubmitInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request, annotation)) { // 创建 JSON 格式的错误信息 AjaxResult ajaxResult = AjaxResult.error(annotation.message()); // 将错误信息渲染为字符串并作为响应发送 ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); return false; } } return true; } else { return true; } } /** * 验证是否重复提交由子类实现具体的防重复提交的规则 * * @param request 请求信息 * @param annotation 防重复注解参数 * @return 结果 * @throws Exception */ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); }
package com.ruoyi.framework.interceptor.impl; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import com.alibaba.fastjson2.JSON; import com.ruoyi.common.annotation.RepeatSubmit; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.filter.RepeatedlyRequestWrapper; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.http.HttpHelper; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; /** * 判断请求url和数据是否和上一次相同, * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 * * @author ruoyi */ @Component public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_TIME = "repeatTime"; // 令牌自定义标识 @Value("${token.header}") private String header; @Autowired private RedisCache redisCache; @SuppressWarnings("unchecked") @Override public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) { String nowParams = ""; if (request instanceof RepeatedlyRequestWrapper) { RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; nowParams = HttpHelper.getBodyString(repeatedlyRequest); } // body参数为空,获取Parameter的数据 if (StringUtils.isEmpty(nowParams)) { nowParams = JSON.toJSONString(request.getParameterMap()); } Map<String, Object> nowDataMap = new HashMap<String, Object>(); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放cache的key值) String url = request.getRequestURI(); // 唯一值(没有消息头则使用请求地址) String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); // 唯一标识(指定key + url + 消息头) String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; // 从缓存中获取会话对象 Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); if (sessionObj != null) { // 将会话对象转换为 Map Map<String, Object> sessionMap = (Map<String, Object>) sessionObj; // 检查会话 Map 是否包含当前 URL if (sessionMap.containsKey(url)) { // 获取之前访问的数据 Map Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url); // 比较当前数据和之前访问的数据的参数和时间 if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) { // 如果是重复请求,返回 true return true; } } } // 如果之前没有访问过相同的数据,将当前数据添加到缓存 Map 中 Map<String, Object> cacheMap = new HashMap<String, Object>(); cacheMap.put(url, nowDataMap); // 将缓存 Map 存储到缓存中,并设置过期时间 redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); // 返回 false 表示不是重复请求 return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 - time2) < interval) { return true; } return false; } }
自定义注解防止表单重复提交
package com.ruoyi.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解防止表单重复提交
*
* @author ruoyi
*
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
/**
* 提示消息
*/
public String message() default "不允许重复提交,请稍候再试";
}
自定义注解实现日志输出
自定义操作日志记录注解
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.enums.OperatorType;
import java.lang.annotation.*;
/**
* 自定义操作日志记录注解
*/
@Target({ElementType.PARAMETER,ElementType.METHOD})
//@Target 注解用于指定注解可以应用的目标元素类型。在这个例子中, @Log注解:可以应用在方法和参数上。
@Retention(RetentionPolicy.RUNTIME)
//@Retention 注解用于指定注解的保留策略,即注解在什么时候生效。在这个例子中, @Log注解在运行时保留,通过反射机制读取。
@Documented
// @Documented 注解用于指定注解是否要包含在生成的文档中。在这个例子中,如果生成文档时包含了@Log注解的使用,它会被包含在文档中。
public @interface Log
//@interface注解用于定义一个自定义注解。在这个例子中, @Log注解用于标记需要记录日志的方法或参数。
{
/**
* 模块
*/
String title()default "";
/**
* 功能
*/
BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
boolean isSaveResponseData() default true;
/**
* 排除指定的请求参数
*/
String[] excludeParamNames() default {};
}
/**
* 操作人类别
*
* @author ruoyi
*/
public enum OperatorType
{
/**
* 其它
*/
OTHER,
/**
* 后台用户
*/
MANAGE,
/**
* 手机端用户
*/
MOBILE
}
/**
* 业务操作类型
*
* @author ruoyi
*/
public enum BusinessType
{
/**
* 其它
*/
OTHER,
/**
* 新增
*/
INSERT,
/**
* 修改
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 授权
*/
GRANT,
/**
* 导出
*/
EXPORT,
/**
* 导入
*/
IMPORT,
/**
* 强退
*/
FORCE,
/**
* 生成代码
*/
GENCODE,
/**
* 清空数据
*/
CLEAN,
}
操作日志记录处理
@Aspect @Component public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); /** * 排除敏感属性字段 */ public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"}; /** * 计算操作消耗时间 */ private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time"); /** * 处理请求前执行 */ @Before(value = "@annotation(controllerLog)") public void boBefore(JoinPoint joinPoint, Log controllerLog) { TIME_THREADLOCAL.set(System.currentTimeMillis()); } /** * 处理完请求后执行 * * @param joinPoint 切点 */ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) { handleLog(joinPoint, controllerLog, null, jsonResult); } /** * 拦截异常操作 * * @param joinPoint 切点 * @param e 异常 */ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) { handleLog(joinPoint, controllerLog, e, null); } protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) { try { // 获取当前的用户 LoginUser loginUser = SecurityUtils.getLoginUser(); // *========数据库日志=========*// SysOperLog operLog = new SysOperLog(); operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 请求的地址 String ip = IpUtils.getIpAddr(); operLog.setOperIp(ip); operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); if (loginUser != null) { operLog.setOperName(loginUser.getUsername()); SysUser currentUser = loginUser.getUser(); if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept())) { operLog.setDeptName(currentUser.getDept().getDeptName()); } } if (e != null) { operLog.setStatus(BusinessStatus.FAIL.ordinal()); operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 设置方法名称 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); operLog.setMethod(className + "." + methodName + "()"); // 设置请求方式 operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); // 处理设置注解上的参数 getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); // 设置消耗时间 operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); // 保存数据库 AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); } catch (Exception exp) { // 记录本地异常日志 log.error("异常信息:{}", exp.getMessage()); exp.printStackTrace(); } finally { TIME_THREADLOCAL.remove(); } } /** * 获取注解中对方法的描述信息 用于Controller层注解 * * @param log 日志 * @param operLog 操作日志 * @throws Exception */ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception { // 设置action动作 operLog.setBusinessType(log.businessType().ordinal()); // 设置标题 operLog.setTitle(log.title()); // 设置操作人类别 operLog.setOperatorType(log.operatorType().ordinal()); // 是否需要保存request,参数和值 if (log.isSaveRequestData()) { // 获取参数的信息,传入到数据库中。 setRequestValue(joinPoint, operLog, log.excludeParamNames()); } // 是否需要保存response,参数和值 if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) { operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); } } /** * 获取请求的参数,放到log中 * * @param operLog 操作日志 * @throws Exception 异常 */ private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception { Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); String requestMethod = operLog.getRequestMethod(); if (StringUtils.isEmpty(paramsMap) && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) { String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); operLog.setOperParam(StringUtils.substring(params, 0, 2000)); } else { operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000)); } } /** * 参数拼装 */ private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) { String params = ""; if (paramsArray != null && paramsArray.length > 0) { for (Object o : paramsArray) { if (StringUtils.isNotNull(o) && !isFilterObject(o)) { try { String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); params += jsonObj.toString() + " "; } catch (Exception e) { } } } } return params.trim(); }
判断请求url和数据是否和上一次相同,如果和上次相同,则是重复提交表单。 有效时间为10秒内。
import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import com.alibaba.fastjson2.JSON; import com.ruoyi.common.annotation.RepeatSubmit; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.filter.RepeatedlyRequestWrapper; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.http.HttpHelper; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; /** * 判断请求url和数据是否和上一次相同, * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 */ @Component public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_TIME = "repeatTime"; // 令牌自定义标识 @Value("${token.header}") private String header; @Autowired private RedisCache redisCache; @SuppressWarnings("unchecked") @Override public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) { String nowParams = ""; if (request instanceof RepeatedlyRequestWrapper) { RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; nowParams = HttpHelper.getBodyString(repeatedlyRequest); } // body参数为空,获取Parameter的数据 if (StringUtils.isEmpty(nowParams)) { nowParams = JSON.toJSONString(request.getParameterMap()); } Map<String, Object> nowDataMap = new HashMap<String, Object>(); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放cache的key值) String url = request.getRequestURI(); // 唯一值(没有消息头则使用请求地址) String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); // 唯一标识(指定key + url + 消息头) String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; // 从缓存中获取会话对象 Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); if (sessionObj != null) { // 将会话对象转换为 Map Map<String, Object> sessionMap = (Map<String, Object>) sessionObj; // 检查会话 Map 是否包含当前 URL if (sessionMap.containsKey(url)) { // 获取之前访问的数据 Map Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url); // 比较当前数据和之前访问的数据的参数和时间 if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) { // 如果是重复请求,返回 true return true; } } } // 如果之前没有访问过相同的数据,将当前数据添加到缓存 Map 中 Map<String, Object> cacheMap = new HashMap<String, Object>(); cacheMap.put(url, nowDataMap); // 将缓存 Map 存储到缓存中,并设置过期时间 redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); // 返回 false 表示不是重复请求 return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 - time2) < interval) { return true; } return false; } }
异步任务管理器
import java.util.TimerTask; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import com.ruoyi.common.utils.Threads; import com.ruoyi.common.utils.spring.SpringUtils; /** * 异步任务管理器 */ public class AsyncManager { /** * 操作延迟10毫秒 */ private final int OPERATE_DELAY_TIME = 10; /** * 异步操作任务调度线程池 */ private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); /** * 单例模式 */ private AsyncManager() { } private static AsyncManager me = new AsyncManager(); public static AsyncManager me() { return me; } /** * 执行任务 * * @param task 任务 */ public void execute(TimerTask task) { executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); } /** * 停止任务线程池 */ public void shutdown() { Threads.shutdownAndAwaitTermination(executor); } }
线程池配置
import com.ruoyi.common.utils.Threads; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; /** * 线程池配置 * * @author ruoyi **/ @Configuration class ThreadPoolConfig { // 核心线程池大小 private int corePoolSize = 50; // 最大可创建的线程数 private int maxPoolSize = 200; // 队列最大长度 private int queueCapacity = 1000; // 线程池维护线程所允许的空闲时间 private int keepAliveSeconds = 300; @Bean(name = "threadPoolTaskExecutor") public ThreadPoolTaskExecutor threadPoolTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setMaxPoolSize(maxPoolSize); executor.setCorePoolSize(corePoolSize); executor.setQueueCapacity(queueCapacity); executor.setKeepAliveSeconds(keepAliveSeconds); // 线程池对拒绝任务(无线程可用)的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; } /** * 执行周期性或定时任务 */ @Bean(name = "scheduledExecutorService") protected ScheduledExecutorService scheduledExecutorService() { return new ScheduledThreadPoolExecutor(corePoolSize, new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), new ThreadPoolExecutor.CallerRunsPolicy()) { @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); Threads.printException(r, t); } }; } }
登录技术栈
在上面登录流程中,登录成功后,最后会给前端返回一个token,会调用TokenService的createToken方法: 如下
token验证处理
ID生成器工具类 提供通用唯一识别码(universally unique identifier)(UUID)实现
/** * ID生成器工具类 * * @author ruoyi */ public class IdUtils { /** * 获取随机UUID * * @return 随机UUID */ public static String randomUUID() { return UUID.randomUUID().toString(); } /** * 简化的UUID,去掉了横线 * * @return 简化的UUID,去掉了横线 */ public static String simpleUUID() { return UUID.randomUUID().toString(true); } /** * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID * * @return 随机UUID */ public static String fastUUID() { return UUID.fastUUID().toString(); } /** * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID * * @return 简化的UUID,去掉了横线 */ public static String fastSimpleUUID() { return UUID.fastUUID().toString(true); } } package com.ruoyi.common.utils.uuid; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import com.ruoyi.common.exception.UtilException; /** * 提供通用唯一识别码(universally unique identifier)(UUID)实现 * * @author ruoyi */ public final class UUID implements java.io.Serializable, Comparable<UUID> { private static final long serialVersionUID = -1185015143654744140L; /** * SecureRandom 的单例 * */ private static class Holder { static final SecureRandom numberGenerator = getSecureRandom(); } /** 此UUID的最高64有效位 */ private final long mostSigBits; /** 此UUID的最低64有效位 */ private final long leastSigBits; /** * 私有构造 * * @param data 数据 */ private UUID(byte[] data) { long msb = 0; long lsb = 0; assert data.length == 16 : "data must be 16 bytes in length"; for (int i = 0; i < 8; i++) { msb = (msb << 8) | (data[i] & 0xff); } for (int i = 8; i < 16; i++) { lsb = (lsb << 8) | (data[i] & 0xff); } this.mostSigBits = msb; this.leastSigBits = lsb; } /** * 使用指定的数据构造新的 UUID。 * * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 */ public UUID(long mostSigBits, long leastSigBits) { this.mostSigBits = mostSigBits; this.leastSigBits = leastSigBits; } /** * 获取类型 4(伪随机生成的)UUID 的静态工厂。 * * @return 随机生成的 {@code UUID} */ public static UUID fastUUID() { return randomUUID(false); } /** * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 * * @return 随机生成的 {@code UUID} */ public static UUID randomUUID() { return randomUUID(true); } /** * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 * * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 * @return 随机生成的 {@code UUID} */ public static UUID randomUUID(boolean isSecure) { final Random ng = isSecure ? Holder.numberGenerator : getRandom(); byte[] randomBytes = new byte[16]; ng.nextBytes(randomBytes); randomBytes[6] &= 0x0f; /* clear version */ randomBytes[6] |= 0x40; /* set to version 4 */ randomBytes[8] &= 0x3f; /* clear variant */ randomBytes[8] |= 0x80; /* set to IETF variant */ return new UUID(randomBytes); } /** * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 * * @param name 用于构造 UUID 的字节数组。 * * @return 根据指定数组生成的 {@code UUID} */ public static UUID nameUUIDFromBytes(byte[] name) { MessageDigest md; try { md = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException nsae) { throw new InternalError("MD5 not supported"); } byte[] md5Bytes = md.digest(name); md5Bytes[6] &= 0x0f; /* clear version */ md5Bytes[6] |= 0x30; /* set to version 3 */ md5Bytes[8] &= 0x3f; /* clear variant */ md5Bytes[8] |= 0x80; /* set to IETF variant */ return new UUID(md5Bytes); } /** * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 * * @param name 指定 {@code UUID} 字符串 * @return 具有指定值的 {@code UUID} * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 * */ public static UUID fromString(String name) { String[] components = name.split("-"); if (components.length != 5) { throw new IllegalArgumentException("Invalid UUID string: " + name); } for (int i = 0; i < 5; i++) { components[i] = "0x" + components[i]; } long mostSigBits = Long.decode(components[0]).longValue(); mostSigBits <<= 16; mostSigBits |= Long.decode(components[1]).longValue(); mostSigBits <<= 16; mostSigBits |= Long.decode(components[2]).longValue(); long leastSigBits = Long.decode(components[3]).longValue(); leastSigBits <<= 48; leastSigBits |= Long.decode(components[4]).longValue(); return new UUID(mostSigBits, leastSigBits); } /** * 返回此 UUID 的 128 位值中的最低有效 64 位。 * * @return 此 UUID 的 128 位值中的最低有效 64 位。 */ public long getLeastSignificantBits() { return leastSigBits; } /** * 返回此 UUID 的 128 位值中的最高有效 64 位。 * * @return 此 UUID 的 128 位值中最高有效 64 位。 */ public long getMostSignificantBits() { return mostSigBits; } /** * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 * <p> * 版本号具有以下含意: * <ul> * <li>1 基于时间的 UUID * <li>2 DCE 安全 UUID * <li>3 基于名称的 UUID * <li>4 随机生成的 UUID * </ul> * * @return 此 {@code UUID} 的版本号 */ public int version() { // Version is bits masked by 0x000000000000F000 in MS long return (int) ((mostSigBits >> 12) & 0x0f); } /** * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 * <p> * 变体号具有以下含意: * <ul> * <li>0 为 NCS 向后兼容保留 * <li>2 <a href="http://www.ietf.org/rfc/rfc4122.txt">IETF RFC 4122</a>(Leach-Salz), 用于此类 * <li>6 保留,微软向后兼容 * <li>7 保留供以后定义使用 * </ul> * * @return 此 {@code UUID} 相关联的变体号 */ public int variant() { // This field is composed of a varying number of bits. // 0 - - Reserved for NCS backward compatibility // 1 0 - The IETF aka Leach-Salz variant (used by this class) // 1 1 0 Reserved, Microsoft backward compatibility // 1 1 1 Reserved for future definition. return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); } /** * 与此 UUID 相关联的时间戳值。 * * <p> * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。<br> * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 * * <p> * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。<br> * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 * * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 */ public long timestamp() throws UnsupportedOperationException { checkTimeBase(); return (mostSigBits & 0x0FFFL) << 48// | ((mostSigBits >> 16) & 0x0FFFFL) << 32// | mostSigBits >>> 32; } /** * 与此 UUID 相关联的时钟序列值。 * * <p> * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 * <p> * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 * UnsupportedOperationException。 * * @return 此 {@code UUID} 的时钟序列 * * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 */ public int clockSequence() throws UnsupportedOperationException { checkTimeBase(); return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); } /** * 与此 UUID 相关的节点值。 * * <p> * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 * <p> * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。<br> * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 * * @return 此 {@code UUID} 的节点值 * * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 */ public long node() throws UnsupportedOperationException { checkTimeBase(); return leastSigBits & 0x0000FFFFFFFFFFFFL; } /** * 返回此{@code UUID} 的字符串表现形式。 * * <p> * UUID 的字符串表示形式由此 BNF 描述: * * <pre> * {@code * UUID = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node> * time_low = 4*<hexOctet> * time_mid = 2*<hexOctet> * time_high_and_version = 2*<hexOctet> * variant_and_sequence = 2*<hexOctet> * node = 6*<hexOctet> * hexOctet = <hexDigit><hexDigit> * hexDigit = [0-9a-fA-F] * } * </pre> * * </blockquote> * * @return 此{@code UUID} 的字符串表现形式 * @see #toString(boolean) */ @Override public String toString() { return toString(false); } /** * 返回此{@code UUID} 的字符串表现形式。 * * <p> * UUID 的字符串表示形式由此 BNF 描述: * * <pre> * {@code * UUID = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node> * time_low = 4*<hexOctet> * time_mid = 2*<hexOctet> * time_high_and_version = 2*<hexOctet> * variant_and_sequence = 2*<hexOctet> * node = 6*<hexOctet> * hexOctet = <hexDigit><hexDigit> * hexDigit = [0-9a-fA-F] * } * </pre> * * </blockquote> * * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 * @return 此{@code UUID} 的字符串表现形式 */ public String toString(boolean isSimple) { final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); // time_low builder.append(digits(mostSigBits >> 32, 8)); if (!isSimple) { builder.append('-'); } // time_mid builder.append(digits(mostSigBits >> 16, 4)); if (!isSimple) { builder.append('-'); } // time_high_and_version builder.append(digits(mostSigBits, 4)); if (!isSimple) { builder.append('-'); } // variant_and_sequence builder.append(digits(leastSigBits >> 48, 4)); if (!isSimple) { builder.append('-'); } // node builder.append(digits(leastSigBits, 12)); return builder.toString(); } /** * 返回此 UUID 的哈希码。 * * @return UUID 的哈希码值。 */ @Override public int hashCode() { long hilo = mostSigBits ^ leastSigBits; return ((int) (hilo >> 32)) ^ (int) hilo; } /** * 将此对象与指定对象比较。 * <p> * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 * * @param obj 要与之比较的对象 * * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} */ @Override public boolean equals(Object obj) { if ((null == obj) || (obj.getClass() != UUID.class)) { return false; } UUID id = (UUID) obj; return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); } // Comparison Operations /** * 将此 UUID 与指定的 UUID 比较。 * * <p> * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 * * @param val 与此 UUID 比较的 UUID * * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 * */ @Override public int compareTo(UUID val) { // The ordering is intentionally set up so that the UUIDs // can simply be numerically compared as two numbers return (this.mostSigBits < val.mostSigBits ? -1 : // (this.mostSigBits > val.mostSigBits ? 1 : // (this.leastSigBits < val.leastSigBits ? -1 : // (this.leastSigBits > val.leastSigBits ? 1 : // 0)))); } // ------------------------------------------------------------------------------------------------------------------- // Private method start /** * 返回指定数字对应的hex值 * * @param val 值 * @param digits 位 * @return 值 */ private static String digits(long val, int digits) { long hi = 1L << (digits * 4); return Long.toHexString(hi | (val & (hi - 1))).substring(1); } /** * 检查是否为time-based版本UUID */ private void checkTimeBase() { if (version() != 1) { throw new UnsupportedOperationException("Not a time-based UUID"); } } /** * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) * * @return {@link SecureRandom} */ public static SecureRandom getSecureRandom() { try { return SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException e) { throw new UtilException(e); } } /** * 获取随机数生成器对象<br> * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 * * @return {@link ThreadLocalRandom} */ public static ThreadLocalRandom getRandom() { return ThreadLocalRandom.current(); } }
/** * token验证处理 * * @author ruoyi */ @Component public class TokenService { private static final Logger log = LoggerFactory.getLogger(TokenService.class); //令牌自定义标识 @Value("${token.header}") private String header; //令牌秘钥 @Value("${token.expireTime}") private String secret; //令牌有效期(默认30分钟) @Value("${token.expireTime}") private int expireTime; protected static final long MILLIS_SECOND = 1000; protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; @Autowired private RedisCache redisCache; /** * 获取用户身份信息 * @param request HttpServletRequest对象 * @return 用户信息 */ public LoginUser getLoginUser(HttpServletRequest request) { // 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)){ try{ //解析令牌 Claims claims = parseToken(token); //根据token获取权限以及用户所对应信息,从claims获取用户的uuid String uuid = (String) claims.get (Constants.LOGIN_USER_KEY); //业务前缀和uuid 拼接成redis中的key String userKey = getTokenKey(uuid); //redis中根据key获取用户对象 LoginUser user = redisCache.getCacheObject(userKey); return user; }catch (Exception e){ log.error("获取用户信息异常'{}'",e.getMessage()); } } return null; } /** * 设置用户身份信息 */ public void setLoginUser(LoginUser loginUser){ if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())){ refreshToken(loginUser); } } /** * 删除用户身份信息 */ public void delLoginUser(String token){ if (StringUtils.isNotEmpty(token)){ String userKey = getTokenKey(token); redisCache.deleteObject(userKey); } } /** * 创建令牌 * * @param loginUser 用户信息 * @return 令牌 */ public String createToken(LoginUser loginUser){ //生成一个唯一的令牌 String token = IdUtils.fastUUID(); //将令牌设置到登录用户对象中 loginUser.setToken(token); //设置用户代理信息 setUserAgent(loginUser); //刷新令牌 refreshToken(loginUser); // 创建一个包含令牌信息的Map对象 Map<String,Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY,token); //调用createToken方法生成最终的令牌 return createToken(claims); } /** * 验证令牌有效期,相差不足20分钟,自动刷新缓存 * * @param loginUser * @return 令牌 */ public void verifyToken(LoginUser loginUser){ long expireTime = loginUser.getExpireTime(); long currentTime = System.currentTimeMillis(); if (expireTime - currentTime <=MILLIS_MINUTE_TEN){ refreshToken(loginUser); } } /** * 刷新令牌有效期 * * @param loginUser 登录信息 */ public void refreshToken(LoginUser loginUser){ loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); //根据uuid 将loginUser缓存、 String userToken = getTokenKey(loginUser.getToken()); redisCache.setCacheObject(userToken,loginUser,expireTime,TimeUnit.MINUTES); } /** * 设置用户代理信息 * * @param loginUser 登录信息 */ private void setUserAgent(LoginUser loginUser) { UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("uSER-Agent")); String ip = IpUtils.getIpAddr(); loginUser.setIpaddr(ip); loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); loginUser.setBrowser(userAgent.getBrowser().getName()); loginUser.setOs(userAgent.getOperatingSystem().getName()); } /** * 从数据声明生成令牌 * * @param claims 数据声明 * @return 令牌 */ private String createToken(Map<String,Object> claims){ String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; } /** * 从令牌中获取数据声明 * * @param token 令牌 * @return 数据声明 */ private Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } /** * 从令牌中获取用户名 * * @param token 令牌 * @return 用户名 */ public String getUsernameFromToken(String token){ Claims claims = parseToken(token); return claims.getSubject(); } /** * 获取请求token * * @param request * @return token */ private String getToken(HttpServletRequest request) { String token = request.getHeader(header); if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)){ token = token.replace(Constants.TOKEN_PREFIX, ""); } return token; } private String getTokenKey(String uuid) { return CacheConstants.LOGIN_TOKEN_KEY + uuid; } }
token配置文件
#token配置 token: #令牌自定义标识 header: Authorization #令牌密钥 secret: abcdefghijklmnopqrstuvwxyz #令牌有效期(默认30分钟) expireTime: 30
spring redis 工具类
import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; /** * spring redis 工具类 * * @author ruoyi **/ @SuppressWarnings(value = { "unchecked", "rawtypes" }) @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获取有效时间 * * @param key Redis键 * @return 有效时间 */ public long getExpire(final String key) { return redisTemplate.getExpire(key); } /** * 判断 key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public Boolean hasKey(String key) { return redisTemplate.hasKey(key); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public boolean deleteObject(final Collection collection) { return redisTemplate.delete(collection) > 0; } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 删除Hash中的某条数据 * * @param key Redis键 * @param hKey Hash键 * @return 是否成功 */ public boolean deleteCacheMapValue(final String key, final String hKey) { return redisTemplate.opsForHash().delete(key, hKey) > 0; } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }
token过滤器 验证token有效性
import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.web.service.TokenService; /** * token过滤器 验证token有效性 * * @author ruoyi */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{ @Autowired private TokenService tokenService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //从请求头中获取用户对象 LoginUser loginUser = tokenService.getLoginUser(request); if (StringUtils.isNotNull(loginUser) && StringUtils.isNotNull(SecurityUtils.getAuthentication())){ //校验令牌 tokenService.verifyToken(loginUser); //封装token(和登陆的时候封装不一样,使用用户名和权限封装的) UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); //WebAuthenticationDetailsSource主要用于存储与Web相关的认证详细信息,列如远程主机地址和会话ID等 //buildDetails(request) 从request中建构信息 //setDetails 为认证令牌对象提供更多关于请求的详细信息 authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } filterChain.doFilter(request,response); } }