升级springboot3、权限采用sa-token

This commit is contained in:
JEECG
2025-09-26 12:18:30 +08:00
parent ea8b6098e4
commit 6aa8f9ae61
27 changed files with 752 additions and 401 deletions

View File

@@ -40,17 +40,17 @@ v2.1.3 | 2025-09-05
快速集成积木报表
-----------------------------------
> 快速集成到自己项目中支持SpringBoot脚手架项目springboot2依赖采用jdk8编译支持jdk8、jdk17、jdk21+。
> 快速集成到自己项目中支持SpringBoot3脚手架项目(要求jdk17+)springboot2版本要求jdk17+
#### 第一步:引入积木报表依赖
- springboot2
- springboot3
```
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<artifactId>jimureport-spring-boot3-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- mongo、redis和文件数据集支持包按需引入 -->
@@ -67,12 +67,12 @@ v2.1.3 | 2025-09-05
</dependency>
```
- springboot3
- springboot2
```
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- mongo、redis和文件数据集支持包按需引入 -->

View File

@@ -8,6 +8,14 @@
环境要求
-----------------------------------
- 要求jdk17+本项目springboot3架构
- 要求mysql5.7+ 手工执行db/jimureport.mysql5.7.create.sql会自动创建库jimureport
- 要求redis
- 项目配置src/main/resources/application-dev.yml
使用步骤
-----------------------------------

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<version>3.5.5</version>
<relativePath/>
</parent>
@@ -45,12 +45,12 @@
</repositories>
<properties>
<java.version>1.8</java.version>
<java.version>17</java.version>
<minio.version>8.0.3</minio.version>
<!-- DB驱动 -->
<mysql-connector-java.version>8.0.27</mysql-connector-java.version>
<!-- SQL解析引擎 -->
<jsqlparser.version>4.6</jsqlparser.version>
<jsqlparser.version>4.9</jsqlparser.version>
</properties>
@@ -64,7 +64,7 @@
<!-- 积木报表 -->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<artifactId>jimureport-spring-boot3-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- mongo、redis和文件数据集支持包按需引入 -->
@@ -82,17 +82,16 @@
<!-- 积木BI大屏 -->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId>
<version>2.1.3</version>
<artifactId>jimubi-spring-boot3-starter</artifactId>
<version>2.1.3.1</version>
</dependency>
<!-- jsqlparser -->
<!-- jsqlparser-->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
</dependency>
<!-- SpringBoot-->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -103,10 +102,22 @@
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--spring security-->
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.44.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- minio oss-->

View File

@@ -1,7 +1,6 @@
package com.jeecg.modules;
package com.jeecg;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
@@ -10,8 +9,7 @@ import org.springframework.core.env.Environment;
/**
* 积木报表独立服务启动类
*/
@SpringBootApplication(scanBasePackages = {"org.jeecg", "com.jeecg"})
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
@SpringBootApplication(scanBasePackages = {"org.jeecg", "com.jeecg"}, exclude = {MongoAutoConfiguration.class})
public class JimuReportApplication {
public static void main(String[] args) {
@@ -24,6 +22,7 @@ public class JimuReportApplication {
// 默认编码不是UTF-8设置为UTF-8
System.setProperty("file.encoding", "UTF-8");
}
System.out.println("\n----------------------------------------------------------\n\t" +
"JimuReport 积木报表平台 is running! Access URL:\n\t" +
"报表工作台: \t\thttp://localhost:" + port + path + "/jmreport/list\n\t" +

View File

@@ -1,35 +0,0 @@
package com.jeecg.modules.jmreport.config;
import com.alibaba.fastjson.JSONObject;
import org.jeecg.modules.jmreport.common.util.OkConvertUtils;
import org.springframework.security.core.context.SecurityContextImpl;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* @Description: api访问权限过滤器
*
* @author: wangshuai
* @date: 2024/9/25 下午6:22
*/
public class ApiSecurityConfigFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String loginFrom = req.getHeader("jm_login_from");
if(null != loginFrom && !loginFrom.isEmpty()){
String springSecurityContext = req.getHeader("jm_spring_security_context");
if(null != springSecurityContext && !springSecurityContext.isEmpty()){
SecurityContextImpl securityContext = JSONObject.parseObject(springSecurityContext, SecurityContextImpl.class);
HttpSession session = req.getSession();
session.setAttribute("loginFrom", loginFrom);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}
}
chain.doFilter(request, response);
}
}

View File

@@ -1,18 +0,0 @@
package com.jeecg.modules.jmreport.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CustomCorsConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有接口
.allowCredentials(true) // 是否发送 Cookie
.allowedOriginPatterns("*") // 支持域
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"}) // 支持方法
.allowedHeaders("*")
.exposedHeaders("*");
}
}

View File

@@ -1,53 +0,0 @@
package com.jeecg.modules.jmreport.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
/**
* 自定义springsecurity登录成功处理
* [TV360X-1884]jimureport-example集成简单的 spring security设置登录账号密码
* @author chenrui
* @date 2024/8/2 16:26
*/
@Slf4j
public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
HttpSession session = request.getSession();
session.setAttribute("loginFrom", "jimu_example");
SavedRequest savedRequest = (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
String redirectUrl = savedRequest != null ? savedRequest.getRedirectUrl() : null;
if (redirectUrl != null) {
try {
URI uri = new URI(redirectUrl);
String path = uri.getPath(); // 提取路径部分
if (path != null && path.startsWith("/jmreport")) {
// 路径符合要求,执行默认跳转
super.onAuthenticationSuccess(request, response, authentication);
return;
}
} catch (URISyntaxException e) {
// URL解析失败打印日志后继续执行默认跳转
log.error(e.getMessage(), e);
}
}
// 不符合 /jmreport 开头,或 redirectUrl 为 null 或异常
response.sendRedirect("/");
}
}

View File

@@ -1,61 +0,0 @@
package com.jeecg.modules.jmreport.config;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* 积木报表子系统切换处理类
* @author chenrui
* @date 2024/10/31 11:33
*/
@Component("jimuReportSwitchSysHandler")
public class JimuReportSysSwitchFilter implements Filter {
// 包含积木报表
boolean includeJimuReport = false;
// 包含积木仪表盘
boolean includeJimuDrag = false;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
// 判断是否包含积木报表
try {
Class.forName("org.jeecg.modules.jmreport.config.JmReportBaseConfig");
includeJimuReport = true;
} catch (ClassNotFoundException ignored) {
}
// 判断是否包含积木仪表盘
try {
Class.forName("org.jeecg.modules.drag.service.IOnlDragCompService");
includeJimuDrag = true;
} catch (ClassNotFoundException ignored) {
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpSession session = httpRequest.getSession();
if(includeJimuReport) {
session.setAttribute("switchJimuReport", "true");
}else{
session.setAttribute("switchJimuReport", "false");
}
if(includeJimuDrag) {
session.setAttribute("switchJimuDrag", "true");
}else {
session.setAttribute("switchJimuDrag", "false");
}
}
chain.doFilter(request, response);
}
}

View File

@@ -1,111 +0,0 @@
package com.jeecg.modules.jmreport.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.firewall.StrictHttpFirewall;
/**
* spring security 配置
* [TV360X-1884]jimureport-example集成简单的 spring security设置登录账号密码
* @Author chenrui
* @Date 2024-07-23
*/
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login/**").permitAll()
// 放过静态资源
.antMatchers("/jmreport/**/cdn/**",
"/jmreport/desreport_/**/*.js",
"/jmreport/desreport_/**/*.css",
"/jmreport/desreport_/**/*.ico",
"/jmreport/desreport_/**/*.png").permitAll()
// 放过静态资源-仪表盘
.antMatchers("/drag/lib/**/*.css",
"/drag/lib/**/*.js",
"/drag/lib/**/*.png",
"/drag/**/*.ico").permitAll()
// 不需要登录的接口
.antMatchers("/jmreport/excelQueryByTemplate",
"/jmreport/query/report/folder/template",
"/jmreport/img/**",
"/jmreport/download/image",
"/jmreport/verificationToken",
"/jmreport/link/queryByIds",
"/jmreport/test/getUserMsg",
"/jmreport/test/getOrder",
"/jimureport/test/**",
"/jmreport/auto/export/download/**",
"/jmreport/auto/export",
"/jmreport/qurestSql",
"/jmreport/qurestApi").permitAll()
// 分享页面
.antMatchers("/jmreport/shareView/**",
"/jmreport/exportPdfStream",
"/jmreport/exportAllExcelStream",
"/jmreport/checkParam/**",
"/jmreport/share/verification",
"/jmreport/getQueryInfo",
"/jmreport/show",
"/jmreport/form/submit",
"/jmreport/form/repeat/check/**",
"/jmreport/exportReport",
"/jmreport/dictCodeSearch",
"/jmreport/query/multiple/initValue",
"/jmreport/addViewCount/**").permitAll()
// 仪表盘分享页面
.antMatchers("/jimubi/share/view/**",
"/drag/share/view/**",
"/drag/page/queryById",
"/drag/page/addVisitsNumber",
"/drag/page/queryTemplateList",
"/drag/onlDragDatasetHead/getAllChartData",
"/drag/onlDragDatasetHead/getTotalData",
"/drag/onlDragDatasetHead/getDictByCodes",
"/drag/mock/json/**",
"/drag/getImageBase64/**").permitAll()
// view页面
.antMatchers("/jmreport/view/**").access("@viewPageCustomAccess.check(request,authentication)")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login/login.html")
.loginProcessingUrl("/login")
.successHandler(new CustomLoginSuccessHandler())
.permitAll().and()
.addFilterBefore(new ApiSecurityConfigFilter(), BasicAuthenticationFilter.class)
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true).permitAll();
// 开放iframe访问限制
http.headers().frameOptions().disable();
// 禁用默认的 no-cache
http.headers().cacheControl().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
http.rememberMe().useSecureCookie(true);
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
// 允许反斜杠
firewall.setAllowBackSlash(true);
// 允许双反斜杠
firewall.setAllowUrlEncodedDoubleSlash(true);
return (web) -> web.httpFirewall(firewall);
}
}

View File

@@ -1,43 +0,0 @@
package com.jeecg.modules.jmreport.config;
import org.jeecg.modules.jmreport.common.util.OkConvertUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义view页面access处理
* for: [TV360X-2206] 目前这个版本必须登录后才能看报表,如何设置不登录也能查看报表 #2919
* @author chenrui
* @date 2024/8/23 14:28
*/
@Component("viewPageCustomAccess")
public class ViewPageCustomAccess {
@Value("${spring.security.open-view-page:false}")
boolean openViewPage = false;
public boolean check(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
if (null == principal || principal.toString().isEmpty() || "anonymousUser".equalsIgnoreCase(principal.toString())) {
// 未登录
if (openViewPage) {
// 配置文件设置了开放view页面
return true;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
String previousPage = httpRequest.getParameter("previousPage");
String jmLink = httpRequest.getParameter("jmLink");
if (null != previousPage && !previousPage.isEmpty()
&& null != jmLink && !jmLink.isEmpty()) {
// 参数中有previousPage和jmLink
return true;
}
return false;
}
return true;
}
}

View File

@@ -1,21 +0,0 @@
package com.jeecg.modules.jmreport.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 积木报表-设置默认首页跳转
*/
@Controller
public class IndexController {
private Logger logger = LoggerFactory.getLogger(IndexController.class);
@GetMapping("/")
public String index(Model model) {
model.addAttribute("name", "jimureport");
return "jmreport/list"; // 视图重定向 - 跳转
}
}

View File

@@ -0,0 +1,89 @@
package com.jeecg.modules.jmreport.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.jeecg.modules.jmreport.satoken.config.SecurityConfig;
import com.jeecg.modules.jmreport.satoken.util.AjaxRequestUtils;
import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.net.URLEncoder;
/**
* 积木报表-设置默认首页跳转
*/
@Controller
public class LoginController {
private Logger logger = LoggerFactory.getLogger(LoginController.class);
public final static String LOGIN_PAGE = "/login/login.html";
@Autowired
SecurityConfig securityConfig;
/**
* 登录请求
*
* @param username
* @param password
* @param req
* @return
*/
@GetMapping("/doLogin")
public String login(@RequestParam String username, @RequestParam String password, jakarta.servlet.http.HttpServletRequest req) {
logger.info("登录请求,用户名:" + username + ",密码:" + password);
// 此处通过yml配置文件获取登录账号和密码实际项目中请从数据库读取进行验证
if (securityConfig.getUser().getName().equals(username) && securityConfig.getUser().getPassword().equals(password)) {
StpUtil.login(securityConfig.getUser().getName());
logger.info("登录成功当前会话tokeName={}, tokenValue={}", StpUtil.getTokenName(), StpUtil.getTokenValue());
// 设置登录来源,方便退出登录时区分
AjaxRequestUtils.setLoginSessionInfo();
return "jmreport/list";
}else{
logger.error("登录失败,用户名或密码错误");
// 返回密码错误提示需进行URL编码
return "redirect:" + LOGIN_PAGE + "?error=1";
}
}
/**
* 首页跳转
*
* @param model
* @return
*/
@GetMapping("/")
public String index(Model model) {
model.addAttribute("name", "jimureport");
return "jmreport/list"; // 视图重定向 - 跳转
}
/**
* 查询登录状态
*
* @return
*/
@RequestMapping("/isLogin")
public String isLogin() {
logger.info("查询登录状态:{}", StpUtil.getTokenInfo());
return "当前会话是否登录:" + StpUtil.isLogin();
}
/**
* 退出登录
*
* @return
*/
@RequestMapping("/logout")
public String logout() {
StpUtil.logout();
return "redirect:" + LOGIN_PAGE;
}
}

View File

@@ -30,9 +30,8 @@ import java.util.Map;
@Component
public class JimuDragExternalServiceImpl implements IOnlDragExternalService {
@Autowired
@Lazy
@Autowired
private IJimuReportDictService reportDictService;
/**

View File

@@ -1,22 +1,32 @@
package com.jeecg.modules.jmreport.extend;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import com.jeecg.modules.jmreport.satoken.config.SecurityConfig;
import com.jeecg.modules.jmreport.satoken.util.AjaxRequestUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
import org.jeecg.modules.jmreport.common.constant.JmConst;
import org.jeecg.modules.jmreport.common.expetion.JimuReportException;
import org.jeecg.modules.jmreport.common.util.JimuSpringContextUtils;
import org.jeecg.modules.jmreport.common.util.OkConvertUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义积木报表鉴权(如果不进行自定义,则所有请求不做权限控制)
* 1.自定义获取登录token
* 2.自定义获取登录用户
*/
@Slf4j
@Component
public class JimuReportTokenServiceImpl implements JmReportTokenServiceI {
@Autowired
SecurityConfig securityConfig;
/**
* 通过请求获取Token
* @param request
@@ -24,9 +34,15 @@ public class JimuReportTokenServiceImpl implements JmReportTokenServiceI {
*/
@Override
public String getToken(HttpServletRequest request) {
//System.out.println("---------call---------getToken-----------------------");
//return TokenUtils.getTokenByRequest(request);
return "123456";
String token = StpUtil.getTokenValue();
log.debug("------SA--TOKEN-----RequestPath={} GET Token = {}", SaHolder.getRequest().getRequestPath(), token);
if(StringUtils.isEmpty(token)){
token = request.getParameter("token");
// 将URL上的token设置到SaToken上下文方便后续操作
log.info("------SA--Init--TOKEN-----RequestPath={} 从URL参数获取Token = {}", SaHolder.getRequest().getRequestPath(), token);
StpUtil.setTokenValue(token);
}
return token;
}
/**
@@ -36,8 +52,9 @@ public class JimuReportTokenServiceImpl implements JmReportTokenServiceI {
*/
@Override
public String getUsername(String token) {
// return JwtUtil.getUsername(token);
return "admin";
String username = StpUtil.getLoginIdAsString();
log.debug("------SA--TOKEN-----RequestPath={} Token={} , LoginId={}", SaHolder.getRequest().getRequestPath(), token, username);
return username;
}
/**
@@ -75,25 +92,46 @@ public class JimuReportTokenServiceImpl implements JmReportTokenServiceI {
*/
@Override
public Boolean verifyToken(String token) {
//System.out.println("---------verify-----Token---------------");
//return TokenUtils.verifyToken(token, sysBaseAPI, redisUtil);
try {
if(securityConfig.getEnable()!=null && !securityConfig.getEnable()){
// 如果security.enable=false,则不进行登录校验
return true;
}
StpUtil.checkLogin();
log.debug("--SaToken verifyToken-成功RequestPath={}Token = {}", SaHolder.getRequest().getRequestPath(), token);
} catch (Exception e) {
log.warn("Token校验失败: token = {}error:{}", token, e.getMessage());
if(e instanceof NotLoginException){
// 跳转登录页面
try {
if(!AjaxRequestUtils.isAjaxRequest(JimuSpringContextUtils.getHttpServletRequest())){
JimuSpringContextUtils.getHttpServletResponse().sendRedirect("/login/login.html");
}
} catch (Exception ex) {
}
return false;
}else{
throw new JimuReportException(e);
}
}
return true;
}
/**
* 自定义请求头
* @return
*/
@Override
public HttpHeaders customApiHeader() {
HttpHeaders header = new HttpHeaders();
header.add("custom-header1", "Please set a custom value 1");
header.add("token", "token value 2");
return header;
}
// /**
// * 自定义请求头
// * @return
// */
// @Override
// public HttpHeaders customApiHeader() {
// HttpHeaders header = new HttpHeaders();
// header.add("custom-header1", "Please set a custom value 1");
// header.add("token", "token value 2");
// return header;
// }
/**
* 自定义获取租户
* 自定义租户
*
* @return
*/
@@ -110,7 +148,6 @@ public class JimuReportTokenServiceImpl implements JmReportTokenServiceI {
headerTenantId = request.getParameter(JmConst.TENANT_ID);
}
}
//return headerTenantId;
return null;
return headerTenantId;
}
}

View File

@@ -0,0 +1,85 @@
package com.jeecg.modules.jmreport.satoken;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import com.jeecg.modules.jmreport.controller.LoginController;
import com.jeecg.modules.jmreport.satoken.util.AjaxRequestUtils;
import jakarta.servlet.http.HttpSession;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.util.SaResult;
/**
* [Sa-Token 权限认证] 配置类
*
* @author click33
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册 Sa-Token 拦截器打开注解鉴权功能
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器打开注解鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
/**
* 注册 [Sa-Token 全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 [拦截路由] 与 [放行路由]
.addInclude("/**")
.addExclude("/favicon.ico")
.addExclude("/login/**")
.addExclude("/doLogin")
// 认证函数: 每次请求执行
.setAuth(obj -> {
// 设置登录来源,方便退出登录时区分
AjaxRequestUtils.setLoginSessionInfo();
// System.out.println("---------- sa全局认证path = " + SaHolder.getRequest().getRequestPath());
// System.out.println("---------- sa全局认证token = " + StpUtil.getTokenValue());
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
System.out.println("---------- sa全局异常path = " + SaHolder.getRequest().getRequestPath());
System.out.println("---------- sa全局认证token = " + StpUtil.getTokenValue());
e.printStackTrace();
return SaResult.error(e.getMessage());
})
// 前置函数在每次认证函数之前执行BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
.setBeforeAuth(r -> {
// ---------- 设置一些安全响应头 ----------
SaHolder.getResponse()
// 服务器名称
.setServer("sa-server")
// 是否可以在iframe显示视图 DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
.setHeader("X-Frame-Options", "SAMEORIGIN")
// 是否启用浏览器默认XSS防护 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时停止渲染页面
.setHeader("X-XSS-Protection", "1; mode=block")
// 禁用浏览器内容嗅探
.setHeader("X-Content-Type-Options", "nosniff")
;
});
}
}

View File

@@ -0,0 +1,44 @@
package com.jeecg.modules.jmreport.satoken;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.stp.StpInterface;
/**
* 自定义权限验证接口扩展
*/
@Component // 打开此注解保证此类被springboot扫描即可完成sa-token的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本list仅做模拟实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user-add");
list.add("user-delete");
list.add("user-update");
list.add("user-get");
list.add("article-get");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本list仅做模拟实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}

View File

@@ -1,4 +1,4 @@
package com.jeecg.modules.jmreport.config;
package com.jeecg.modules.jmreport.satoken.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
@@ -42,12 +42,10 @@ public class RedisConfig {
}
private Jackson2JsonRedisSerializer<Object> jacksonSerializer() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 使用新的替代方法避免已废弃的enableDefaultTyping
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
// 直接在构造器中传入 objectMapper
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
}

View File

@@ -0,0 +1,40 @@
package com.jeecg.modules.jmreport.satoken.config;
import com.jeecg.modules.jmreport.satoken.config.vo.User;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;
/**
* 加载项目配置
*
* @author: jeecg-boot
*/
@Component("securityConfig")
@ConfigurationProperties(prefix = "spring.security")
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SecurityConfig {
private Boolean enable = true;
/**
* 登录账号和密码
*/
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
}

View File

@@ -0,0 +1,20 @@
package com.jeecg.modules.jmreport.satoken.config.vo;
public class User {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,162 @@
//package com.jeecg.modules.jmreport.satoken.exception;
//
//import java.io.Serializable;
//import java.util.List;
//
//
///**
// * ajax请求返回Json格式数据的封装
// */
//public class AjaxJson implements Serializable{
//
// private static final long serialVersionUID = 1L; // 序列化版本号
//
// public static final int CODE_SUCCESS = 200; // 成功状态码
// public static final int CODE_ERROR = 500; // 错误状态码
// public static final int CODE_WARNING = 501; // 警告状态码
// public static final int CODE_NOT_JUR = 403; // 无权限状态码
// public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
// public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
//
// public int code; // 状态码
// public String msg; // 描述信息
// public Object data; // 携带对象
// public Long dataCount; // 数据总数,用于分页
//
// /**
// * 返回code
// * @return
// */
// public int getCode() {
// return this.code;
// }
//
// /**
// * 给msg赋值连缀风格
// */
// public AjaxJson setMsg(String msg) {
// this.msg = msg;
// return this;
// }
// public String getMsg() {
// return this.msg;
// }
//
// /**
// * 给data赋值连缀风格
// */
// public AjaxJson setData(Object data) {
// this.data = data;
// return this;
// }
//
// /**
// * 将data还原为指定类型并返回
// */
// @SuppressWarnings("unchecked")
// public <T> T getData(Class<T> cs) {
// return (T) data;
// }
//
// // ============================ 构建 ==================================
//
// public AjaxJson(int code, String msg, Object data, Long dataCount) {
// this.code = code;
// this.msg = msg;
// this.data = data;
// this.dataCount = dataCount;
// }
//
// // 返回成功
// public static AjaxJson getSuccess() {
// return new AjaxJson(CODE_SUCCESS, "ok", null, null);
// }
// public static AjaxJson getSuccess(String msg) {
// return new AjaxJson(CODE_SUCCESS, msg, null, null);
// }
// public static AjaxJson getSuccess(String msg, Object data) {
// return new AjaxJson(CODE_SUCCESS, msg, data, null);
// }
// public static AjaxJson getSuccessData(Object data) {
// return new AjaxJson(CODE_SUCCESS, "ok", data, null);
// }
// public static AjaxJson getSuccessArray(Object... data) {
// return new AjaxJson(CODE_SUCCESS, "ok", data, null);
// }
//
// // 返回失败
// public static AjaxJson getError() {
// return new AjaxJson(CODE_ERROR, "error", null, null);
// }
// public static AjaxJson getError(String msg) {
// return new AjaxJson(CODE_ERROR, msg, null, null);
// }
//
// // 返回警告
// public static AjaxJson getWarning() {
// return new AjaxJson(CODE_ERROR, "warning", null, null);
// }
// public static AjaxJson getWarning(String msg) {
// return new AjaxJson(CODE_WARNING, msg, null, null);
// }
//
// // 返回未登录
// public static AjaxJson getNotLogin() {
// return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
// }
//
// // 返回没有权限的
// public static AjaxJson getNotJur(String msg) {
// return new AjaxJson(CODE_NOT_JUR, msg, null, null);
// }
//
// // 返回一个自定义状态码的
// public static AjaxJson get(int code, String msg){
// return new AjaxJson(code, msg, null, null);
// }
//
// // 返回分页和数据的
// public static AjaxJson getPageData(Long dataCount, Object data){
// return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
// }
//
// // 返回,根据受影响行数的(大于0=ok小于0=error)
// public static AjaxJson getByLine(int line){
// if(line > 0){
// return getSuccess("ok", line);
// }
// return getError("error").setData(line);
// }
//
// // 返回,根据布尔值来确定最终结果的 (true=okfalse=error)
// public static AjaxJson getByBoolean(boolean b){
// return b ? getSuccess("ok") : getError("error");
// }
//
// /* (non-Javadoc)
// * @see java.lang.Object#toString()
// */
// @SuppressWarnings("rawtypes")
// @Override
// public String toString() {
// String data_string = null;
// if(data == null){
//
// } else if(data instanceof List){
// data_string = "List(length=" + ((List)data).size() + ")";
// } else {
// data_string = data.toString();
// }
// return "{"
// + "\"code\": " + this.getCode()
// + ", \"msg\": \"" + this.getMsg() + "\""
// + ", \"data\": " + data_string
// + ", \"dataCount\": " + dataCount
// + "}";
// }
//
//
//
//
//
//}

View File

@@ -0,0 +1,53 @@
//package com.jeecg.modules.jmreport.satoken.exception;
//
//import org.springframework.web.bind.annotation.ExceptionHandler;
//import org.springframework.web.bind.annotation.RestControllerAdvice;
//import cn.dev33.satoken.exception.DisableServiceException;
//import cn.dev33.satoken.exception.NotLoginException;
//import cn.dev33.satoken.exception.NotPermissionException;
//import cn.dev33.satoken.exception.NotRoleException;
//import jakarta.servlet.http.HttpServletRequest;
//import jakarta.servlet.http.HttpServletResponse;
//
///**
// * 全局异常处理
// */
//@RestControllerAdvice
//public class GlobalException {
//
// // 全局异常拦截(拦截项目中的所有异常)
// @ExceptionHandler
// public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)
// throws Exception {
//
// // 打印堆栈,以供调试
// System.out.println("全局异常---------------");
// //e.printStackTrace();
//
// // 不同异常返回不同状态码
// AjaxJson aj = null;
// if (e instanceof NotLoginException) { // 如果是未登录异常
// NotLoginException ee = (NotLoginException) e;
// aj = AjaxJson.getNotLogin().setMsg(ee.getMessage());
// }
// else if(e instanceof NotRoleException) { // 如果是角色异常
// NotRoleException ee = (NotRoleException) e;
// aj = AjaxJson.getNotJur("无此角色:" + ee.getRole());
// }
// else if(e instanceof NotPermissionException) { // 如果是权限异常
// NotPermissionException ee = (NotPermissionException) e;
// aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission());
// }
// else if(e instanceof DisableServiceException) { // 如果是被封禁异常
// DisableServiceException ee = (DisableServiceException) e;
// aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + ")" + ee.getDisableTime() + "秒后解封");
// }
// else { // 普通异常, 输出500 + 异常信息
// aj = AjaxJson.getError(e.getMessage());
// }
//
// // 返回给前端
// return aj;
// }
//
//}

View File

@@ -0,0 +1,38 @@
//package com.jeecg.modules.jmreport.satoken.exception;
//
//import cn.dev33.satoken.exception.NotLoginException;
//import cn.dev33.satoken.util.SaResult;
//import com.jeecg.modules.jmreport.satoken.util.AjaxRequestUtils;
//import jakarta.servlet.http.HttpServletRequest;
//import jakarta.servlet.http.HttpServletResponse;
//import org.springframework.web.bind.annotation.ExceptionHandler;
//import org.springframework.web.bind.annotation.RestControllerAdvice;
//
//import java.io.IOException;
//import java.net.URLEncoder;
//
//@RestControllerAdvice
//public class GlobalExceptionHandler {
//
// // 捕获未登录异常
// @ExceptionHandler(NotLoginException.class)
// public SaResult handlerNotLoginException(NotLoginException e,
// HttpServletRequest request,
// HttpServletResponse response) {
//
// // AJAX 请求返回 JSON
// if (AjaxRequestUtils.isAjaxRequest(request)) {
// return SaResult.error("未登录,请先登录").setCode(401);
// }
//
// // 普通请求重定向到登录页
// try {
// response.sendRedirect("/login/login.html?redirect=" + URLEncoder.encode(request.getRequestURI(), "UTF-8"));
// } catch (IOException ex) {
// ex.printStackTrace();
// }
//
// return SaResult.error("未登录");
// }
//
//}

View File

@@ -0,0 +1,79 @@
package com.jeecg.modules.jmreport.satoken.util;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.session.SaSession;
import com.alibaba.fastjson.JSON;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
public class AjaxRequestUtils {
/**
* 判断是否为 AJAX 请求
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
// 1. 检查 X-Requested-With 头部
String xRequestedWith = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
return true;
}
// 2. 检查 Accept 头部
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
return true;
}
// 3. 检查 Content-Type 头部(对于 POST 请求)
String contentType = request.getHeader("Content-Type");
if (contentType != null && contentType.contains("application/json")) {
return true;
}
// 4. 检查请求参数(某些框架会添加特定参数)
String ajaxParam = request.getParameter("_ajax");
if ("true".equals(ajaxParam)) {
return true;
}
return false;
}
/**
* 根据请求类型返回相应响应
*/
public static void writeResponse(HttpServletRequest request,
HttpServletResponse response,
Object data) throws IOException {
if (isAjaxRequest(request)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(data));
} else {
// 普通请求处理
request.setAttribute("data", data);
// 这里可以转发到相应的 JSP 页面
}
}
/**
* 设置 Sa-Token 会话的登录来源和拖拽开关
*/
// 登录来源-积木报表示例
public final static String LOGIN_FROM_MODEL_NEED_LOGOUT = "jimu_example";
public static void setLoginSessionInfo() {
HttpSession originalSession = ((HttpServletRequest) SaHolder.getRequest().getSource()).getSession();
if (originalSession!=null && originalSession.getAttribute("loginFrom") == null) {
log.info("设置登录来源BI与报表切换开关注入个性化session信息。");
originalSession.setAttribute("loginFrom", LOGIN_FROM_MODEL_NEED_LOGOUT);
originalSession.setAttribute("switchJimuDrag", true);
}
}
}

View File

@@ -7,9 +7,17 @@ spring:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
#Redis配置
data:
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
# 登录认证配置
security:
#是否放开预览页面不需要登录
open-view-page: true
#是否开启登录认证
enable: true
#登录账号和密码
user:
name: "admin"

View File

@@ -7,10 +7,16 @@ spring:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
#Redis配置
data:
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
# 登录认证配置
security:
#是否放开预览页面不需要登录
open-view-page: true
#登录账号和密码
#是否开启登录认证
user:
name: "admin"
password: "123456"

View File

@@ -1,3 +1,20 @@
spring:
profiles:
active: dev
active: dev
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: X-Access-Token
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: false
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: uuid
# 是否输出操作日志
is-log: false

View File

@@ -280,7 +280,7 @@
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<form class="form-signin" method="get" action="/doLogin">
<h2 class="form-signin-heading">欢迎使用积木报表</h2>
<p>
<label for="username" class="sr-only">Username</label>