diff --git a/sa-token-starter/pom.xml b/sa-token-starter/pom.xml
index 12aa11ba..810fd079 100644
--- a/sa-token-starter/pom.xml
+++ b/sa-token-starter/pom.xml
@@ -21,6 +21,8 @@
sa-token-spring-boot-starter
sa-token-reactor-spring-boot-starter
sa-token-solon-plugin
+ sa-token-jboot-plugin
+ sa-token-jfinal-plugin
\ No newline at end of file
diff --git a/sa-token-starter/sa-token-jboot-plugin/pom.xml b/sa-token-starter/sa-token-jboot-plugin/pom.xml
new file mode 100644
index 00000000..bcf033bb
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/pom.xml
@@ -0,0 +1,55 @@
+
+
+
+ sa-token-starter
+ cn.dev33
+ 1.28.0
+
+ 4.0.0
+ jar
+
+ sa-token-jboot-plugin
+ jboot integrate sa-token
+
+
+ 8
+ 8
+
+
+
+
+ io.jboot
+ jboot
+ 3.11.4
+ provided
+
+
+ cn.dev33
+ sa-token-core
+ ${sa-token-version}
+
+
+ cn.dev33
+ sa-token-servlet
+ ${sa-token-version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.6.1
+
+ 1.8
+ 1.8
+ UTF-8
+ -parameters
+
+
+
+
+
\ No newline at end of file
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java
new file mode 100644
index 00000000..3667207c
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java
@@ -0,0 +1,60 @@
+package cn.dev33.satoken.jboot;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PathAnalyzer {
+
+ private static Map cached = new LinkedHashMap();
+ private Pattern pattern;
+
+ public static PathAnalyzer get(String expr) {
+ PathAnalyzer pa = (PathAnalyzer)cached.get(expr);
+ if (pa == null) {
+ synchronized(expr.intern()) {
+ pa = (PathAnalyzer)cached.get(expr);
+ if (pa == null) {
+ pa = new PathAnalyzer(expr);
+ cached.put(expr, pa);
+ }
+ }
+ }
+
+ return pa;
+ }
+
+ private PathAnalyzer(String expr) {
+ this.pattern = Pattern.compile(exprCompile(expr), 2);
+ }
+
+ public Matcher matcher(String uri) {
+ return this.pattern.matcher(uri);
+ }
+
+ public boolean matches(String uri) {
+ return this.pattern.matcher(uri).find();
+ }
+
+ private static String exprCompile(String expr) {
+ String p = expr.replace(".", "\\.");
+ p = p.replace("$", "\\$");
+ p = p.replace("**", ".[]");
+ p = p.replace("*", "[^/]*");
+ if (p.indexOf("{") >= 0) {
+ if (p.indexOf("_}") > 0) {
+ p = p.replaceAll("\\{[^\\}]+?\\_\\}", "(.+?)");
+ }
+
+ p = p.replaceAll("\\{[^\\}]+?\\}", "([^/]+?)");
+ }
+
+ if (!p.startsWith("/")) {
+ p = "/" + p;
+ }
+
+ p = p.replace(".[]", ".*");
+ return "^" + p + "$";
+ }
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java
new file mode 100644
index 00000000..28d94e0f
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java
@@ -0,0 +1,16 @@
+package cn.dev33.satoken.jboot;
+
+import cn.dev33.satoken.strategy.SaStrategy;
+import com.jfinal.aop.Interceptor;
+import com.jfinal.aop.Invocation;
+
+/**
+ * 注解式鉴权 - 拦截器
+ */
+public class SaAnnotationInterceptor implements Interceptor {
+ @Override
+ public void intercept(Invocation invocation) {
+ SaStrategy.me.checkMethodAnnotation.accept((invocation.getMethod()));
+ invocation.invoke();
+ }
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java
new file mode 100644
index 00000000..32879fff
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java
@@ -0,0 +1,52 @@
+package cn.dev33.satoken.jboot;
+
+import cn.dev33.satoken.context.SaTokenContext;
+import cn.dev33.satoken.context.model.SaRequest;
+import cn.dev33.satoken.context.model.SaResponse;
+import cn.dev33.satoken.context.model.SaStorage;
+import cn.dev33.satoken.servlet.model.SaRequestForServlet;
+import cn.dev33.satoken.servlet.model.SaResponseForServlet;
+import cn.dev33.satoken.servlet.model.SaStorageForServlet;
+import io.jboot.web.controller.JbootControllerContext;
+
+/**
+ * Sa-Token 上下文处理器 [Jboot 版本实现]
+ */
+public class SaTokenContextForJboot implements SaTokenContext {
+ /**
+ * 获取当前请求的Request对象
+ */
+ @Override
+ public SaRequest getRequest() {
+ return new SaRequestForServlet(JbootControllerContext.get().getRequest());
+ }
+
+ /**
+ * 获取当前请求的Response对象
+ */
+ @Override
+ public SaResponse getResponse() {
+ return new SaResponseForServlet(JbootControllerContext.get().getResponse());
+ }
+
+ /**
+ * 获取当前请求的 [存储器] 对象
+ */
+ @Override
+ public SaStorage getStorage() {
+ return new SaStorageForServlet(JbootControllerContext.get().getRequest());
+ }
+
+ /**
+ * 校验指定路由匹配符是否可以匹配成功指定路径
+ */
+ @Override
+ public boolean matchPath(String pattern, String path) {
+ return PathAnalyzer.get(pattern).matches(path);
+ }
+
+ @Override
+ public boolean isValid() {
+ return SaTokenContext.super.isValid();
+ }
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenDaoRedis.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenDaoRedis.java
new file mode 100644
index 00000000..a54097ce
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenDaoRedis.java
@@ -0,0 +1,219 @@
+package cn.dev33.satoken.jboot;
+
+import cn.dev33.satoken.dao.SaTokenDao;
+import cn.dev33.satoken.util.SaFoxUtil;
+import io.jboot.Jboot;
+import io.jboot.components.cache.redis.JbootRedisCacheConfig;
+import io.jboot.exception.JbootIllegalConfigException;
+import io.jboot.support.redis.JbootRedis;
+import io.jboot.support.redis.JbootRedisManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class SaTokenDaoRedis implements SaTokenDao {
+
+ private final JbootRedis redis;
+ /**
+ * 标记:是否已初始化成功
+ */
+ public boolean isInit;
+
+ public SaTokenDaoRedis(){
+ JbootRedisCacheConfig redisConfig = Jboot.config(JbootRedisCacheConfig.class);
+
+ //优先使用 jboot.cache.redis 的配置
+ if (redisConfig.isConfigOk()) {
+ redis = JbootRedisManager.me().getRedis(redisConfig);
+ }
+ // 当 jboot.cache.redis 配置不存在时,
+ // 使用 jboot.redis 的配置
+ else {
+ redis = Jboot.getRedis();
+ }
+
+ if (redis == null) {
+ this.isInit = false;
+ throw new JbootIllegalConfigException("can not get redis, please check your jboot.properties , please correct config jboot.cache.redis.host or jboot.redis.host ");
+ }else{
+ this.isInit = true;
+ }
+ }
+
+ /**
+ * 获取Value,如无返空
+ * @param key
+ * @return
+ */
+ @Override
+ public String get(String key) {
+ return redis.get(key);
+ }
+
+ /**
+ * 写入Value,并设定存活时间 (单位: 秒)
+ * @param key
+ * @param value
+ * @param timeout
+ */
+ @Override
+ public void set(String key, String value, long timeout) {
+ if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ redis.set(key, value);
+ }else{
+ redis.setex(key,Integer.parseInt(timeout+""),value);
+ }
+ }
+
+ /**
+ * 修改指定key-value键值对 (过期时间不变)
+ * @param key
+ * @param value
+ */
+ @Override
+ public void update(String key, String value) {
+ long expire = getTimeout(key);
+ // -2 = 无此键
+ if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ this.set(key,value,expire);
+ }
+
+ /**
+ * 删除Value
+ * @param key
+ */
+ @Override
+ public void delete(String key) {
+ redis.del(key);
+ }
+
+ /**
+ * 获取Value的剩余存活时间 (单位: 秒)
+ * @param key
+ * @return
+ */
+ @Override
+ public long getTimeout(String key) {
+ return redis.ttl(key);
+ }
+
+ /**
+ * 修改Value的剩余存活时间 (单位: 秒)
+ * @param key
+ * @param timeout
+ */
+ @Override
+ public void updateTimeout(String key, long timeout) {
+ //判断是否想要设置为永久
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ long expire = getTimeout(key);
+ if(expire == SaTokenDao.NEVER_EXPIRE) {
+ // 如果其已经被设置为永久,则不作任何处理
+ } else {
+ // 如果尚未被设置为永久,那么再次set一次
+ this.set(key, this.get(key), timeout);
+ }
+ return;
+ }
+ redis.expire(key,Integer.parseInt(timeout+""));
+ }
+
+ /**
+ * 获取Object,如无返空
+ * @param key
+ * @return
+ */
+ @Override
+ public Object getObject(String key) {
+ return redis.get(key);
+ }
+
+ /**
+ * 写入Object,并设定存活时间 (单位: 秒)
+ * @param key
+ * @param object
+ * @param timeout
+ */
+ @Override
+ public void setObject(String key, Object object, long timeout) {
+ if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ redis.set(key, object);
+ }else{
+ redis.setex(key,Integer.parseInt(timeout+""),object);
+ }
+ }
+
+ /**
+ * 更新Object (过期时间不变)
+ * @param key
+ * @param object
+ */
+ @Override
+ public void updateObject(String key, Object object) {
+ long expire = getObjectTimeout(key);
+ // -2 = 无此键
+ if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ this.setObject(key, object, expire);
+ }
+
+ /**
+ * 删除Object
+ * @param key
+ */
+ @Override
+ public void deleteObject(String key) {
+ redis.del(key);
+ }
+
+ @Override
+ public long getObjectTimeout(String key) {
+ return redis.ttl(key);
+ }
+
+ /**
+ * 修改Object的剩余存活时间 (单位: 秒)
+ * @param key
+ * @param timeout
+ */
+ @Override
+ public void updateObjectTimeout(String key, long timeout) {
+ //判断是否想要设置为永久
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ long expire = getTimeout(key);
+ if(expire == SaTokenDao.NEVER_EXPIRE) {
+ // 如果其已经被设置为永久,则不作任何处理
+ } else {
+ // 如果尚未被设置为永久,那么再次set一次
+ this.set(key, this.get(key), timeout);
+ }
+ return;
+ }
+ redis.expire(key,Integer.parseInt(timeout+""));
+ }
+
+ /**
+ * 搜索数据
+ * @param prefix
+ * @param keyword
+ * @param start
+ * @param size
+ * @return
+ */
+ @Override
+ public List searchData(String prefix, String keyword, int start, int size) {
+ Set keys = redis.keys(prefix + "*" + keyword + "*");
+ List list = new ArrayList(keys);
+ return SaFoxUtil.searchList(list, start, size);
+ }
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java
new file mode 100644
index 00000000..5b7f9372
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java
@@ -0,0 +1,35 @@
+package cn.dev33.satoken.jboot.test;
+
+import cn.dev33.satoken.annotation.SaCheckRole;
+import cn.dev33.satoken.stp.StpUtil;
+import io.jboot.app.JbootApplication;
+import io.jboot.web.controller.JbootController;
+import io.jboot.web.controller.annotation.RequestMapping;
+
+@RequestMapping("/")
+public class AppRun extends JbootController {
+ public static void main(String[] args) {
+ JbootApplication.run(args);
+ }
+
+ public void index(){
+ renderText("index");
+ }
+
+ public void doLogin(){
+ StpUtil.login(10001);
+ //赋值角色
+ renderText("登录成功");
+ }
+
+ public void getLoginInfo(){
+ System.out.println("是否登录:"+StpUtil.isLogin());
+ System.out.println("登录信息"+StpUtil.getTokenInfo());
+ renderJson(StpUtil.getTokenInfo());
+ }
+
+ @SaCheckRole("super-admin")
+ public void add(){
+ renderText("超级管理员方法!");
+ }
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java
new file mode 100644
index 00000000..88465900
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java
@@ -0,0 +1,77 @@
+package cn.dev33.satoken.jboot.test;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.context.SaTokenContext;
+import com.jfinal.config.Constants;
+import com.jfinal.config.Interceptors;
+import com.jfinal.config.Routes;
+import com.jfinal.template.Engine;
+import cn.dev33.satoken.jboot.SaAnnotationInterceptor;
+import cn.dev33.satoken.jboot.SaTokenContextForJboot;
+import cn.dev33.satoken.jboot.SaTokenDaoRedis;
+import io.jboot.aop.jfinal.JfinalHandlers;
+import io.jboot.aop.jfinal.JfinalPlugins;
+import io.jboot.core.listener.JbootAppListener;
+
+public class AtteStartListener implements JbootAppListener {
+ public void onInit() {
+ //注册权限验证功能,由saToken处理请求上下文
+ SaTokenContext saTokenContext = new SaTokenContextForJboot();
+ SaManager.setSaTokenContext(saTokenContext);
+ //加载权限角色设置数据接口
+ SaManager.setStpInterface(new StpInterfaceImpl());
+ //增加redis缓存,需要先配置redis地址
+// SaManager.setSaTokenDao(new SaTokenDaoRedis());
+ }
+
+ @Override
+ public void onConstantConfig(Constants constants) {
+
+ }
+
+ @Override
+ public void onRouteConfig(Routes routes) {
+
+ }
+
+ @Override
+ public void onEngineConfig(Engine engine) {
+
+ }
+
+ @Override
+ public void onPluginConfig(JfinalPlugins plugins) {
+
+ }
+
+ @Override
+ public void onInterceptorConfig(Interceptors interceptors) {
+ //开启注解方式权限验证
+ interceptors.add(new SaAnnotationInterceptor());
+ }
+
+ @Override
+ public void onHandlerConfig(JfinalHandlers handlers) {
+
+ }
+
+ @Override
+ public void onStartBefore() {
+
+ }
+
+ @Override
+ public void onStart() {
+
+ }
+
+ @Override
+ public void onStartFinish() {
+
+ }
+
+ @Override
+ public void onStop() {
+
+ }
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java
new file mode 100644
index 00000000..183b4515
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java
@@ -0,0 +1,23 @@
+package cn.dev33.satoken.jboot.test;
+
+import cn.dev33.satoken.stp.StpInterface;
+import io.jboot.aop.annotation.Bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Bean
+public class StpInterfaceImpl implements StpInterface {
+ @Override
+ public List getPermissionList(Object o, String s) {
+ return null;
+ }
+
+ @Override
+ public List getRoleList(Object o, String s) {
+ List list = new ArrayList();
+ list.add("admin");
+ list.add("super-admin");
+ return list;
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/pom.xml b/sa-token-starter/sa-token-jfinal-plugin/pom.xml
new file mode 100644
index 00000000..612683c2
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/pom.xml
@@ -0,0 +1,88 @@
+
+
+
+ sa-token-starter
+ cn.dev33
+ 1.28.0
+
+ 4.0.0
+ jar
+
+ sa-token-jfinal-plugin
+ jfinal integrate sa-token
+
+
+ 8
+ 8
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.24
+
+
+ com.jfinal
+ jfinal-undertow
+ 2.7
+
+
+ com.jfinal
+ jfinal
+ 4.9.17
+ provided
+
+
+ cn.dev33
+ sa-token-core
+ ${sa-token-version}
+
+
+ cn.dev33
+ sa-token-servlet
+ ${sa-token-version}
+
+
+ org.apache.commons
+ commons-pool2
+ 2.11.1
+ test
+
+
+ redis.clients
+ jedis
+ 3.7.0
+ test
+
+
+ slf4j-api
+ org.slf4j
+
+
+
+
+ de.ruedigermoeller
+ fst
+ 2.29
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.6.1
+
+ 1.8
+ 1.8
+ UTF-8
+ -parameters
+
+
+
+
+
\ No newline at end of file
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java
new file mode 100644
index 00000000..d2305a07
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java
@@ -0,0 +1,60 @@
+package cn.dev33.satoken.jfinal;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PathAnalyzer {
+
+ private static Map cached = new LinkedHashMap();
+ private Pattern pattern;
+
+ public static PathAnalyzer get(String expr) {
+ PathAnalyzer pa = (PathAnalyzer)cached.get(expr);
+ if (pa == null) {
+ synchronized(expr.intern()) {
+ pa = (PathAnalyzer)cached.get(expr);
+ if (pa == null) {
+ pa = new PathAnalyzer(expr);
+ cached.put(expr, pa);
+ }
+ }
+ }
+
+ return pa;
+ }
+
+ private PathAnalyzer(String expr) {
+ this.pattern = Pattern.compile(exprCompile(expr), 2);
+ }
+
+ public Matcher matcher(String uri) {
+ return this.pattern.matcher(uri);
+ }
+
+ public boolean matches(String uri) {
+ return this.pattern.matcher(uri).find();
+ }
+
+ private static String exprCompile(String expr) {
+ String p = expr.replace(".", "\\.");
+ p = p.replace("$", "\\$");
+ p = p.replace("**", ".[]");
+ p = p.replace("*", "[^/]*");
+ if (p.indexOf("{") >= 0) {
+ if (p.indexOf("_}") > 0) {
+ p = p.replaceAll("\\{[^\\}]+?\\_\\}", "(.+?)");
+ }
+
+ p = p.replaceAll("\\{[^\\}]+?\\}", "([^/]+?)");
+ }
+
+ if (!p.startsWith("/")) {
+ p = "/" + p;
+ }
+
+ p = p.replace(".[]", ".*");
+ return "^" + p + "$";
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java
new file mode 100644
index 00000000..ed9b84f5
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java
@@ -0,0 +1,16 @@
+package cn.dev33.satoken.jfinal;
+
+import cn.dev33.satoken.strategy.SaStrategy;
+import com.jfinal.aop.Interceptor;
+import com.jfinal.aop.Invocation;
+
+/**
+ * 注解式鉴权 - 拦截器
+ */
+public class SaAnnotationInterceptor implements Interceptor {
+ @Override
+ public void intercept(Invocation invocation) {
+ SaStrategy.me.checkMethodAnnotation.accept((invocation.getMethod()));
+ invocation.invoke();
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java
new file mode 100644
index 00000000..64cdc36c
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java
@@ -0,0 +1,20 @@
+package cn.dev33.satoken.jfinal;
+
+import com.jfinal.core.Controller;
+
+public class SaControllerContext {
+ private static ThreadLocal controllers = new ThreadLocal<>();
+
+
+ public static void hold(Controller controller) {
+ controllers.set(controller);
+ }
+
+ public static Controller get() {
+ return controllers.get();
+ }
+
+ public static void release() {
+ controllers.remove();
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java
new file mode 100644
index 00000000..56c9e903
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java
@@ -0,0 +1,155 @@
+package cn.dev33.satoken.jfinal;
+
+import com.jfinal.aop.Invocation;
+import com.jfinal.config.Constants;
+import com.jfinal.core.*;
+import com.jfinal.kit.ReflectKit;
+import com.jfinal.log.Log;
+import com.jfinal.render.Render;
+import com.jfinal.render.RenderException;
+import com.jfinal.render.RenderManager;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class SaTokenActionHandler extends ActionHandler {
+ protected boolean devMode;
+ protected ActionMapping actionMapping;
+ protected ControllerFactory controllerFactory;
+ protected ActionReporter actionReporter;
+ protected static final RenderManager renderManager = RenderManager.me();
+ private static final Log log = Log.getLog(ActionHandler.class);
+
+ protected void init(ActionMapping actionMapping, Constants constants) {
+ this.actionMapping = actionMapping;
+ this.devMode = constants.getDevMode();
+ this.controllerFactory = constants.getControllerFactory();
+ this.actionReporter = constants.getActionReporter();
+ }
+
+ /**
+ * 子类覆盖 getAction 方法可以定制路由功能
+ */
+ protected Action getAction(String target, String[] urlPara) {
+ return actionMapping.getAction(target, urlPara);
+ }
+
+ @Override
+ public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {
+ if (target.indexOf('.') != -1) {
+ return ;
+ }
+
+ isHandled[0] = true;
+ String[] urlPara = {null};
+ Action action = getAction(target, urlPara);
+
+ if (action == null) {
+ if (log.isWarnEnabled()) {
+ String qs = request.getQueryString();
+ log.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs));
+ }
+ renderManager.getRenderFactory().getErrorRender(404).setContext(request, response).render();
+ return ;
+ }
+
+ Controller controller = null;
+ try {
+ // Controller controller = action.getControllerClass().newInstance();
+ controller = controllerFactory.getController(action.getControllerClass());
+ CPI._init_(controller, action, request, response, urlPara[0]);
+ //加入SaToken上下文处理
+ SaControllerContext.hold(controller);
+
+ if (devMode) {
+ if (actionReporter.isReportAfterInvocation(request)) {
+ new Invocation(action, controller).invoke();
+ actionReporter.report(target, controller, action);
+ } else {
+ actionReporter.report(target, controller, action);
+ new Invocation(action, controller).invoke();
+ }
+ }
+ else {
+ new Invocation(action, controller).invoke();
+ }
+
+ Render render = controller.getRender();
+ if (render instanceof ForwardActionRender) {
+ String actionUrl = ((ForwardActionRender)render).getActionUrl();
+ if (target.equals(actionUrl)) {
+ throw new RuntimeException("The forward action url is the same as before.");
+ } else {
+ handle(actionUrl, request, response, isHandled);
+ }
+ return ;
+ }
+
+ if (render == null) {
+ render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName());
+ }
+ render.setContext(request, response, action.getViewPath()).render();
+ }
+ catch (RenderException e) {
+ if (log.isErrorEnabled()) {
+ String qs = request.getQueryString();
+ log.error(qs == null ? target : target + "?" + qs, e);
+ }
+ }
+ catch (ActionException e) {
+ handleActionException(target, request, response, action, e);
+ }
+ catch (Exception e) {
+ if (log.isErrorEnabled()) {
+ String qs = request.getQueryString();
+ String targetInfo = (qs == null ? target : target + "?" + qs);
+ String sign = ReflectKit.getMethodSignature(action.getMethod());
+ log.error(sign + " : " + targetInfo, e);
+ }
+ renderManager.getRenderFactory().getErrorRender(500).setContext(request, response, action.getViewPath()).render();
+ } finally {
+ SaControllerContext.release();
+ controllerFactory.recycle(controller);
+ }
+ }
+
+ /**
+ * 抽取出该方法是为了缩短 handle 方法中的代码量,确保获得 JIT 优化,
+ * 方法长度超过 8000 个字节码时,将不会被 JIT 编译成二进制码
+ *
+ * 通过开启 java 的 -XX:+PrintCompilation 启动参数得知,handle(...)
+ * 方法(73 行代码)已被 JIT 优化,优化后的字节码长度为 593 个字节,相当于
+ * 每行代码产生 8.123 个字节
+ */
+ private void handleActionException(String target, HttpServletRequest request, HttpServletResponse response, Action action, ActionException e) {
+ int errorCode = e.getErrorCode();
+ String msg = null;
+ if (errorCode == 404) {
+ msg = "404 Not Found: ";
+ } else if (errorCode == 400) {
+ msg = "400 Bad Request: ";
+ } else if (errorCode == 401) {
+ msg = "401 Unauthorized: ";
+ } else if (errorCode == 403) {
+ msg = "403 Forbidden: ";
+ }
+
+ if (msg != null) {
+ if (log.isWarnEnabled()) {
+ String qs = request.getQueryString();
+ msg = msg + (qs == null ? target : target + "?" + qs);
+ if (e.getMessage() != null) {
+ msg = msg + "\n" + e.getMessage();
+ }
+ log.warn(msg);
+ }
+ } else {
+ if (log.isErrorEnabled()) {
+ String qs = request.getQueryString();
+ log.error(errorCode + " Error: " + (qs == null ? target : target + "?" + qs), e);
+ }
+ }
+
+ e.getErrorRender().setContext(request, response, action.getViewPath()).render();
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java
new file mode 100644
index 00000000..72aadc52
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java
@@ -0,0 +1,51 @@
+package cn.dev33.satoken.jfinal;
+
+import cn.dev33.satoken.context.SaTokenContext;
+import cn.dev33.satoken.context.model.SaRequest;
+import cn.dev33.satoken.context.model.SaResponse;
+import cn.dev33.satoken.context.model.SaStorage;
+import cn.dev33.satoken.servlet.model.SaRequestForServlet;
+import cn.dev33.satoken.servlet.model.SaResponseForServlet;
+import cn.dev33.satoken.servlet.model.SaStorageForServlet;
+
+/**
+ * Sa-Token 上线文处理器 [Jfinal 版本实现]
+ */
+public class SaTokenContextForJfinal implements SaTokenContext {
+ /**
+ * 获取当前请求的Request对象
+ */
+ @Override
+ public SaRequest getRequest() {
+ return new SaRequestForServlet(SaControllerContext.get().getRequest());
+ }
+
+ /**
+ * 获取当前请求的Response对象
+ */
+ @Override
+ public SaResponse getResponse() {
+ return new SaResponseForServlet(SaControllerContext.get().getResponse());
+ }
+
+ /**
+ * 获取当前请求的 [存储器] 对象
+ */
+ @Override
+ public SaStorage getStorage() {
+ return new SaStorageForServlet(SaControllerContext.get().getRequest());
+ }
+
+ /**
+ * 校验指定路由匹配符是否可以匹配成功指定路径
+ */
+ @Override
+ public boolean matchPath(String pattern, String path) {
+ return PathAnalyzer.get(pattern).matches(path);
+ }
+
+ @Override
+ public boolean isValid() {
+ return SaTokenContext.super.isValid();
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java
new file mode 100644
index 00000000..38417458
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java
@@ -0,0 +1,200 @@
+package cn.dev33.satoken.jfinal;
+
+import cn.dev33.satoken.dao.SaTokenDao;
+import cn.dev33.satoken.util.SaFoxUtil;
+import com.jfinal.plugin.redis.Cache;
+import com.jfinal.plugin.redis.Redis;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class SaTokenDaoRedis implements SaTokenDao {
+
+ private Cache redis = null;
+ /**
+ * 标记:是否已初始化成功
+ */
+ public boolean isInit;
+
+ public SaTokenDaoRedis(String confName){
+ redis = Redis.use(confName);
+ this.isInit = redis != null;
+ }
+
+ /**
+ * 获取Value,如无返空
+ * @param key
+ * @return
+ */
+ @Override
+ public String get(String key) {
+ return redis.get(key);
+ }
+
+ /**
+ * 写入Value,并设定存活时间 (单位: 秒)
+ * @param key
+ * @param value
+ * @param timeout
+ */
+ @Override
+ public void set(String key, String value, long timeout) {
+ if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ redis.set(key, value);
+ }else{
+ redis.setex(key,Integer.parseInt(timeout+""),value);
+ }
+ }
+
+ /**
+ * 修改指定key-value键值对 (过期时间不变)
+ * @param key
+ * @param value
+ */
+ @Override
+ public void update(String key, String value) {
+ long expire = getTimeout(key);
+ // -2 = 无此键
+ if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ this.set(key,value,expire);
+ }
+
+ /**
+ * 删除Value
+ * @param key
+ */
+ @Override
+ public void delete(String key) {
+ redis.del(key);
+ }
+
+ /**
+ * 获取Value的剩余存活时间 (单位: 秒)
+ * @param key
+ * @return
+ */
+ @Override
+ public long getTimeout(String key) {
+ return redis.ttl(key);
+ }
+
+ /**
+ * 修改Value的剩余存活时间 (单位: 秒)
+ * @param key
+ * @param timeout
+ */
+ @Override
+ public void updateTimeout(String key, long timeout) {
+ //判断是否想要设置为永久
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ long expire = getTimeout(key);
+ if(expire == SaTokenDao.NEVER_EXPIRE) {
+ // 如果其已经被设置为永久,则不作任何处理
+ } else {
+ // 如果尚未被设置为永久,那么再次set一次
+ this.set(key, this.get(key), timeout);
+ }
+ return;
+ }
+ redis.expire(key,Integer.parseInt(timeout+""));
+ }
+
+ /**
+ * 获取Object,如无返空
+ * @param key
+ * @return
+ */
+ @Override
+ public Object getObject(String key) {
+ return redis.get(key);
+ }
+
+ /**
+ * 写入Object,并设定存活时间 (单位: 秒)
+ * @param key
+ * @param object
+ * @param timeout
+ */
+ @Override
+ public void setObject(String key, Object object, long timeout) {
+ if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ redis.set(key, object);
+ }else{
+ redis.setex(key,Integer.parseInt(timeout+""),object);
+ }
+ }
+
+ /**
+ * 更新Object (过期时间不变)
+ * @param key
+ * @param object
+ */
+ @Override
+ public void updateObject(String key, Object object) {
+ long expire = getObjectTimeout(key);
+ // -2 = 无此键
+ if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ this.setObject(key, object, expire);
+ }
+
+ /**
+ * 删除Object
+ * @param key
+ */
+ @Override
+ public void deleteObject(String key) {
+ redis.del(key);
+ }
+
+ @Override
+ public long getObjectTimeout(String key) {
+ return redis.ttl(key);
+ }
+
+ /**
+ * 修改Object的剩余存活时间 (单位: 秒)
+ * @param key
+ * @param timeout
+ */
+ @Override
+ public void updateObjectTimeout(String key, long timeout) {
+ //判断是否想要设置为永久
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ long expire = getTimeout(key);
+ if(expire == SaTokenDao.NEVER_EXPIRE) {
+ // 如果其已经被设置为永久,则不作任何处理
+ } else {
+ // 如果尚未被设置为永久,那么再次set一次
+ this.set(key, this.get(key), timeout);
+ }
+ return;
+ }
+ redis.expire(key,Integer.parseInt(timeout+""));
+ }
+
+ /**
+ * 搜索数据
+ * @param prefix
+ * @param keyword
+ * @param start
+ * @param size
+ * @return
+ */
+ @Override
+ public List searchData(String prefix, String keyword, int start, int size) {
+ Set keys = redis.keys(prefix + "*" + keyword + "*");
+ List list = new ArrayList(keys);
+ return SaFoxUtil.searchList(list, start, size);
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java
new file mode 100644
index 00000000..731ef14c
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java
@@ -0,0 +1,154 @@
+package cn.dev33.satoken.jfinal;
+
+import cn.dev33.satoken.exception.SaTokenException;
+import cn.dev33.satoken.filter.SaFilterAuthStrategy;
+import cn.dev33.satoken.filter.SaFilterErrorStrategy;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SaTokenPathFilter {
+
+ // ------------------------ 设置此过滤器 拦截 & 放行 的路由
+
+ /**
+ * 拦截路由
+ */
+ private List includeList = new ArrayList<>();
+
+ /**
+ * 放行路由
+ */
+ private List excludeList = new ArrayList<>();
+
+ /**
+ * 添加 [拦截路由]
+ * @param paths 路由
+ * @return 对象自身
+ */
+ public SaTokenPathFilter addInclude(String... paths) {
+ includeList.addAll(Arrays.asList(paths));
+ return this;
+ }
+
+ /**
+ * 添加 [放行路由]
+ * @param paths 路由
+ * @return 对象自身
+ */
+ public SaTokenPathFilter addExclude(String... paths) {
+ excludeList.addAll(Arrays.asList(paths));
+ return this;
+ }
+
+ /**
+ * 写入 [拦截路由] 集合
+ * @param pathList 路由集合
+ * @return 对象自身
+ */
+ public SaTokenPathFilter setIncludeList(List pathList) {
+ includeList = pathList;
+ return this;
+ }
+
+ /**
+ * 写入 [放行路由] 集合
+ * @param pathList 路由集合
+ * @return 对象自身
+ */
+ public SaTokenPathFilter setExcludeList(List pathList) {
+ excludeList = pathList;
+ return this;
+ }
+
+ /**
+ * 获取 [拦截路由] 集合
+ * @return see note
+ */
+ public List getIncludeList() {
+ return includeList;
+ }
+
+ /**
+ * 获取 [放行路由] 集合
+ * @return see note
+ */
+ public List getExcludeList() {
+ return excludeList;
+ }
+
+
+ // ------------------------ 钩子函数
+
+ /**
+ * 认证函数:每次请求执行
+ */
+ public SaFilterAuthStrategy auth = r -> {};
+
+ /**
+ * 异常处理函数:每次[认证函数]发生异常时执行此函数
+ */
+ public SaFilterErrorStrategy error = e -> {
+ throw new SaTokenException(e);
+ };
+
+ /**
+ * 前置函数:在每次[认证函数]之前执行
+ */
+ public SaFilterAuthStrategy beforeAuth = r -> {};
+
+ /**
+ * 写入[认证函数]: 每次请求执行
+ * @param auth see note
+ * @return 对象自身
+ */
+ public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) {
+ this.auth = auth;
+ return this;
+ }
+
+ /**
+ * 写入[异常处理函数]:每次[认证函数]发生异常时执行此函数
+ * @param error see note
+ * @return 对象自身
+ */
+ public SaTokenPathFilter setError(SaFilterErrorStrategy error) {
+ this.error = error;
+ return this;
+ }
+
+ /**
+ * 写入[前置函数]:在每次[认证函数]之前执行
+ * @param beforeAuth see note
+ * @return 对象自身
+ */
+ public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {
+ this.beforeAuth = beforeAuth;
+ return this;
+ }
+
+
+ /*@Override
+ public void doFilter(Controller ctx, FilterChain chain) throws Throwable {
+ try {
+ // 执行全局过滤器
+ SaRouter.match(includeList).notMatch(excludeList).check(r -> {
+ beforeAuth.run(null);
+ auth.run(null);
+ });
+
+ } catch (StopMatchException e) {
+
+ } catch (Throwable e) {
+ // 1. 获取异常处理策略结果
+ String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));
+ // 2. 写入输出流
+ ctx.renderText(result);
+ return;
+ }
+
+ // 执行
+ chain.doFilter(ctx);
+ }*/
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java
new file mode 100644
index 00000000..606f2355
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java
@@ -0,0 +1,38 @@
+package cn.dev33.satoken.jfinal.test;
+
+import cn.dev33.satoken.annotation.SaCheckRole;
+import cn.dev33.satoken.stp.StpUtil;
+import com.jfinal.core.Controller;
+import com.jfinal.core.Path;
+import com.jfinal.server.undertow.UndertowServer;
+
+@Path("/")
+public class AppRun extends Controller {
+ public static void main(String[] args) {
+ UndertowServer.create(Config.class)
+ .addHotSwapClassPrefix("cn.dev33.satoken.jfinal.")
+ .start();
+ }
+
+ public void index(){
+ renderText("index");
+ }
+
+ public void doLogin(){
+ StpUtil.logout();
+ StpUtil.login(10002);
+ //赋值角色
+ renderText("登录成功");
+ }
+
+ public void getLoginInfo(){
+ System.out.println("是否登录:"+StpUtil.isLogin());
+ System.out.println("登录信息"+StpUtil.getTokenInfo());
+ renderJson(StpUtil.getTokenInfo());
+ }
+
+ @SaCheckRole("super-admin")
+ public void add(){
+ renderText("超级管理员方法!");
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java
new file mode 100644
index 00000000..d4445dcb
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java
@@ -0,0 +1,72 @@
+package cn.dev33.satoken.jfinal.test;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.context.SaTokenContext;
+import com.jfinal.config.*;
+import com.jfinal.plugin.redis.RedisPlugin;
+import com.jfinal.template.Engine;
+import cn.dev33.satoken.jfinal.SaAnnotationInterceptor;
+import cn.dev33.satoken.jfinal.SaTokenActionHandler;
+import cn.dev33.satoken.jfinal.SaTokenContextForJfinal;
+import cn.dev33.satoken.jfinal.SaTokenDaoRedis;
+
+public class Config extends JFinalConfig {
+
+ public Config(){
+ //注册权限验证功能,由saToken处理请求上下文
+ SaTokenContext saTokenContext = new SaTokenContextForJfinal();
+ SaManager.setSaTokenContext(saTokenContext);
+ //加载权限角色设置数据接口
+ SaManager.setStpInterface(new StpInterfaceImpl());
+
+ }
+
+ @Override
+ public void configConstant(Constants constants) {
+
+ }
+
+ @Override
+ public void configRoute(Routes routes) {
+ //路由扫描
+ routes.scan("cn.dev33.satoken.jfinal");
+ }
+
+ @Override
+ public void configEngine(Engine engine) {
+
+ }
+
+ @Override
+ public void configPlugin(Plugins plugins) {
+ //添加redis扩展
+// plugins.add(createRedisPlugin("satoken",10));
+ }
+
+ @Override
+ public void configInterceptor(Interceptors interceptors) {
+ //开启注解方式权限验证
+ interceptors.add(new SaAnnotationInterceptor());
+ }
+
+ @Override
+ public void configHandler(Handlers handlers) {
+ //将上下文交给satoken处理
+ handlers.setActionHandler(new SaTokenActionHandler());
+ }
+
+ /**
+ * 创建Redis插件
+ * @param name 名称
+ * @param dbIndex 使用的库ID
+ * @return
+ */
+ private RedisPlugin createRedisPlugin(String name, Integer dbIndex) {
+ return new RedisPlugin(name, "redis-host", 6379, 3000,"pwd",dbIndex);
+ }
+ @Override
+ public void onStart(){
+ //增加redis缓存,需要先配置redis地址
+// SaManager.setSaTokenDao(new SaTokenDaoRedis("satoken"));
+ }
+}
diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java
new file mode 100644
index 00000000..f4ab4366
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java
@@ -0,0 +1,21 @@
+package cn.dev33.satoken.jfinal.test;
+
+import cn.dev33.satoken.stp.StpInterface;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StpInterfaceImpl implements StpInterface {
+ @Override
+ public List getPermissionList(Object o, String s) {
+ return null;
+ }
+
+ @Override
+ public List getRoleList(Object o, String s) {
+ List list = new ArrayList();
+ list.add("admin");
+ list.add("super-admin");
+ return list;
+ }
+}