diff --git a/sa-token-starter/pom.xml b/sa-token-starter/pom.xml
index ded82e3a..2ff62a9b 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..37edaa66
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/pom.xml
@@ -0,0 +1,55 @@
+
+
+
+ sa-token-starter
+ cn.dev33
+ 1.29.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/SaJdkSerializer.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJdkSerializer.java
new file mode 100644
index 00000000..7f8d694d
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJdkSerializer.java
@@ -0,0 +1,57 @@
+package cn.dev33.satoken.jboot;
+
+import com.jfinal.log.Log;
+import io.jboot.components.serializer.JbootSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+public class SaJdkSerializer implements JbootSerializer {
+
+ private static final Log LOG = Log.getLog(SaJdkSerializer.class);
+
+ @Override
+ public byte[] serialize(Object value) {
+ if (value == null) {
+ return null;
+ }
+ ObjectOutputStream objectOut = null;
+ try {
+ ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024);
+ objectOut = new ObjectOutputStream(bytesOut);
+ objectOut.writeObject(value);
+ objectOut.flush();
+ return bytesOut.toByteArray();
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ if(objectOut != null)
+ try {objectOut.close();} catch (Exception e) {
+ LOG.error(e.getMessage(), e);}
+ }
+ }
+
+ @Override
+ public Object deserialize(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return null;
+ }
+ ObjectInputStream objectInput = null;
+ try {
+ ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes);
+ objectInput = new ObjectInputStream(bytesInput);
+ return objectInput.readObject();
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ if (objectInput != null)
+ try {objectInput.close();} catch (Exception e) {LOG.error(e.getMessage(), e);}
+ }
+ }
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJedisImpl.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJedisImpl.java
new file mode 100644
index 00000000..652aabd0
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJedisImpl.java
@@ -0,0 +1,459 @@
+package cn.dev33.satoken.jboot;
+
+import cn.dev33.satoken.util.SaFoxUtil;
+import com.jfinal.log.Log;
+import io.jboot.Jboot;
+import io.jboot.components.serializer.JbootSerializer;
+import io.jboot.components.serializer.JbootSerializerManager;
+import io.jboot.exception.JbootIllegalConfigException;
+import io.jboot.utils.StrUtil;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class SaJedisImpl {
+
+ private final JbootSerializer serializer;
+
+ protected JedisPool jedisPool;
+ private static final Log LOG = Log.getLog(SaJedisImpl.class);
+ private final SaRedisConfig config;
+ public SaJedisImpl(SaRedisConfig config) {
+ if (config == null || StrUtil.isBlank(config.getSerializer())) {
+ serializer = Jboot.getSerializer();
+ } else {
+ serializer = JbootSerializerManager.me().getSerializer(config.getSerializer());
+ }
+ this.config = config;
+ assert this.config != null;
+ String host = this.config.getHost();
+ Integer port = this.config.getPort();
+ Integer timeout = this.config.getTimeout();
+ String password = this.config.getPassword();
+ Integer database = this.config.getSaDb()==null?this.config.getDatabase():this.config.getSaDb();
+ String clientName = this.config.getClientName();
+
+ if (host.contains(":")) {
+ port = Integer.valueOf(host.split(":")[1]);
+ }
+
+
+ JedisPoolConfig poolConfig = new JedisPoolConfig();
+
+ if (StrUtil.isNotBlank(config.getTestWhileIdle())) {
+ poolConfig.setTestWhileIdle(config.getTestWhileIdle());
+ }
+
+ if (StrUtil.isNotBlank(config.getTestOnBorrow())) {
+ poolConfig.setTestOnBorrow(config.getTestOnBorrow());
+ }
+
+ if (StrUtil.isNotBlank(config.getTestOnCreate())) {
+ poolConfig.setTestOnCreate(config.getTestOnCreate());
+ }
+
+ if (StrUtil.isNotBlank(config.getTestOnReturn())) {
+ poolConfig.setTestOnReturn(config.getTestOnReturn());
+ }
+
+ if (StrUtil.isNotBlank(config.getMinEvictableIdleTimeMillis())) {
+ poolConfig.setMinEvictableIdleTime(Duration.ofMillis(config.getMinEvictableIdleTimeMillis()));
+ }
+
+ if (StrUtil.isNotBlank(config.getTimeBetweenEvictionRunsMillis())) {
+ poolConfig.setSoftMinEvictableIdleTime(Duration.ofMillis(config.getTimeBetweenEvictionRunsMillis()));
+ }
+
+ if (StrUtil.isNotBlank(config.getNumTestsPerEvictionRun())) {
+ poolConfig.setNumTestsPerEvictionRun(config.getNumTestsPerEvictionRun());
+ }
+
+ if (StrUtil.isNotBlank(config.getMaxTotal())) {
+ poolConfig.setMaxTotal(config.getMaxTotal());
+ }
+
+ if (StrUtil.isNotBlank(config.getMaxIdle())) {
+ poolConfig.setMaxIdle(config.getMaxIdle());
+ }
+
+ if (StrUtil.isNotBlank(config.getMinIdle())) {
+ poolConfig.setMinIdle(config.getMinIdle());
+ }
+
+ if (StrUtil.isNotBlank(config.getMaxWaitMillis())) {
+ poolConfig.setMaxWaitMillis(config.getMaxWaitMillis());
+ }
+
+ this.jedisPool = new JedisPool(poolConfig, host, port, timeout, timeout, password, database, clientName);
+ }
+
+ /**
+ * 获取Value,如无返空
+ * @param key
+ * @return
+ */
+ public String get(String key){
+ Jedis jedis = getJedis();
+ try{
+ return jedis.get(key);
+ }finally {
+ returnResource(jedis);
+ }
+ }
+ /**
+ * 写入Value
+ * @param key
+ * @param value
+ */
+ public void set(String key, String value) {
+ Jedis jedis = getJedis();
+ try{
+ jedis.set(key, value);
+ }finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 写入Value,并设定存活时间 (单位: 秒)
+ * @param key
+ * @param value
+ * @param timeout
+ */
+ public void setex(String key, String value, long timeout){
+ Jedis jedis = getJedis();
+ try{
+ jedis.setex(key,timeout,value);
+ }finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 删除Value
+ * @param key
+ */
+ public void del(String key) {
+ Jedis jedis = getJedis();
+ try{
+ jedis.del(key);
+ }finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 获取Value的剩余存活时间 (单位: 秒)
+ * @param key
+ * @return
+ */
+ public long ttl(String key) {
+ Jedis jedis = getJedis();
+ try{
+ return jedis.ttl(key);
+ }finally {
+ returnResource(jedis);
+ }
+ }
+
+ public void expire(String key,long timeout){
+ Jedis jedis = getJedis();
+ try {
+ jedis.expire(key,timeout);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 获取Object,如无返空
+ * @param key
+ * @return
+ */
+ public Object getObject(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return valueFromBytes(jedis.get(keyToBytes(key)));
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 写入Object
+ * @param key
+ * @param value
+ */
+ public void setObject(Object key, Object value) {
+ Jedis jedis = getJedis();
+ try {
+ jedis.set(keyToBytes(key), valueToBytes(value));
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 写入Object,并设定存活时间 (单位: 秒)
+ * @param key
+ * @param object
+ * @param timeout
+ */
+ public void setexObject(String key, Object object,long timeout) {
+ Jedis jedis = getJedis();
+ try {
+ jedis.setex(keyToBytes(key),timeout, valueToBytes(object));
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 删除Object
+ * @param key
+ */
+ public void deleteObject(String key) {
+ Jedis jedis = getJedis();
+ try {
+ jedis.del(keyToBytes(key));
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ public long getObjectTimeout(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return getJedis().ttl(keyToBytes(key));
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 修改Object的剩余存活时间 (单位: 秒)
+ * @param key
+ * @param timeout
+ */
+ public void expireObject(String key, long timeout) {
+ Jedis jedis = getJedis();
+ try {
+ jedis.expire(keyToBytes(key), timeout);
+ }
+ finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 查找所有符合给定模式 pattern 的 key 。
+ * KEYS * 匹配数据库中所有 key 。
+ * KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
+ * KEYS h*llo 匹配 hllo 和 heeeeello 等。
+ * KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。
+ * 特殊符号用 \ 隔开
+ */
+ public Set keys(String pattern) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.keys(pattern);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 将哈希表 key 中的域 field 的值设为 value 。
+ * 如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。
+ * 如果域 field 已经存在于哈希表中,旧值将被覆盖。
+ */
+ public Long hset(String key, String field, String value) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hset(key, field, value);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 同时将多个 field-value (域-值)对设置到哈希表 key 中。
+ * 此命令会覆盖哈希表中已存在的域。
+ * 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。
+ */
+ public String hmset(String key, Map hash) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hmset(key,hash);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+ /**
+ * 返回哈希表 key 中给定域 field 的值。
+ * @param key
+ * @param field
+ * @return
+ */
+ public String hget(String key,String field){
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hget(key,field);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 返回哈希表 key 中,一个或多个给定域的值。
+ * 如果给定的域不存在于哈希表,那么返回一个 nil 值。
+ * 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。
+ */
+ public List hmget(String key, String... fields) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hmget(key,fields);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+ /**
+ * 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
+ * @param key
+ * @param fields
+ * @return
+ */
+ public Long hdel(String key, String... fields) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hdel(key,fields);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+ /**
+ * 查看哈希表 key 中,给定域 field 是否存在。
+ * @param key
+ * @param field
+ * @return
+ */
+ public boolean hexists(String key, String field){
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hexists(key,field);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+ /**
+ * 返回哈希表 key 中,所有的域和值。
+ * 在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。
+ */
+ public Map hgetAll(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hgetAll(key);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 返回哈希表 key 中所有域的值。
+ * @param key
+ * @return
+ */
+ public List hvals(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hvals(key);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 返回哈希表 key 中的所有域。
+ * @param key
+ * @return
+ */
+ public Set hkeys(String key){
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hkeys(key);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ /**
+ * 返回哈希表 key 中域的数量。
+ * @param key
+ * @return
+ */
+ public Long hlen(String key){
+ Jedis jedis = getJedis();
+ try {
+ return jedis.hlen(key);
+ } finally {
+ returnResource(jedis);
+ }
+ }
+
+ public Jedis getJedis() {
+ try {
+ return jedisPool.getResource();
+ } catch (JedisConnectionException e) {
+ throw new JbootIllegalConfigException("can not connect to redis host " + config.getHost() + ":" + config.getPort() + " ," +
+ " cause : " + e.toString(), e);
+ }
+ }
+
+
+ public void returnResource(Jedis jedis) {
+ if (jedis != null) {
+ jedis.close();
+ }
+ }
+
+ public byte[] keyToBytes(Object key) {
+ return key.toString().getBytes();
+ }
+
+ public String bytesToKey(byte[] bytes) {
+ return new String(bytes);
+ }
+
+ public byte[] valueToBytes(Object value) {
+ return serializer.serialize(value);
+ }
+
+ public Object valueFromBytes(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return null;
+ }
+ return serializer.deserialize(bytes);
+ }
+
+ /**
+ * 搜索数据
+ * @param prefix
+ * @param keyword
+ * @param start
+ * @param size
+ * @return
+ */
+ public List searchData(String prefix, String keyword, int start, int size) {
+ Set keys = getJedis().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/main/java/cn/dev33/satoken/jboot/SaRedisConfig.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisConfig.java
new file mode 100644
index 00000000..33dc56be
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisConfig.java
@@ -0,0 +1,27 @@
+package cn.dev33.satoken.jboot;
+
+import io.jboot.app.config.annotation.ConfigModel;
+import io.jboot.support.redis.JbootRedisConfig;
+
+/**
+ * SA-Token redis缓存配置,获取database
+ */
+@ConfigModel(
+ prefix = "jboot.redis"
+)
+public class SaRedisConfig extends JbootRedisConfig{
+
+ private Integer saDb;
+
+ public SaRedisConfig(){
+
+ }
+ public Integer getSaDb() {
+ return this.saDb;
+ }
+
+ public void setSaDb(Integer saDb) {
+ this.saDb = saDb;
+ }
+
+}
diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisManager.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisManager.java
new file mode 100644
index 00000000..08c5b3c8
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisManager.java
@@ -0,0 +1,38 @@
+package cn.dev33.satoken.jboot;
+
+import io.jboot.Jboot;
+
+public class SaRedisManager {
+
+ private static SaRedisManager manager = new SaRedisManager();
+
+ private SaRedisManager() {
+ }
+
+ public static SaRedisManager me() {
+ return manager;
+ }
+
+ private SaJedisImpl redis;
+
+ public SaJedisImpl getRedis() {
+ if (redis == null) {
+ SaRedisConfig config = Jboot.config(SaRedisConfig.class);
+ redis = getRedis(config);
+ }
+
+ return redis;
+ }
+
+ public SaJedisImpl getRedis(SaRedisConfig config) {
+ if (config == null || !config.isConfigOk()) {
+ return null;
+ }
+ return getJedisClient(config);
+ }
+
+
+ private SaJedisImpl getJedisClient(SaRedisConfig config) {
+ return new SaJedisImpl(config);
+ }
+}
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..457a5735
--- /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..ce038125
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenDaoRedis.java
@@ -0,0 +1,211 @@
+package cn.dev33.satoken.jboot;
+
+import cn.dev33.satoken.dao.SaTokenDao;
+import cn.dev33.satoken.util.SaFoxUtil;
+import io.jboot.Jboot;
+import io.jboot.exception.JbootIllegalConfigException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class SaTokenDaoRedis implements SaTokenDao {
+
+ private SaJedisImpl redis;
+ /**
+ * 标记:是否已初始化成功
+ */
+ public boolean isInit;
+
+ public SaTokenDaoRedis(){
+ SaRedisConfig redisConfig = Jboot.config(SaRedisConfig.class);
+ redisConfig.setSerializer("cn.dev33.satoken.jboot.SaJdkSerializer");
+ //优先使用 jboot.cache.redis 的配置
+ if (redisConfig.isConfigOk()) {
+ redis = SaRedisManager.me().getRedis(redisConfig);
+ }
+
+ 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,value,timeout);
+ }
+ }
+
+ /**
+ * 修改指定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,timeout);
+ }
+
+ /**
+ * 获取Object,如无返空
+ * @param key
+ * @return
+ */
+ @Override
+ public Object getObject(String key) {
+ return redis.getObject(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.setObject(key, object);
+ }else{
+ redis.setexObject(key,object,timeout);
+ }
+ }
+
+ /**
+ * 更新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.deleteObject(key);
+ }
+
+ @Override
+ public long getObjectTimeout(String key) {
+ return redis.getObjectTimeout(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.setObject(key, this.get(key), timeout);
+ }
+ return;
+ }
+ redis.expireObject(key,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/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java
new file mode 100644
index 00000000..7e24ffa6
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java
@@ -0,0 +1,154 @@
+package cn.dev33.satoken.jboot;
+
+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-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..64580ec0
--- /dev/null
+++ b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java
@@ -0,0 +1,85 @@
+package cn.dev33.satoken.jboot.test;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.config.SaCookieConfig;
+import cn.dev33.satoken.config.SaTokenConfig;
+import cn.dev33.satoken.context.SaTokenContext;
+import cn.dev33.satoken.jboot.SaAnnotationInterceptor;
+import cn.dev33.satoken.jboot.SaTokenContextForJboot;
+import cn.dev33.satoken.jboot.SaTokenDaoRedis;
+import cn.dev33.satoken.util.SaTokenConsts;
+import com.jfinal.config.Constants;
+import com.jfinal.config.Interceptors;
+import com.jfinal.config.Routes;
+import com.jfinal.template.Engine;
+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() {
+ SaTokenContext saTokenContext = new SaTokenContextForJboot();
+ SaManager.setSaTokenContext(saTokenContext);
+ SaManager.setStpInterface(new StpInterfaceImpl());
+ SaTokenConfig saTokenConfig = new SaTokenConfig();
+ saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID);
+ saTokenConfig.setTimeout(60*60*4); //登录有效时间4小时
+ saTokenConfig.setActivityTimeout(30*60); //半小时无操作过期处理
+ saTokenConfig.setIsShare(false);
+ saTokenConfig.setTokenName("token"); //更换satoken的名称
+ saTokenConfig.setCookie(new SaCookieConfig().setHttpOnly(true)); //开启cookies的httponly属性
+ SaManager.setConfig(saTokenConfig);
+ 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..4ce99d15
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/pom.xml
@@ -0,0 +1,87 @@
+
+
+
+ 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.8
+
+
+ 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
+
+
+ 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..257e50f2
--- /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.contains("{")) {
+ 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/SaJdkSerializer.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaJdkSerializer.java
new file mode 100644
index 00000000..bb1836d6
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaJdkSerializer.java
@@ -0,0 +1,70 @@
+package cn.dev33.satoken.jfinal;
+
+import com.jfinal.kit.LogKit;
+import com.jfinal.plugin.redis.serializer.ISerializer;
+import com.jfinal.plugin.redis.serializer.JdkSerializer;
+import redis.clients.jedis.util.SafeEncoder;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+public class SaJdkSerializer implements ISerializer {
+
+ public static final ISerializer me = new JdkSerializer();
+
+ public byte[] keyToBytes(String key) {
+ return SafeEncoder.encode(key);
+ }
+
+ public String keyFromBytes(byte[] bytes) {
+ return SafeEncoder.encode(bytes);
+ }
+
+ public byte[] fieldToBytes(Object field) {
+ return valueToBytes(field);
+ }
+
+ public Object fieldFromBytes(byte[] bytes) {
+ return valueFromBytes(bytes);
+ }
+
+ public byte[] valueToBytes(Object value) {
+ ObjectOutputStream objectOut = null;
+ try {
+ ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024);
+ objectOut = new ObjectOutputStream(bytesOut);
+ objectOut.writeObject(value);
+ objectOut.flush();
+ return bytesOut.toByteArray();
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ if(objectOut != null)
+ try {objectOut.close();} catch (Exception e) {
+ LogKit.error(e.getMessage(), e);}
+ }
+ }
+
+ public Object valueFromBytes(byte[] bytes) {
+ if(bytes == null || bytes.length == 0)
+ return null;
+
+ ObjectInputStream objectInput = null;
+ try {
+ ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes);
+ objectInput = new ObjectInputStream(bytesInput);
+ return objectInput.readObject();
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ if (objectInput != null)
+ try {objectInput.close();} catch (Exception e) {LogKit.error(e.getMessage(), e);}
+ }
+ }
+}
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..a3553dc4
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java
@@ -0,0 +1,156 @@
+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..6ea97aff
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java
@@ -0,0 +1,286 @@
+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 com.jfinal.plugin.redis.serializer.ISerializer;
+import redis.clients.jedis.Jedis;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class SaTokenDaoRedis implements SaTokenDao {
+
+ protected Cache redis;
+ protected ISerializer serializer;
+ /**
+ * 标记:是否已初始化成功
+ */
+ public boolean isInit;
+
+ public SaTokenDaoRedis(String confName) {
+ redis = Redis.use(confName);
+ serializer = new SaJdkSerializer();
+ }
+
+ /**
+ * 获取Value,如无返空
+ *
+ * @param key
+ * @return
+ */
+ @Override
+ public String get(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.get(key);
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 写入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;
+ }
+ Jedis jedis = getJedis();
+ try {
+ if (timeout == SaTokenDao.NEVER_EXPIRE) {
+ jedis.set(key, value);
+ } else {
+ jedis.setex(key, timeout, value);
+ }
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 修改指定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) {
+ Jedis jedis = getJedis();
+ try {
+ jedis.del(key);
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 获取Value的剩余存活时间 (单位: 秒)
+ *
+ * @param key
+ * @return
+ */
+ @Override
+ public long getTimeout(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.ttl(key);
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 修改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;
+ }
+ Jedis jedis = getJedis();
+ try {
+ jedis.expire(key, timeout);
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 获取Object,如无返空
+ *
+ * @param key
+ * @return
+ */
+ @Override
+ public Object getObject(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return valueFromBytes(jedis.get(keyToBytes(key)));
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 写入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;
+ }
+ Jedis jedis = getJedis();
+ try {
+ if (timeout == SaTokenDao.NEVER_EXPIRE) {
+ jedis.set(keyToBytes(key), valueToBytes(object));
+ } else {
+ jedis.setex(keyToBytes(key), timeout, valueToBytes(object));
+ }
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 更新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) {
+ Jedis jedis = getJedis();
+ try {
+ jedis.del(keyToBytes(key));
+ } finally {
+ close(jedis);
+ }
+ }
+
+ @Override
+ public long getObjectTimeout(String key) {
+ Jedis jedis = getJedis();
+ try {
+ return jedis.ttl(keyToBytes(key));
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 修改Object的剩余存活时间 (单位: 秒)
+ *
+ * @param key
+ * @param timeout
+ */
+ @Override
+ public void updateObjectTimeout(String key, long timeout) {
+ //判断是否想要设置为永久
+ if (timeout == SaTokenDao.NEVER_EXPIRE) {
+ long expire = getObjectTimeout(key);
+ if (expire == SaTokenDao.NEVER_EXPIRE) {
+ // 如果其已经被设置为永久,则不作任何处理
+ } else {
+ // 如果尚未被设置为永久,那么再次set一次
+ this.setObject(key, this.getObject(key), timeout);
+ }
+ return;
+ }
+ Jedis jedis = getJedis();
+ try {
+ jedis.expire(keyToBytes(key), timeout);
+ } finally {
+ close(jedis);
+ }
+ }
+
+ /**
+ * 搜索数据
+ *
+ * @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);
+ }
+
+ public Jedis getJedis() {
+ return redis.getJedis();
+ }
+
+ public void close(Jedis jedis) {
+ if (jedis != null)
+ jedis.close();
+ }
+
+ protected byte[] keyToBytes(Object key) {
+ return key.toString().getBytes();
+ }
+
+ protected byte[] valueToBytes(Object value) {
+ return serializer.valueToBytes(value);
+ }
+
+ protected Object valueFromBytes(byte[] bytes) {
+ return serializer.valueFromBytes(bytes);
+ }
+}
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..edf9d288
--- /dev/null
+++ b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java
@@ -0,0 +1,86 @@
+package cn.dev33.satoken.jfinal.test;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.config.SaCookieConfig;
+import cn.dev33.satoken.config.SaTokenConfig;
+import cn.dev33.satoken.context.SaTokenContext;
+import cn.dev33.satoken.jfinal.*;
+import cn.dev33.satoken.util.SaTokenConsts;
+import com.jfinal.config.*;
+import com.jfinal.plugin.redis.RedisPlugin;
+import com.jfinal.plugin.redis.serializer.ISerializer;
+import com.jfinal.template.Engine;
+
+public class Config extends JFinalConfig {
+
+ public Config(){
+ //注册权限验证功能,由saToken处理请求上下文
+ SaTokenContext saTokenContext = new SaTokenContextForJfinal();
+ SaManager.setSaTokenContext(saTokenContext);
+ //加载权限角色设置数据接口
+ SaManager.setStpInterface(new StpInterfaceImpl());
+ //设置token生成类型
+ SaTokenConfig saTokenConfig = new SaTokenConfig();
+ saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID);
+ saTokenConfig.setTimeout(60*60*4); //登录有效时间4小时
+ saTokenConfig.setActivityTimeout(30*60); //半小时无操作过期处理
+ saTokenConfig.setIsShare(false);
+ saTokenConfig.setTokenName("token"); //更改satoken的cookies名称
+ SaCookieConfig saCookieConfig = new SaCookieConfig();
+ saCookieConfig.setHttpOnly(true); //开启cookies 的httponly属性
+ saTokenConfig.setCookie(saCookieConfig);
+ SaManager.setConfig(saTokenConfig);
+ }
+
+ @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",1, SaJdkSerializer.me));
+ }
+
+ @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
+ * @param serializer 自定义序列化方法
+ * @return
+ */
+ private RedisPlugin createRedisPlugin(String name, Integer dbIndex, ISerializer serializer) {
+ RedisPlugin redisPlugin = new RedisPlugin(name, "redis-host", 6379, 3000,"pwd",dbIndex);
+ redisPlugin.setSerializer(serializer);
+ return redisPlugin;
+ }
+ @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;
+ }
+}