feat(plugin): 新增 freemarker 集成插件。 fix: #651

This commit is contained in:
click33 2025-01-08 07:11:08 +08:00
parent 6c4cdf514e
commit 3192717c0f
18 changed files with 633 additions and 4 deletions

View File

@ -24,6 +24,7 @@ cd sa-token-demo-springboot-redisson & call mvn clean & cd ..
cd sa-token-demo-ssm & call mvn clean & cd ..
cd sa-token-demo-test & call mvn clean & cd ..
cd sa-token-demo-thymeleaf & call mvn clean & cd ..
cd sa-token-demo-freemarker & call mvn clean & cd ..
cd sa-token-demo-webflux & call mvn clean & cd ..
cd sa-token-demo-webflux-springboot3 & call mvn clean & cd ..
cd sa-token-demo-websocket & call mvn clean & cd ..

View File

@ -154,6 +154,11 @@
<artifactId>sa-token-dialect-thymeleaf</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-freemarker</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>

View File

@ -0,0 +1,71 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-demo-freemarker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.39.0</sa-token.version>
</properties>
<dependencies>
<!-- springboot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Freemarker 视图引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 在 Freemarker 页面中使用 Sa-Token 自定义标签 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-freemarker</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 热刷新 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>provided</scope>
</dependency>
<!-- @ConfigurationProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
package com.pj;
import cn.dev33.satoken.SaManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SaTokenFreemarkerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenFreemarkerDemoApplication.class, args);
System.out.println("\n启动成功Sa-Token 配置如下:" + SaManager.getConfig());
System.out.println("\n测试访问http://localhost:8081/");
}
}

View File

@ -0,0 +1,39 @@
package com.pj.satoken;
import cn.dev33.satoken.freemarker.dialect.SaTokenTemplateModel;
import cn.dev33.satoken.stp.StpUtil;
import freemarker.template.TemplateModelException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import javax.annotation.PostConstruct;
/**
* [Sa-Token 权限认证] 配置类
*
* @author click33
*/
@Configuration
public class SaTokenConfigure {
@Autowired
FreeMarkerConfigurer configurer;
/**
* 注入 Sa-Token Freemarker 标签模板模型 对象
*/
@PostConstruct
public void setSaTokenTemplateModel() throws TemplateModelException {
// 注入 Sa-Token Freemarker 标签模板模型使之可以在 xxx.ftl 文件中使用 sa 标签
// 例如<#if sa.login()>...</#if>
configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel());
// 注入 Sa-Token Freemarker 全局对象使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法
// 例如<span>${stp.getSession().get('name')}</span>
configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic);
}
}

View File

@ -0,0 +1,43 @@
package com.pj.satoken;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义权限验证接口扩展
*/
@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

@ -0,0 +1,23 @@
package com.pj.test;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 全局异常处理
*/
@RestControllerAdvice
public class GlobalException {
// 全局异常拦截拦截项目中的所有异常
@ExceptionHandler
public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}

View File

@ -0,0 +1,39 @@
package com.pj.test;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* 测试 Controller
*
* @author click33
*/
@RestController
public class TestController {
// 首页
@RequestMapping("/")
public Object index() {
return new ModelAndView("index");
}
// 登录
@RequestMapping("login")
public SaResult login(@RequestParam(defaultValue="10001") String id) {
StpUtil.login(id);
StpUtil.getSession().set("name", "zhangsan");
return SaResult.ok();
}
// 注销
@RequestMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}

View File

@ -0,0 +1,16 @@
# 端口
server:
port: 8081
spring:
# Freemarker 相关配置
freemarker:
# 指定模板文件的目录
template-loader-path: classpath:/templates
# 指定Freemarker模板文件的后缀名
suffix: .ftl
# 关闭模板缓存,方便测试
cache: false
# 检查模板更新延迟时间设置为0表示立即检查如果时间大于0会有缓存不方便进行模板测试
settings:
template_update_delay: 0

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Sa-Token 集成 Freemarker 标签方言</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div class="view-box" style="padding: 30px;">
<h2>Sa-Token 集成 Freemarker 标签方言 —— 测试页面</h2>
<p>当前是否登录:<#if stp.isLogin()>是<#else>否</#if></p>
<p>
<a href="login" target="_blank">登录</a>
<a href="logout" target="_blank">注销</a>
</p>
<p>登录之后才能显示:<@sa.login>value</@sa.login></p>
<p>不登录才能显示:<@sa.notLogin>value</@sa.notLogin></p>
<p>具有角色 admin 才能显示:<@sa.hasRole value="admin">value</@sa.hasRole></p>
<p>同时具备多个角色才能显示:<@sa.hasRoleAnd value="admin, ceo, cto">value</@sa.hasRoleAnd></p>
<p>只要具有其中一个角色就能显示:<@sa.hasRoleOr value="admin, ceo, cto">value</@sa.hasRoleOr></p>
<p>不具有角色 admin 才能显示:<@sa.notRole value="admin">value</@sa.notRole></p>
<p>具有权限 user-add 才能显示:<@sa.hasPermission value="user-add">value</@sa.hasPermission></p>
<p>同时具备多个权限才能显示:<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value</@sa.hasPermissionAnd></p>
<p>只要具有其中一个权限就能显示:<@sa.hasPermissionOr value="user-add, user-delete, user-get">value</@sa.hasPermissionOr></p>
<p>不具有权限 user-add 才能显示:<@sa.notPermission value="user-add">value</@sa.notPermission></p>
<p>
从SaSession中取值
<#if stp.isLogin()>
<span>${stp.getSession().get('name')}</span>
</#if>
</p>
</div>
</body>
</html>

View File

@ -9,9 +9,9 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
/**
* Sa-Token-SSO Server端 Controller
* @author click33
* 测试 Controller
*
* @author click33
*/
@RestController
public class TestController {

View File

@ -23,6 +23,7 @@
<servlet-api.version>3.1.0</servlet-api.version>
<jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<freemarker.version>2.3.34</freemarker.version>
<solon.version>3.0.4</solon.version>
<noear-redisx.version>1.6.2</noear-redisx.version>
<noear-snack3.version>3.2.88</noear-snack3.version>
@ -200,8 +201,15 @@
<artifactId>thymeleaf</artifactId>
<version>${thymeleaf.version}</version>
</dependency>
<!-- 视图引擎 -->
<!-- freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>${freemarker.version}</version>
</dependency>
<!-- spring-boot-starter-thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>

View File

@ -87,6 +87,7 @@
- [Alone独立Redis插件](/plugin/alone-redis)
- [持久层扩展](/plugin/dao-extend)
- [和 Thymeleaf 集成](/plugin/thymeleaf-extend)
- [和 Freemarker 集成](/plugin/freemarker-extend)
- [和 jwt 集成](/plugin/jwt-extend)
- [和 Dubbo 集成](/plugin/dubbo-extend)
- [和 gRPC 集成](/plugin/grpc-extend)

View File

@ -0,0 +1,130 @@
# Freemarker 自定义标签
本插件的作用是让我们可以在 Freemarker 页面中使用 Sa-Token 自定义标签以及相关API。
---
### 1、引入依赖
首先我们确保项目已经引入 Freemarker 依赖,然后在此基础上继续添加:
<!---------------------------- tabs:start ---------------------------->
<!-------- tab:Maven 方式 -------->
``` xml
<!-- 在 Freemarker 页面中使用 Sa-Token 自定义标签 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-freemarker</artifactId>
<version>${sa.top.version}</version>
</dependency>
```
<!-------- tab:Gradle 方式 -------->
``` gradle
// 在 Freemarker 页面中使用 Sa-Token 自定义标签
implementation 'cn.dev33:sa-token-freemarker:${sa.top.version}'
```
<!---------------------------- tabs:end ---------------------------->
### 2、注入 Sa-Token Freemarker 标签模板模型 对象
在 SaTokenConfigure 配置类中增加配置
``` java
@Configuration
public class SaTokenConfigure {
@Autowired
FreeMarkerConfigurer configurer;
/**
* 注入 Sa-Token Freemarker 标签模板模型 对象
*/
@PostConstruct
public void setSaTokenTemplateModel() throws TemplateModelException {
// 注入 Sa-Token Freemarker 标签模板模型,使之可以在 xxx.ftl 文件中使用 sa 标签,
// 例如:<#if sa.login()>...</#if>
configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel());
// 注入 Sa-Token Freemarker 全局对象,使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法,
// 例如:<span>${stp.getSession().get('name')}</span>
configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic);
}
}
```
### 3、使用自定义标签
然后我们就可以愉快的使用在 Freemarker 页面中使用 Sa-Token 自定义标签了
##### 3.1、登录判断
``` html
<h2>标签方言测试页面</h2>
<p>
登录之后才能显示:
<@sa.login>value</@sa.login>
</p>
<p>
不登录才能显示:
<@sa.notLogin>value</@sa.notLogin>
</p>
```
##### 3.2、角色判断
``` html
<p>
具有角色 admin 才能显示:
<@sa.hasRole value="admin">value</@sa.hasRole>
</p>
<p>
同时具备多个角色才能显示:
<@sa.hasRoleAnd value="admin, ceo, cto">value</@sa.hasRoleAnd>
</p>
<p>
只要具有其中一个角色就能显示:
<@sa.hasRoleOr value="admin, ceo, cto">value</@sa.hasRoleOr>
</p>
<p>
不具有角色 admin 才能显示:
<@sa.notRole value="admin">value</@sa.notRole>
</p>
```
##### 3.3、权限判断
``` html
<p>
具有权限 user-add 才能显示:
<@sa.hasPermission value="user-add">value</@sa.hasPermission>
</p>
<p>
同时具备多个权限才能显示:
<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value</@sa.hasPermissionAnd>
</p>
<p>
只要具有其中一个权限就能显示:
<@sa.hasPermissionOr value="user-add, user-delete, user-get">value</@sa.hasPermissionOr>
</p>
<p>
不具有权限 user-add 才能显示:
<@sa.notPermission value="user-add">value</@sa.notPermission>
</p>
```
### 4、调用 Sa-Token 相关API
以上的自定义标签,可以满足我们大多数场景下的权限判断,然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API :
``` html
<p>
从SaSession中取值
<#if stp.isLogin()>
<span>${stp.getSession().get('name')}</span>
</#if>
</p>
```

View File

@ -28,6 +28,7 @@
<module>sa-token-alone-redis</module>
<module>sa-token-hutool-timed-cache</module>
<module>sa-token-dialect-thymeleaf</module>
<module>sa-token-freemarker</module>
<module>sa-token-sso</module>
<module>sa-token-oauth2</module>
<module>sa-token-quick-login</module>

View File

@ -0,0 +1,33 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-plugin</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<packaging>jar</packaging>
<name>sa-token-freemarker</name>
<artifactId>sa-token-freemarker</artifactId>
<description>sa-token-freemarker</description>
<dependencies>
<!-- sa-token-core -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
<!-- freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,66 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* 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.dev33.satoken.freemarker.dialect;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import java.io.IOException;
import java.util.Map;
import java.util.function.Function;
/**
* Sa-Token Freemarker 标签模板指令模型
*
* @author click33
* @since 1.40.0
*/
public class SaTokenTemplateDirectiveModel implements TemplateDirectiveModel {
/**
* 使用标签指令模板时指定值的属性名
*/
String attrName;
/**
* 断言函数返回 true 时标签内容显示返回 false 时标签内容不显示
*/
Function <String, Boolean> fun;
public SaTokenTemplateDirectiveModel(String attrName, Function <String, Boolean> fun) {
this.attrName = attrName;
this.fun = fun;
}
@Override
public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody)
throws TemplateException, IOException {
// 获取 value
Object obj = map.get(attrName);
String value = obj == null ? null : obj.toString();
// 使用断言函数判断是否显示标签内容
if(this.fun.apply(value)) {
templateDirectiveBody.render(environment.getOut());
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* 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.dev33.satoken.freemarker.dialect;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
import freemarker.template.SimpleHash;
import java.util.List;
/**
* Sa-Token Freemarker 标签模板模型
*
* @author click33
* @since 1.40.0
*/
public class SaTokenTemplateModel extends SimpleHash {
/**
* 默认值属性名
*/
public static final String DEFAULT_ATTR_NAME = "value";
/**
* 底层使用的 StpLogic
*/
public StpLogic stpLogic;
/**
* 使用默认参数注册标签模板模型
*/
public SaTokenTemplateModel() {
this(DEFAULT_ATTR_NAME, StpUtil.stpLogic);
}
/**
* 构造标签模板模型使用自定义参数
*
* @param stpLogic 使用的 StpLogic 对象
*/
public SaTokenTemplateModel(StpLogic stpLogic) {
this(DEFAULT_ATTR_NAME, stpLogic);
}
/**
* 构造标签模板模型使用自定义参数
*
* @param attrName 属性名
* @param stpLogic 使用的 StpLogic 对象
*/
public SaTokenTemplateModel(String attrName, StpLogic stpLogic) {
this.stpLogic = stpLogic;
// 登录判断
put("login", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.isLogin()));
put("notLogin", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.isLogin()));
// 角色判断
put("hasRole", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRole(value)));
put("hasRoleAnd", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleAnd(toArray(value))));
put("hasRoleOr", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleOr(toArray(value))));
put("notRole", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value)));
put("lackRole", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value)));
// 权限判断
put("hasPermission", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermission(value)));
put("hasPermissionAnd", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionAnd(toArray(value))));
put("hasPermissionOr", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionOr(toArray(value))));
put("notPermission", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value)));
put("lackPermission", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value)));
}
/**
* String Array
* @param str 字符串
* @return 数组
*/
public String[] toArray(String str) {
List<String> list = SaFoxUtil.convertStringToList(str);
return list.toArray(new String[0]);
}
}