更改目录结构

This commit is contained in:
liuyingfang
2024-03-02 18:31:44 +08:00
parent 8f7acca8e6
commit 0a70a66807
603 changed files with 2256 additions and 6114 deletions

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* WebMvcConfigurer
*
* @author Zheng Jie
* @date 2018-11-30
*/
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
/** 文件配置 */
private final FileProperties properties;
public ConfigurerAdapter(FileProperties properties) {
this.properties = properties;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
FileProperties.ElPath path = properties.getPath();
String avatarUtl = "file:" + path.getAvatar().replace("\\","/");
String pathUtl = "file:" + path.getPath().replace("\\","/");
registry.addResourceHandler("/avatar/**").addResourceLocations(avatarUtl).setCachePeriod(0);
registry.addResourceHandler("/file/**").addResourceLocations(pathUtl).setCachePeriod(0);
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 使用 fastjson 序列化,会导致 @JsonIgnore 失效,可以使用 @JSONField(serialize = false) 替换
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
List<MediaType> supportMediaTypeList = new ArrayList<>();
supportMediaTypeList.add(MediaType.APPLICATION_JSON);
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
config.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
converter.setFastJsonConfig(config);
converter.setSupportedMediaTypes(supportMediaTypeList);
converter.setDefaultCharset(StandardCharsets.UTF_8);
converters.add(converter);
}
}

View File

@@ -0,0 +1,31 @@
package cn.ysk.cashier.config;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.io.IOException;
@Configuration
public class JacksonConfig {
@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("");
}
});
return objectMapper;
}
}

View File

@@ -0,0 +1,16 @@
package cn.ysk.cashier.config;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.annotation.Configuration;
/**
* @author bearBoy80
*/
@Configuration(proxyBeanMethods = false)
public class RelaxedQueryCharsConnectorCustomizer implements TomcatConnectorCustomizer {
@Override
public void customize(Connector connector) {
connector.setProperty("relaxedQueryChars", "[]{}");
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author ZhangHouYing
* @date 2019-08-24 15:44
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@@ -0,0 +1,48 @@
package cn.ysk.cashier.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class fastJsonConfig extends WebMvcConfigurationSupport {
/**
* 使用阿里 fastjson 作为JSON MessageConverter
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
// 保留map空的字段
SerializerFeature.WriteMapNullValue,
// 将String类型的null转成""
SerializerFeature.WriteNullStringAsEmpty,
// 将Number类型的null转成0
SerializerFeature.WriteNullNumberAsZero,
// 将List类型的null转成[]
SerializerFeature.WriteNullListAsEmpty,
// 将Boolean类型的null转成false
SerializerFeature.WriteNullBooleanAsFalse,
// 避免循环引用
SerializerFeature.DisableCircularReferenceDetect);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(Charset.forName("UTF-8"));
List<MediaType> mediaTypeList = new ArrayList<>();
// 解决中文乱码问题相当于在Controller上的@RequestMapping中加了个属性produces = "application/json"
mediaTypeList.add(MediaType.APPLICATION_JSON);
converter.setSupportedMediaTypes(mediaTypeList);
converters.add(converter);
}
}

View File

@@ -0,0 +1,20 @@
package cn.ysk.cashier.config.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author lyf
*/
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
String userName = request.getHeader("userName");
request.setAttribute("userName", userName);
return true;
}
}

View File

@@ -0,0 +1,16 @@
package cn.ysk.cashier.config.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author lyf
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new UserInterceptor());
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.config;
import cn.ysk.cashier.config.security.config.bean.LoginProperties;
import cn.ysk.cashier.config.security.config.bean.SecurityProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @apiNote 配置文件转换Pojo类的 统一配置 类
* @author: liaojinlong
* @date: 2020/6/10 19:04
*/
@Configuration
public class ConfigBeanConfiguration {
@Bean
@ConfigurationProperties(prefix = "login")
public LoginProperties loginProperties() {
return new LoginProperties();
}
@Bean
@ConfigurationProperties(prefix = "jwt")
public SecurityProperties securityProperties() {
return new SecurityProperties();
}
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.config;
import cn.ysk.cashier.config.security.security.JwtAccessDeniedHandler;
import cn.ysk.cashier.config.security.security.JwtAuthenticationEntryPoint;
import cn.ysk.cashier.config.security.security.TokenConfigurer;
import cn.ysk.cashier.config.security.security.TokenProvider;
import lombok.RequiredArgsConstructor;
import cn.ysk.cashier.annotation.AnonymousAccess;
import cn.ysk.cashier.config.security.config.bean.SecurityProperties;
import cn.ysk.cashier.config.security.service.OnlineUserService;
import cn.ysk.cashier.config.security.service.UserCacheManager;
import cn.ysk.cashier.utils.enums.RequestMethodEnum;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.*;
/**
* @author Zheng Jie
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
private final JwtAuthenticationEntryPoint authenticationErrorHandler;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final ApplicationContext applicationContext;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
// 去除 ROLE_ 前缀
return new GrantedAuthorityDefaults("");
}
@Bean
public PasswordEncoder passwordEncoder() {
// 密码加密方式
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 搜寻匿名标记 url @AnonymousAccess
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
// 获取匿名标记
Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);
httpSecurity
// 禁用 CSRF
.csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
// 授权异常
.exceptionHandling()
.authenticationEntryPoint(authenticationErrorHandler)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()
// 不创建会话
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 静态资源等等
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll()
// swagger 文档
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 文件
.antMatchers("/avatar/**").permitAll()
.antMatchers("/file/**").permitAll()
// 阿里巴巴 druid
.antMatchers("/druid/**").permitAll()
// 放行OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 自定义匿名访问所有url放行允许匿名和带Token访问细腻化到每个 Request 类型
// GET
.antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
// POST
.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
// PUT
.antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
// PATCH
.antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
// DELETE
.antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
// 所有类型的接口都放行
.antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
.antMatchers("/auth/appletsLogin").permitAll()
// 所有请求都需要认证
.anyRequest().authenticated()
.and().apply(securityConfigurerAdapter());
}
private TokenConfigurer securityConfigurerAdapter() {
return new TokenConfigurer(tokenProvider, properties, onlineUserService, userCacheManager);
}
private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
Map<String, Set<String>> anonymousUrls = new HashMap<>(8);
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (null != anonymousAccess) {
List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case POST:
post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PUT:
put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PATCH:
patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case DELETE:
delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
default:
all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
}
}
}
anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
return anonymousUrls;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.config.bean;
import lombok.Data;
/**
* 登录验证码配置信息
*
* @author liaojinlong
* @date 2020/6/10 18:53
*/
@Data
public class LoginCode {
/**
* 验证码配置
*/
private LoginCodeEnum codeType;
/**
* 验证码有效期 分钟
*/
private Long expiration = 2L;
/**
* 验证码内容长度
*/
private int length = 2;
/**
* 验证码宽度
*/
private int width = 111;
/**
* 验证码高度
*/
private int height = 36;
/**
* 验证码字体
*/
private String fontName;
/**
* 字体大小
*/
private int fontSize = 25;
public LoginCodeEnum getCodeType() {
return codeType;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.config.bean;
/**
* 验证码配置枚举
*
* @author: liaojinlong
* @date: 2020/6/10 17:40
*/
public enum LoginCodeEnum {
/**
* 算数
*/
ARITHMETIC,
/**
* 中文
*/
CHINESE,
/**
* 中文闪图
*/
CHINESE_GIF,
/**
* 闪图
*/
GIF,
SPEC
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version loginCode.length.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-loginCode.length.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.config.bean;
import com.wf.captcha.*;
import com.wf.captcha.base.Captcha;
import lombok.Data;
import cn.ysk.cashier.exception.BadConfigurationException;
import cn.ysk.cashier.utils.StringUtils;
import java.awt.*;
import java.util.Objects;
/**
* 配置文件读取
*
* @author liaojinlong
* @date loginCode.length0loginCode.length0/6/10 17:loginCode.length6
*/
@Data
public class LoginProperties {
/**
* 账号单用户 登录
*/
private boolean singleLogin = false;
private LoginCode loginCode;
public static final String cacheKey = "USER-LOGIN-DATA";
public boolean isSingleLogin() {
return singleLogin;
}
/**
* 获取验证码生产类
*
* @return /
*/
public Captcha getCaptcha() {
if (Objects.isNull(loginCode)) {
loginCode = new LoginCode();
if (Objects.isNull(loginCode.getCodeType())) {
loginCode.setCodeType(LoginCodeEnum.ARITHMETIC);
}
}
return switchCaptcha(loginCode);
}
/**
* 依据配置信息生产验证码
*
* @param loginCode 验证码配置信息
* @return /
*/
private Captcha switchCaptcha(LoginCode loginCode) {
Captcha captcha;
switch (loginCode.getCodeType()) {
case ARITHMETIC:
// 算术类型 https://gitee.com/whvse/EasyCaptcha
captcha = new FixedArithmeticCaptcha(loginCode.getWidth(), loginCode.getHeight());
// 几位数运算,默认是两位
captcha.setLen(loginCode.getLength());
break;
case CHINESE:
captcha = new ChineseCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
case CHINESE_GIF:
captcha = new ChineseGifCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
case GIF:
captcha = new GifCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
case SPEC:
captcha = new SpecCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
default:
throw new BadConfigurationException("验证码配置信息错误!正确配置查看 LoginCodeEnum ");
}
if(StringUtils.isNotBlank(loginCode.getFontName())){
captcha.setFont(new Font(loginCode.getFontName(), Font.PLAIN, loginCode.getFontSize()));
}
return captcha;
}
static class FixedArithmeticCaptcha extends ArithmeticCaptcha {
public FixedArithmeticCaptcha(int width, int height) {
super(width, height);
}
@Override
protected char[] alphas() {
// 生成随机数字和运算符
int n1 = num(1, 10), n2 = num(1, 10);
int opt = num(3);
// 计算结果
int res = new int[]{n1 + n2, n1 - n2, n1 * n2}[opt];
// 转换为字符运算符
char optChar = "+-x".charAt(opt);
this.setArithmeticString(String.format("%s%c%s=?", n1, optChar, n2));
this.chars = String.valueOf(res);
return chars.toCharArray();
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.config.bean;
import lombok.Data;
/**
* Jwt参数配置
*
* @author Zheng Jie
* @date 2019年11月28日
*/
@Data
public class SecurityProperties {
/**
* Request Headers Authorization
*/
private String header;
/**
* 令牌前缀,最后留个空格 Bearer
*/
private String tokenStartWith;
/**
* 必须使用最少88位的Base64对该令牌进行编码
*/
private String base64Secret;
/**
* 令牌过期时间 此处单位/毫秒
*/
private Long tokenValidityInSeconds;
/**
* 在线用户 key根据 key 查询 redis 中在线用户的数据
*/
private String onlineKey;
/**
* 验证码 key
*/
private String codeKey;
/**
* token 续期检查
*/
private Long detect;
/**
* 续期时间
*/
private Long renew;
public String getTokenStartWith() {
return tokenStartWith + " ";
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.rest;
import cn.hutool.core.util.IdUtil;
import cn.ysk.cashier.config.security.config.bean.LoginCodeEnum;
import cn.ysk.cashier.config.security.security.TokenProvider;
import com.wf.captcha.base.Captcha;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import cn.ysk.cashier.annotation.Log;
import cn.ysk.cashier.annotation.rest.AnonymousDeleteMapping;
import cn.ysk.cashier.annotation.rest.AnonymousGetMapping;
import cn.ysk.cashier.annotation.rest.AnonymousPostMapping;
import cn.ysk.cashier.config.RsaProperties;
import cn.ysk.cashier.exception.BadRequestException;
import cn.ysk.cashier.config.security.config.bean.LoginProperties;
import cn.ysk.cashier.config.security.config.bean.SecurityProperties;
import cn.ysk.cashier.config.security.service.dto.AuthUserDto;
import cn.ysk.cashier.config.security.service.dto.JwtUserDto;
import cn.ysk.cashier.config.security.service.OnlineUserService;
import cn.ysk.cashier.pojo.shop.TbShopInfo;
import cn.ysk.cashier.repository.shop.TbShopInfoRepository;
import cn.ysk.cashier.utils.RsaUtils;
import cn.ysk.cashier.utils.RedisUtils;
import cn.ysk.cashier.utils.SecurityUtils;
import cn.ysk.cashier.utils.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Zheng Jie
* @date 2018-11-23
* 授权、根据token获取用户详细信息
*/
@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Api(tags = "系统:系统授权接口")
public class AuthorizationController {
private final SecurityProperties properties;
private final RedisUtils redisUtils;
private final OnlineUserService onlineUserService;
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final TbShopInfoRepository tbShopInfoRepository;
@Resource
private LoginProperties loginProperties;
@Log("用户登录")
@ApiOperation("登录授权")
@AnonymousPostMapping(value = "/login")
public ResponseEntity<Object> login(@Validated @RequestBody AuthUserDto authUser, HttpServletRequest request) throws Exception {
// 密码解密
String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUser.getPassword());
// 查询验证码
String code = (String) redisUtils.get(authUser.getUuid());
// 清除验证码
redisUtils.del(authUser.getUuid());
if (StringUtils.isBlank(code)) {
throw new BadRequestException("验证码不存在或已过期");
}
if (StringUtils.isBlank(authUser.getCode()) || !authUser.getCode().equalsIgnoreCase(code)) {
throw new BadRequestException("验证码错误");
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌与第三方系统获取令牌方式
// UserDetails userDetails = userDetailsService.loadUserByUsername(userInfo.getUsername());
// Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.createToken(authentication);
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 保存在线信息
onlineUserService.save(jwtUserDto, token, request);
// 返回 token 与 用户信息
TbShopInfo byAccount = tbShopInfoRepository.findByAccount(jwtUserDto.getUsername());
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", properties.getTokenStartWith() + token);
put("user", jwtUserDto);
if (byAccount!= null){
put("shopId",byAccount.getId());
}
}};
if (loginProperties.isSingleLogin()) {
//踢掉之前已经登录的token
onlineUserService.checkLoginOnUser(authUser.getUsername(), token);
}
return ResponseEntity.ok(authInfo);
}
/**
* 小程序登录
* @param authUser
* @param request
* @return
* @throws Exception
*/
@PostMapping(value = "/appletsLogin")
public ResponseEntity<Object> appletsLogin(@RequestBody AuthUserDto authUser,HttpServletRequest request)throws Exception{
// 密码解密
String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUser.getPassword());
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌与第三方系统获取令牌方式
String token = tokenProvider.createToken(authentication);
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 保存在线信息
onlineUserService.save(jwtUserDto, token,request);
// 返回 token 与 用户信息
TbShopInfo byAccount = tbShopInfoRepository.findByAccount(jwtUserDto.getUsername());
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", properties.getTokenStartWith() + token);
put("user", jwtUserDto);
if (byAccount!= null){
put("shopId",byAccount.getId());
}
}};
if (loginProperties.isSingleLogin()) {
//踢掉之前已经登录的token
onlineUserService.checkLoginOnUser(authUser.getUsername(), token);
}
return ResponseEntity.ok(authInfo);
}
@ApiOperation("获取用户信息")
@GetMapping(value = "/info")
public ResponseEntity<Object> getUserInfo() {
return ResponseEntity.ok(SecurityUtils.getCurrentUser());
}
@ApiOperation("获取验证码")
@AnonymousGetMapping(value = "/code")
public ResponseEntity<Object> getCode() {
// 获取运算的结果
Captcha captcha = loginProperties.getCaptcha();
String uuid = properties.getCodeKey() + IdUtil.simpleUUID();
//当验证码类型为 arithmetic时且长度 >= 2 时captcha.text()的结果有几率为浮点型
String captchaValue = captcha.text();
if (captcha.getCharType() - 1 == LoginCodeEnum.ARITHMETIC.ordinal() && captchaValue.contains(".")) {
captchaValue = captchaValue.split("\\.")[0];
}
// 保存
redisUtils.set(uuid, captchaValue, loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES);
// 验证码信息
Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("img", captcha.toBase64());
put("uuid", uuid);
}};
return ResponseEntity.ok(imgResult);
}
@ApiOperation("退出登录")
@AnonymousDeleteMapping(value = "/logout")
public ResponseEntity<Object> logout(HttpServletRequest request) {
onlineUserService.logout(tokenProvider.getToken(request));
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.rest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import cn.ysk.cashier.config.security.service.OnlineUserService;
import cn.ysk.cashier.utils.EncryptUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
/**
* @author Zheng Jie
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth/online")
@Api(tags = "系统:在线用户管理")
public class OnlineController {
private final OnlineUserService onlineUserService;
@ApiOperation("查询在线用户")
@GetMapping
@PreAuthorize("@el.check()")
public ResponseEntity<Object> queryOnlineUser(String filter, Pageable pageable){
return new ResponseEntity<>(onlineUserService.getAll(filter, pageable),HttpStatus.OK);
}
@ApiOperation("导出数据")
@GetMapping(value = "/download")
@PreAuthorize("@el.check()")
public void exportOnlineUser(HttpServletResponse response, String filter) throws IOException {
onlineUserService.download(onlineUserService.getAll(filter), response);
}
@ApiOperation("踢出用户")
@DeleteMapping
@PreAuthorize("@el.check()")
public ResponseEntity<Object> deleteOnlineUser(@RequestBody Set<String> keys) throws Exception {
for (String key : keys) {
// 解密Key
key = EncryptUtils.desDecrypt(key);
onlineUserService.kickOut(key);
}
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.security;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Zheng Jie
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//当用户在没有授权的情况下访问受保护的REST资源时将调用此方法发送403 Forbidden响应
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Zheng Jie
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 当用户尝试访问安全的REST资源而不提供任何凭据时将调用此方法发送401 响应
if(!"/api/qiNiuContent/updloadFile".equals(request.getRequestURI())){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage());
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.security;
import cn.ysk.cashier.config.security.config.bean.SecurityProperties;
import cn.ysk.cashier.config.security.service.OnlineUserService;
import cn.ysk.cashier.config.security.service.UserCacheManager;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author /
*/
@RequiredArgsConstructor
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
@Override
public void configure(HttpSecurity http) {
TokenFilter customFilter = new TokenFilter(tokenProvider, properties, onlineUserService, userCacheManager);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.security;
import cn.hutool.core.util.StrUtil;
import cn.ysk.cashier.config.security.config.bean.SecurityProperties;
import io.jsonwebtoken.ExpiredJwtException;
import cn.ysk.cashier.config.security.service.UserCacheManager;
import cn.ysk.cashier.config.security.service.dto.OnlineUserDto;
import cn.ysk.cashier.config.security.service.OnlineUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
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 java.io.IOException;
import java.util.Objects;
/**
* @author /
*/
public class TokenFilter extends GenericFilterBean {
private static final Logger log = LoggerFactory.getLogger(TokenFilter.class);
private final TokenProvider tokenProvider;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
/**
* @param tokenProvider Token
* @param properties JWT
* @param onlineUserService 用户在线
* @param userCacheManager 用户缓存工具
*/
public TokenFilter(TokenProvider tokenProvider, SecurityProperties properties, OnlineUserService onlineUserService, UserCacheManager userCacheManager) {
this.properties = properties;
this.onlineUserService = onlineUserService;
this.tokenProvider = tokenProvider;
this.userCacheManager = userCacheManager;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String token = resolveToken(httpServletRequest);
// 对于 Token 为空的不需要去查 Redis
if (StrUtil.isNotBlank(token)) {
OnlineUserDto onlineUserDto = null;
boolean cleanUserCache = false;
try {
onlineUserDto = onlineUserService.getOne(properties.getOnlineKey() + token);
} catch (ExpiredJwtException e) {
log.error(e.getMessage());
cleanUserCache = true;
} finally {
if (cleanUserCache || Objects.isNull(onlineUserDto)) {
userCacheManager.cleanUserCache(String.valueOf(tokenProvider.getClaims(token).get(TokenProvider.AUTHORITIES_KEY)));
}
}
if (onlineUserDto != null && StringUtils.hasText(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
// Token 续期
tokenProvider.checkRenewal(token);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
/**
* 初步检测Token
*
* @param request /
* @return /
*/
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(properties.getHeader());
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(properties.getTokenStartWith())) {
// 去掉令牌前缀
return bearerToken.replace(properties.getTokenStartWith(), "");
} else {
log.debug("非法Token{}", bearerToken);
}
return null;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.security;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import cn.ysk.cashier.config.security.config.bean.SecurityProperties;
import cn.ysk.cashier.utils.RedisUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author /
*/
@Slf4j
@Component
public class TokenProvider implements InitializingBean {
private final SecurityProperties properties;
private final RedisUtils redisUtils;
public static final String AUTHORITIES_KEY = "user";
private JwtParser jwtParser;
private JwtBuilder jwtBuilder;
public TokenProvider(SecurityProperties properties, RedisUtils redisUtils) {
this.properties = properties;
this.redisUtils = redisUtils;
}
@Override
public void afterPropertiesSet() {
byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret());
Key key = Keys.hmacShaKeyFor(keyBytes);
jwtParser = Jwts.parserBuilder()
.setSigningKey(key)
.build();
jwtBuilder = Jwts.builder()
.signWith(key, SignatureAlgorithm.HS512);
}
/**
* 创建Token 设置永不过期,
* Token 的时间有效性转到Redis 维护
*
* @param authentication /
* @return /
*/
public String createToken(Authentication authentication) {
return jwtBuilder
// 加入ID确保生成的 Token 都不一致
.setId(IdUtil.simpleUUID())
.claim(AUTHORITIES_KEY, authentication.getName())
.setSubject(authentication.getName())
.compact();
}
/**
* 依据Token 获取鉴权信息
*
* @param token /
* @return /
*/
Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
User principal = new User(claims.getSubject(), "******", new ArrayList<>());
return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>());
}
public Claims getClaims(String token) {
return jwtParser
.parseClaimsJws(token)
.getBody();
}
/**
* @param token 需要检查的token
*/
public void checkRenewal(String token) {
// 判断是否续期token,计算token的过期时间
long time = redisUtils.getExpire(properties.getOnlineKey() + token) * 1000;
Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time);
// 判断当前时间与过期时间的时间差
long differ = expireDate.getTime() - System.currentTimeMillis();
// 如果在续期检查的范围内,则续期
if (differ <= properties.getDetect()) {
long renew = time + properties.getRenew();
redisUtils.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS);
}
}
public String getToken(HttpServletRequest request) {
final String requestHeader = request.getHeader(properties.getHeader());
if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) {
return requestHeader.substring(7);
}
return null;
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.service;
import cn.ysk.cashier.config.security.config.bean.SecurityProperties;
import cn.ysk.cashier.config.security.service.dto.JwtUserDto;
import cn.ysk.cashier.utils.*;
import lombok.extern.slf4j.Slf4j;
import cn.ysk.cashier.config.security.service.dto.OnlineUserDto;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
/**
* @author Zheng Jie
* @date 2019年10月26日21:56:27
*/
@Service
@Slf4j
public class OnlineUserService {
private final SecurityProperties properties;
private final RedisUtils redisUtils;
public OnlineUserService(SecurityProperties properties, RedisUtils redisUtils) {
this.properties = properties;
this.redisUtils = redisUtils;
}
/**
* 保存在线用户信息
* @param jwtUserDto /
* @param token /
* @param request /
*/
public void save(JwtUserDto jwtUserDto, String token, HttpServletRequest request){
String dept = jwtUserDto.getUser().getDept().getName();
String ip = StringUtils.getIp(request);
String browser = StringUtils.getBrowser(request);
String address = StringUtils.getCityInfo(ip);
OnlineUserDto onlineUserDto = null;
try {
onlineUserDto = new OnlineUserDto(jwtUserDto.getUsername(), jwtUserDto.getUser().getNickName(), dept, browser , ip, address, EncryptUtils.desEncrypt(token), new Date());
} catch (Exception e) {
log.error(e.getMessage(),e);
}
redisUtils.set(properties.getOnlineKey() + token, onlineUserDto, properties.getTokenValidityInSeconds()/1000);
}
/**
* 查询全部数据
* @param filter /
* @param pageable /
* @return /
*/
public Map<String,Object> getAll(String filter, Pageable pageable){
List<OnlineUserDto> onlineUserDtos = getAll(filter);
return PageUtil.toPage(
PageUtil.toPage(pageable.getPageNumber(),pageable.getPageSize(), onlineUserDtos),
onlineUserDtos.size()
);
}
/**
* 查询全部数据,不分页
* @param filter /
* @return /
*/
public List<OnlineUserDto> getAll(String filter){
List<String> keys = redisUtils.scan(properties.getOnlineKey() + "*");
Collections.reverse(keys);
List<OnlineUserDto> onlineUserDtos = new ArrayList<>();
for (String key : keys) {
OnlineUserDto onlineUserDto = (OnlineUserDto) redisUtils.get(key);
if(StringUtils.isNotBlank(filter)){
if(onlineUserDto.toString().contains(filter)){
onlineUserDtos.add(onlineUserDto);
}
} else {
onlineUserDtos.add(onlineUserDto);
}
}
onlineUserDtos.sort((o1, o2) -> o2.getLoginTime().compareTo(o1.getLoginTime()));
return onlineUserDtos;
}
/**
* 踢出用户
* @param key /
*/
public void kickOut(String key){
key = properties.getOnlineKey() + key;
redisUtils.del(key);
}
/**
* 退出登录
* @param token /
*/
public void logout(String token) {
String key = properties.getOnlineKey() + token;
redisUtils.del(key);
}
/**
* 导出
* @param all /
* @param response /
* @throws IOException /
*/
public void download(List<OnlineUserDto> all, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (OnlineUserDto user : all) {
Map<String,Object> map = new LinkedHashMap<>();
map.put("用户名", user.getUserName());
map.put("部门", user.getDept());
map.put("登录IP", user.getIp());
map.put("登录地点", user.getAddress());
map.put("浏览器", user.getBrowser());
map.put("登录日期", user.getLoginTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
/**
* 查询用户
* @param key /
* @return /
*/
public OnlineUserDto getOne(String key) {
return (OnlineUserDto)redisUtils.get(key);
}
/**
* 检测用户是否在之前已经登录,已经登录踢下线
* @param userName 用户名
*/
public void checkLoginOnUser(String userName, String igoreToken){
List<OnlineUserDto> onlineUserDtos = getAll(userName);
if(onlineUserDtos ==null || onlineUserDtos.isEmpty()){
return;
}
for(OnlineUserDto onlineUserDto : onlineUserDtos){
if(onlineUserDto.getUserName().equals(userName)){
try {
String token =EncryptUtils.desDecrypt(onlineUserDto.getKey());
if(StringUtils.isNotBlank(igoreToken)&&!igoreToken.equals(token)){
this.kickOut(token);
}else if(StringUtils.isBlank(igoreToken)){
this.kickOut(token);
}
} catch (Exception e) {
log.error("checkUser is error",e);
}
}
}
}
/**
* 根据用户名强退用户
* @param username /
*/
@Async
public void kickOutForUsername(String username) throws Exception {
List<OnlineUserDto> onlineUsers = getAll(username);
for (OnlineUserDto onlineUser : onlineUsers) {
if (onlineUser.getUserName().equals(username)) {
String token =EncryptUtils.desDecrypt(onlineUser.getKey());
kickOut(token);
}
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.service;
import cn.hutool.core.util.RandomUtil;
import cn.ysk.cashier.config.security.config.bean.LoginProperties;
import cn.ysk.cashier.config.security.service.dto.JwtUserDto;
import cn.ysk.cashier.utils.RedisUtils;
import cn.ysk.cashier.utils.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author Zheng Jie
* @description 用户缓存管理
* @date 2022-05-26
**/
@Component
public class UserCacheManager {
@Resource
private RedisUtils redisUtils;
@Value("${login.user-cache.idle-time}")
private long idleTime;
/**
* 返回用户缓存
* @param userName 用户名
* @return JwtUserDto
*/
public JwtUserDto getUserCache(String userName) {
if (StringUtils.isNotEmpty(userName)) {
// 获取数据
Object obj = redisUtils.hget(LoginProperties.cacheKey, userName);
if(obj != null){
return (JwtUserDto)obj;
}
}
return null;
}
/**
* 添加缓存到Redis
* @param userName 用户名
*/
@Async
public void addUserCache(String userName, JwtUserDto user) {
if (StringUtils.isNotEmpty(userName)) {
// 添加数据, 避免数据同时过期
long time = idleTime + RandomUtil.randomInt(900, 1800);
redisUtils.hset(LoginProperties.cacheKey, userName, user, time);
}
}
/**
* 清理用户缓存信息
* 用户信息变更时
* @param userName 用户名
*/
@Async
public void cleanUserCache(String userName) {
if (StringUtils.isNotEmpty(userName)) {
// 清除数据
redisUtils.hdel(LoginProperties.cacheKey, userName);
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.service;
import cn.ysk.cashier.config.security.service.dto.JwtUserDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import cn.ysk.cashier.exception.BadRequestException;
import cn.ysk.cashier.exception.EntityNotFoundException;
import cn.ysk.cashier.system.service.DataService;
import cn.ysk.cashier.system.service.RoleService;
import cn.ysk.cashier.system.service.UserService;
import cn.ysk.cashier.system.service.dto.UserLoginDto;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* @author Zheng Jie
* @date 2018-11-22
*/
@Slf4j
@RequiredArgsConstructor
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
private final RoleService roleService;
private final DataService dataService;
private final UserCacheManager userCacheManager;
@Override
public JwtUserDto loadUserByUsername(String username) {
JwtUserDto jwtUserDto = userCacheManager.getUserCache(username);
if(jwtUserDto == null){
UserLoginDto user;
try {
user = userService.getLoginData(username);
} catch (EntityNotFoundException e) {
// SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException
throw new UsernameNotFoundException(username, e);
}
if (user == null) {
throw new UsernameNotFoundException("");
} else {
if (!user.getEnabled()) {
throw new BadRequestException("账号未激活!");
}
jwtUserDto = new JwtUserDto(
user,
dataService.getDeptIds(user),
roleService.mapToGrantedAuthorities(user)
);
// 添加缓存数据
userCacheManager.addUserCache(username, jwtUserDto);
}
}
return jwtUserDto;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.service.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author Zheng Jie
* @date 2018-11-30
*/
@Getter
@Setter
public class AuthUserDto {
@NotBlank
private String username;
@NotBlank
private String password;
private String code;
private String uuid = "";
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.service.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
/**
* 避免序列化问题
* @author Zheng Jie
* @date 2018-11-30
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthorityDto implements GrantedAuthority {
private String authority;
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.service.dto;
import cn.ysk.cashier.system.service.dto.UserLoginDto;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Zheng Jie
* @date 2018-11-23
*/
@Getter
@AllArgsConstructor
public class JwtUserDto implements UserDetails {
private final UserLoginDto user;
private final List<Long> dataScopes;
private final List<AuthorityDto> authorities;
public Set<String> getRoles() {
return authorities.stream().map(AuthorityDto::getAuthority).collect(Collectors.toSet());
}
@Override
@JSONField(serialize = false)
public String getPassword() {
return user.getPassword();
}
@Override
@JSONField(serialize = false)
public String getUsername() {
return user.getUsername();
}
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
@JSONField(serialize = false)
public boolean isEnabled() {
return user.getEnabled();
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.security.service.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 在线用户
* @author Zheng Jie
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OnlineUserDto {
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 岗位
*/
private String dept;
/**
* 浏览器
*/
private String browser;
/**
* IP
*/
private String ip;
/**
* 地址
*/
private String address;
/**
* token
*/
private String key;
/**
* 登录时间
*/
private Date loginTime;
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.thread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步任务线程池装配类
* @author https://juejin.im/entry/5abb8f6951882555677e9da2
* @date 2019年10月31日15:06:18
*/
@Slf4j
@Configuration
public class AsyncTaskExecutePool implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(AsyncTaskProperties.corePoolSize);
//最大线程数
executor.setMaxPoolSize(AsyncTaskProperties.maxPoolSize);
//队列容量
executor.setQueueCapacity(AsyncTaskProperties.queueCapacity);
//活跃时间
executor.setKeepAliveSeconds(AsyncTaskProperties.keepAliveSeconds);
//线程工厂
executor.setThreadFactory(new TheadFactoryName("el-async"));
// setRejectedExecutionHandler当pool已经达到max size的时候如何处理新任务
// CallerRunsPolicy不在新线程中执行任务而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
log.error("===="+throwable.getMessage()+"====", throwable);
log.error("exception method:"+method.getName());
};
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.thread;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 线程池配置属性类
* @author https://juejin.im/entry/5abb8f6951882555677e9da2
* @date 2019年10月31日14:58:18
*/
@Data
@Component
public class AsyncTaskProperties {
public static int corePoolSize;
public static int maxPoolSize;
public static int keepAliveSeconds;
public static int queueCapacity;
@Value("${task.pool.core-pool-size}")
public void setCorePoolSize(int corePoolSize) {
AsyncTaskProperties.corePoolSize = corePoolSize;
}
@Value("${task.pool.max-pool-size}")
public void setMaxPoolSize(int maxPoolSize) {
AsyncTaskProperties.maxPoolSize = maxPoolSize;
}
@Value("${task.pool.keep-alive-seconds}")
public void setKeepAliveSeconds(int keepAliveSeconds) {
AsyncTaskProperties.keepAliveSeconds = keepAliveSeconds;
}
@Value("${task.pool.queue-capacity}")
public void setQueueCapacity(int queueCapacity) {
AsyncTaskProperties.queueCapacity = queueCapacity;
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.thread;
import cn.ysk.cashier.utils.StringUtils;
import org.springframework.stereotype.Component;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义线程名称
* @author Zheng Jie
* @date 2019年10月31日17:49:55
*/
@Component
public class TheadFactoryName implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final static String DEF_NAME = "el-pool-";
public TheadFactoryName() {
this(DEF_NAME);
}
public TheadFactoryName(String name){
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
//此时namePrefix就是 name + 第几个用这个工厂创建线程池的
this.namePrefix = (StringUtils.isNotBlank(name) ? name : DEF_NAME) + "-" + POOL_NUMBER.getAndIncrement();
}
@Override
public Thread newThread(Runnable r) {
//此时线程的名字 就是 namePrefix + -exec- + 这个线程池中第几个执行的线程
Thread t = new Thread(group, r,
namePrefix + "-exec-"+threadNumber.getAndIncrement(),
0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.ysk.cashier.config.thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 用于获取自定义线程池
* @author Zheng Jie
* @date 2019年10月31日18:16:47
*/
public class ThreadPoolExecutorUtil {
public static ExecutorService getPoll(){
return getPoll(null);
}
public static ExecutorService getPoll(String threadName){
return new ThreadPoolExecutor(
AsyncTaskProperties.corePoolSize,
AsyncTaskProperties.maxPoolSize,
AsyncTaskProperties.keepAliveSeconds,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(AsyncTaskProperties.queueCapacity),
new TheadFactoryName(threadName),
// 队列与线程池中线程都满了时使用调用者所在的线程来执行
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}