This commit is contained in:
parent
f32f5c9f3d
commit
ff0c25a1c9
|
|
@ -311,6 +311,7 @@ public class SysUserController extends BaseController {
|
|||
// 权限集合
|
||||
Set<String> permissions = sysMenuService.getMenuPermission(user);
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
user.setPassword(null);
|
||||
ajax.put("user", user);
|
||||
ajax.put("roles", roles);
|
||||
ajax.put("permissions", permissions);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
package com.bonus.system.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 跨域配置类
|
||||
* 解决前后端不分离项目的跨域问题
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${cors.allowed-origins}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Value("${cors.allowed-methods}")
|
||||
private String allowedMethods;
|
||||
|
||||
@Value("${cors.allowed-headers}")
|
||||
private String allowedHeaders;
|
||||
|
||||
@Value("${cors.allow-credentials}")
|
||||
private boolean allowCredentials;
|
||||
|
||||
@Value("${cors.max-age}")
|
||||
private long maxAge;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns(getAllowedOriginPatterns().toArray(new String[0]))
|
||||
.allowedMethods(getAllowedMethodArray())
|
||||
.allowedHeaders(getAllowedHeaderArray())
|
||||
.allowCredentials(allowCredentials)
|
||||
.maxAge(maxAge)
|
||||
.exposedHeaders("Content-Length", "Content-Type", "Token", "Authorization");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOriginPatterns(getAllowedOriginPatterns());
|
||||
configuration.setAllowedMethods(Arrays.asList(getAllowedMethodArray()));
|
||||
configuration.setAllowedHeaders(Arrays.asList(getAllowedHeaderArray()));
|
||||
configuration.setExposedHeaders(Arrays.asList("Content-Length", "Content-Type", "Token", "Authorization"));
|
||||
configuration.setAllowCredentials(allowCredentials);
|
||||
configuration.setMaxAge(maxAge);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
// 统一仅注册一套白名单策略,避免出现“*”
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
private List<String> getAllowedOriginPatterns() {
|
||||
if (allowedOrigins == null || allowedOrigins.trim().isEmpty()) {
|
||||
return Arrays.asList(
|
||||
"http://localhost:*",
|
||||
"http://127.0.0.1:*",
|
||||
"http://192.168.*.*:*",
|
||||
"http://10.1.*.*:*"
|
||||
);
|
||||
}
|
||||
return Arrays.asList(allowedOrigins.split(","));
|
||||
}
|
||||
|
||||
private String[] getAllowedMethodArray() {
|
||||
if (allowedMethods == null || allowedMethods.trim().isEmpty()) {
|
||||
return new String[]{"GET", "POST", "PUT", "DELETE", "OPTIONS"};
|
||||
}
|
||||
return allowedMethods.split(",");
|
||||
}
|
||||
|
||||
private String[] getAllowedHeaderArray() {
|
||||
if (allowedHeaders == null || allowedHeaders.trim().isEmpty()) {
|
||||
return new String[]{"Content-Type", "X-Requested-With", "Token", "Authorization", "X-Custom-Header"};
|
||||
}
|
||||
return allowedHeaders.split(",");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
package com.bonus.system.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Component
|
||||
@Order(1) // 确保过滤器优先级
|
||||
public class CspFilter implements Filter {
|
||||
|
||||
// 静态资源扩展名模式
|
||||
private static final Pattern STATIC_RESOURCE_PATTERN = Pattern.compile(
|
||||
".*\\.(css|js|map|png|jpg|jpeg|gif|ico|svg|webp|bmp|" +
|
||||
"woff|woff2|ttf|eot|otf|pdf|txt|xml|json|" +
|
||||
"zip|rar|7z|tar|gz|mp4|mp3|wav|avi|mov|webm|" +
|
||||
"doc|docx|xls|xlsx|ppt|pptx)$",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
// 静态资源路径前缀
|
||||
private static final List<String> STATIC_PATH_PREFIXES = Arrays.asList(
|
||||
"/static/", "/public/", "/resources/", "/assets/", "/css/", "/js/",
|
||||
"/images/", "/img/", "/fonts/", "/webjars/", "/vendor/", "/dist/",
|
||||
"/uploads/", "/downloads/", "/libs/", "/layui/"
|
||||
);
|
||||
|
||||
// WebGL和3D地图相关页面路径
|
||||
private static final List<String> WEBGL_PAGE_PATHS = Arrays.asList(
|
||||
"/pages/synthesisQuery/digitalSignage.html",
|
||||
"/pages/basic/lineManagement/child/setSpanTowerLonAndLat.html"
|
||||
);
|
||||
|
||||
@Value("${spring.profiles.active:prod}")
|
||||
private String activeProfile;
|
||||
|
||||
@Value("${csp.report-only:false}")
|
||||
private boolean cspReportOnly;
|
||||
|
||||
@Value("${csp.allow-iframe:true}")
|
||||
private boolean allowIframe;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
String requestUri = httpRequest.getRequestURI();
|
||||
|
||||
// 设置所有必要的安全头
|
||||
setSecurityHeaders(httpResponse, requestUri);
|
||||
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void setSecurityHeaders(HttpServletResponse response, String requestUri) {
|
||||
// 1. 设置ClickJacking防护头(优先解决)
|
||||
setClickJackingProtectionHeaders(response, requestUri);
|
||||
|
||||
// 2. 设置CSP头
|
||||
setCspHeader(response, requestUri);
|
||||
|
||||
// 3. 设置其他安全头
|
||||
setAdditionalSecurityHeaders(response);
|
||||
}
|
||||
|
||||
private void setCspHeader(HttpServletResponse response, String requestUri) {
|
||||
String cspPolicy;
|
||||
|
||||
if (isStaticResource(requestUri)) {
|
||||
// 静态资源使用简单策略
|
||||
cspPolicy = "default-src 'self'";
|
||||
}
|
||||
else if (isLoginPage(requestUri)) {
|
||||
// 登录页面 - 使用安全的CSP策略,移除不安全的指令
|
||||
String frameAncestors = allowIframe ? "'self'" : "'none'";
|
||||
|
||||
cspPolicy = "default-src 'self'; " +
|
||||
// 允许同源脚本和外部JavaScript库
|
||||
"script-src 'self' 'unsafe-inline' https:; " +
|
||||
// 只允许同源样式
|
||||
"style-src 'self' 'unsafe-inline' https:; " +
|
||||
// 只允许同源图片和数据URI
|
||||
"img-src 'self' data: blob: https:; " +
|
||||
// 只允许同源字体和数据URI
|
||||
"font-src 'self' data: https:; " +
|
||||
// 只允许同源连接
|
||||
"connect-src 'self' https:; " +
|
||||
"frame-ancestors " + frameAncestors + "; " +
|
||||
"form-action 'self'; " +
|
||||
"object-src 'none'; " +
|
||||
"base-uri 'self'; " +
|
||||
"report-uri /api/csp-violation";
|
||||
}
|
||||
else if (isWebglPage(requestUri)) {
|
||||
// WebGL和3D地图页面 - 需要更宽松的策略支持WebGL、Worker等
|
||||
String frameAncestors = allowIframe ? "'self'" : "'none'";
|
||||
|
||||
cspPolicy = "default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data:; " +
|
||||
"style-src 'self' 'unsafe-inline' data: blob:; " +
|
||||
"img-src 'self' data: blob: https:; " +
|
||||
"font-src 'self' data: blob: https:; " +
|
||||
"connect-src 'self' https: blob: data: http://data.mars3d.cn; " +
|
||||
"frame-ancestors " + frameAncestors + "; " +
|
||||
"form-action 'self'; " +
|
||||
"object-src 'none'; " +
|
||||
"base-uri 'self'; " +
|
||||
"worker-src 'self' blob: data:; " +
|
||||
"child-src 'self' blob: data:; " +
|
||||
"report-uri /api/csp-violation"; // 移除 upgrade-insecure-requests,避免强制HTTPS
|
||||
} else {
|
||||
// 普通HTML页面 - 根据配置决定是否允许iframe
|
||||
String frameAncestors = allowIframe ? "'self'" : "'none'";
|
||||
|
||||
cspPolicy = "default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; " +
|
||||
"style-src 'self' 'unsafe-inline' https:; " +
|
||||
"img-src 'self' data: blob: https:; " +
|
||||
"font-src 'self' data: https:; " +
|
||||
"connect-src 'self' https:; " +
|
||||
"frame-ancestors " + frameAncestors + "; " +
|
||||
"form-action 'self'; " +
|
||||
"object-src 'none'; " +
|
||||
"base-uri 'self'; " +
|
||||
"report-uri /api/csp-violation"; // 移除 upgrade-insecure-requests,避免强制HTTPS
|
||||
}
|
||||
|
||||
String headerName = cspReportOnly ?
|
||||
"Content-Security-Policy-Report-Only" : "Content-Security-Policy";
|
||||
|
||||
response.setHeader(headerName, cspPolicy);
|
||||
}
|
||||
|
||||
private void setClickJackingProtectionHeaders(HttpServletResponse response, String requestUri) {
|
||||
// 对于静态资源,使用宽松的ClickJacking防护
|
||||
if (isStaticResource(requestUri)) {
|
||||
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于HTML页面,根据配置决定防护级别
|
||||
if (allowIframe) {
|
||||
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
} else {
|
||||
response.setHeader("X-Frame-Options", "DENY");
|
||||
}
|
||||
}
|
||||
|
||||
private void setAdditionalSecurityHeaders(HttpServletResponse response) {
|
||||
response.setHeader("X-Content-Type-Options", "nosniff");
|
||||
response.setHeader("X-XSS-Protection", "1; mode=block");
|
||||
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
response.setHeader("Permissions-Policy",
|
||||
"geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=()");
|
||||
|
||||
// 注意:HSTS 只应在 HTTPS 部署下开启;当前未在此处强制设置
|
||||
// 如需开启,请在 HTTPS 部署完成后,通过配置控制
|
||||
// 例如:Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||||
}
|
||||
|
||||
private boolean isStaticResource(String uri) {
|
||||
if (uri == null || uri.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String path = uri.split("\\?")[0];
|
||||
|
||||
if (STATIC_RESOURCE_PATTERN.matcher(path).matches()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return STATIC_PATH_PREFIXES.stream().anyMatch(path::startsWith);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为登录页面
|
||||
*/
|
||||
private boolean isLoginPage(String requestUri) {
|
||||
return requestUri != null && (
|
||||
requestUri.endsWith("/login.html") ||
|
||||
requestUri.endsWith("/login") ||
|
||||
requestUri.contains("/login")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机nonce值
|
||||
*/
|
||||
private String generateNonce() {
|
||||
byte[] nonceBytes = new byte[16];
|
||||
new java.util.Random().nextBytes(nonceBytes);
|
||||
return java.util.Base64.getEncoder().encodeToString(nonceBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成内容的SHA-256哈希值
|
||||
*/
|
||||
private String generateHash(String content) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(content.getBytes("UTF-8"));
|
||||
return "'sha256-" + java.util.Base64.getEncoder().encodeToString(hash) + "'";
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWebglPage(String uri) {
|
||||
if (uri == null || uri.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String path = uri.split("\\?")[0];
|
||||
return WEBGL_PAGE_PATHS.stream().anyMatch(path::contains);
|
||||
}
|
||||
|
||||
private boolean isProduction() {
|
||||
return "prod".equals(activeProfile) || "production".equals(activeProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@
|
|||
from v_att_update_data vat
|
||||
left join sys_user su on vat.user_id = su.user_id
|
||||
left join sys_organization so on vat.org_id = so.id
|
||||
where 1=1
|
||||
<where>
|
||||
<if test="userId != null ">
|
||||
and vat.user_id = #{userId}
|
||||
</if>
|
||||
|
|
@ -225,6 +225,7 @@
|
|||
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
|
||||
and date_format(vat.att_current_day,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<select id="getDetailLeaveList" resultType="com.bonus.system.att.entity.AttDetailLeaveBean">
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@
|
|||
#{currentDay} as currentDay
|
||||
FROM att_group_person_relation
|
||||
WHERE #{currentDay} BETWEEN DATE_FORMAT(effective_time, '%Y-%m-%d')
|
||||
AND DATE_FORMAT(IFNULL(NOW(), expiring_time), '%Y-%m-%d')
|
||||
AND DATE_FORMAT(IFNULL(expiring_time, NOW()), '%Y-%m-%d')
|
||||
ORDER BY user_id,
|
||||
effective_time DESC LIMIT 10000000) aa
|
||||
GROUP BY user_id
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</if>
|
||||
GROUP BY o.id
|
||||
) a
|
||||
where 1=1
|
||||
<where>
|
||||
<if test="bean.orgName != null and bean.orgName != ''">
|
||||
AND a.org_name like concat('%', #{bean.orgName}, '%')
|
||||
</if>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</foreach>
|
||||
)
|
||||
</if>
|
||||
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<select id="selectOrgById" resultType="com.bonus.system.basic.domain.SysOrg">
|
||||
|
|
@ -161,14 +161,14 @@
|
|||
where o.is_active = 1
|
||||
GROUP BY o.id
|
||||
)a
|
||||
where 1=1
|
||||
<where>
|
||||
<if test="bean.orgName != null and bean.orgName != ''">
|
||||
AND a.org_name like concat('%', #{bean.orgName}, '%')
|
||||
</if>
|
||||
<if test="bean.orgHeadUserName != null and bean.orgHeadUserName != ''">
|
||||
AND a.orgHeadUserName like concat('%', #{bean.orgHeadUserName}, '%')
|
||||
</if>
|
||||
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<select id="getOrgHistoryList" resultType="com.bonus.system.basic.domain.SysOrgHistoryBean">
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@
|
|||
SELECT su.user_id as userId, su.user_name as userName
|
||||
FROM sys_user su
|
||||
LEFT JOIN sys_user_org suo ON su.user_id = suo.user_id And suo.is_active= '1' AND su.user_id != #{params.id}
|
||||
WHERE 1=1
|
||||
<where>
|
||||
<if test='params.orgList != null and params.orgList.size() > 0'>
|
||||
and suo.org_id in (
|
||||
<foreach collection="params.orgList" item="item" separator=",">
|
||||
|
|
@ -243,6 +243,7 @@
|
|||
</foreach>
|
||||
)
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
<!--判断出差报备数据是否被核对-->
|
||||
<select id="isCheck" resultType="java.lang.Integer">
|
||||
|
|
|
|||
|
|
@ -200,11 +200,11 @@
|
|||
and la.examine_status = #{examineStatus}
|
||||
</if>
|
||||
) a
|
||||
where 1=1
|
||||
<where>
|
||||
<if test="leaveTypes!=null and leaveTypes!=''">
|
||||
and a.leaveType = #{leaveTypes}
|
||||
</if>
|
||||
|
||||
</where>
|
||||
</select>
|
||||
<select id="getDetailsList" resultType="com.bonus.system.holiday.entity.WorkReportBean">
|
||||
SELECT
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class WebSecurityConfigurer
|
|||
.and().authorizeRequests()
|
||||
.antMatchers(adminContextPath + "/assets/**"
|
||||
, adminContextPath + "/login"
|
||||
, adminContextPath + "/actuator/**"
|
||||
// , adminContextPath + "/actuator/**"
|
||||
, adminContextPath + "/instances/**"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
|
|
|
|||
Loading…
Reference in New Issue