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; + } +}