├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── checkstyle.xml ├── docs ├── README.md └── _config.yml ├── pom.xml └── src ├── integration-test └── java │ ├── org │ └── crazycake │ │ └── shiro │ │ ├── RedisSessionDAOIntegrationTest.java │ │ └── integration │ │ ├── RedisCacheTest.java │ │ ├── RedisManagerTest.java │ │ └── fixture │ │ ├── TestFixture.java │ │ └── model │ │ ├── FakeAuth.java │ │ ├── FakeSession.java │ │ └── UserInfo.java │ └── shiro-standalone.ini ├── main └── java │ └── org │ └── crazycake │ └── shiro │ ├── IRedisManager.java │ ├── LettuceRedisClusterManager.java │ ├── LettuceRedisManager.java │ ├── LettuceRedisSentinelManager.java │ ├── RedisCache.java │ ├── RedisCacheManager.java │ ├── RedisClusterManager.java │ ├── RedisManager.java │ ├── RedisSentinelManager.java │ ├── RedisSessionDAO.java │ ├── common │ ├── AbstractLettuceRedisManager.java │ ├── SessionInMemory.java │ └── WorkAloneRedisManager.java │ ├── exception │ ├── CacheManagerPrincipalIdNotAssignedException.java │ ├── PoolException.java │ ├── PrincipalIdNullException.java │ ├── PrincipalInstanceException.java │ └── SerializationException.java │ └── serializer │ ├── MultiClassLoaderObjectInputStream.java │ ├── ObjectSerializer.java │ ├── RedisSerializer.java │ └── StringSerializer.java └── test └── java └── org └── crazycake └── shiro ├── RedisCacheManagerTest.java ├── RedisCacheTest.java ├── RedisClusterManagerTest.java └── RedisSessionDAOTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | .classpath 3 | .project 4 | target 5 | .idea 6 | shiro-redis.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | services: 3 | - redis-server -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 xi yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | shiro-redis 2 | ============= 3 | 4 | ## Introduction 5 | 6 | shiro only provide the support of ehcache and concurrentHashMap. Here is an implement of redis cache can be used by shiro. Hope it will help you! 7 | 8 | ## Documentation 9 | 10 | Official documentation [is located here](http://alexxiyang.github.io/shiro-redis/). 11 | 12 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-merlot -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | shiro-redis 2 | ============= 3 | 4 | [![Build Status](https://travis-ci.org/alexxiyang/shiro-redis.svg?branch=master)](https://travis-ci.org/alexxiyang/shiro-redis) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.crazycake/shiro-redis/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.crazycake/shiro-redis) 6 | 7 | shiro only provide the support of ehcache and concurrentHashMap. Here is an implement of redis cache can be used by shiro. Hope it will help you! 8 | 9 | # Download 10 | 11 | You use either of the following 2 ways to include `shiro-redis` into your project 12 | * use `git clone https://github.com/alexxiyang/shiro-redis.git` to clone project to your local workspace and build jar file by your self 13 | * add maven dependency 14 | 15 | ```xml 16 | 17 | org.crazycake 18 | shiro-redis 19 | 3.3.1 20 | 21 | ``` 22 | 23 | > **Note:**\ 24 | > 3.3.0 is compiled in java11 by mistake. 25 | > Please use 3.3.1 which is compiled in java8 26 | 27 | ## shiro-core/jedis Version Comparison Charts 28 | 29 | | shiro-redis | shiro | jedis | 30 | | :----------------:| :-------: | :-------: | 31 | | 3.2.3 | 1.3.2 | 2.9.0 | 32 | | 3.3.0 (java11) | 1.6.0 | 3.3.0 | 33 | | 3.3.1 (java8) | 1.6.0 | 3.3.0 | 34 | 35 | # Before use 36 | Here is the first thing you need to know. Shiro-redis needs an id field to identify your authorization object in Redis. So please make sure your principal class has a field which you can get unique id of this object. Please setting this id field name by `cacheManager.principalIdFieldName = ` 37 | 38 | For example: 39 | 40 | If you create `SimpleAuthenticationInfo` like this: 41 | ```java 42 | @Override 43 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 44 | UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token; 45 | UserInfo userInfo = new UserInfo(); 46 | userInfo.setUsername(usernamePasswordToken.getUsername()); 47 | return new SimpleAuthenticationInfo(userInfo, "123456", getName()); 48 | } 49 | ``` 50 | 51 | Then the `userInfo` object is your principal object. You need to make sure `UserInfo` has an unique field for Redis to identify it. Take `userId` as an example: 52 | ```java 53 | public class UserInfo implements Serializable{ 54 | 55 | private Integer userId 56 | 57 | private String username; 58 | 59 | public String getUsername() { 60 | return username; 61 | } 62 | 63 | public void setUsername(String username) { 64 | this.username = username; 65 | } 66 | 67 | public Integer getUserId() { 68 | return this.userId; 69 | } 70 | } 71 | ``` 72 | 73 | Put userId as the value of `cacheManager.principalIdFieldName`, like this: 74 | ```properties 75 | cacheManager.principalIdFieldName = userId 76 | ``` 77 | 78 | If you're using Spring, the configuration should be 79 | ```xml 80 | 81 | ``` 82 | 83 | Then `shiro-redis` will call `userInfo.getUserId()` to get the id for saving Redis object. 84 | 85 | # How to configure ? 86 | 87 | You can configure `shiro-redis` either in `shiro.ini` or in `spring-*.xml` 88 | 89 | ## shiro.ini 90 | Here is the configuration example for shiro.ini. 91 | 92 | ### Redis Standalone 93 | If you are running Redis in Standalone mode 94 | 95 | ```properties 96 | [main] 97 | #==================================== 98 | # shiro-redis configuration [start] 99 | #==================================== 100 | 101 | #=================================== 102 | # Redis Manager [start] 103 | #=================================== 104 | 105 | # Create redisManager 106 | redisManager = org.crazycake.shiro.RedisManager 107 | 108 | # Redis host. If you don't specify host the default value is 127.0.0.1:6379 109 | redisManager.host = 127.0.0.1:6379 110 | 111 | #=================================== 112 | # Redis Manager [end] 113 | #=================================== 114 | 115 | #========================================= 116 | # Redis session DAO [start] 117 | #========================================= 118 | 119 | # Create redisSessionDAO 120 | redisSessionDAO = org.crazycake.shiro.RedisSessionDAO 121 | 122 | # Use redisManager as cache manager 123 | redisSessionDAO.redisManager = $redisManager 124 | 125 | sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager 126 | 127 | sessionManager.sessionDAO = $redisSessionDAO 128 | 129 | securityManager.sessionManager = $sessionManager 130 | 131 | #========================================= 132 | # Redis session DAO [end] 133 | #========================================= 134 | 135 | #========================================== 136 | # Redis cache manager [start] 137 | #========================================== 138 | 139 | # Create cacheManager 140 | cacheManager = org.crazycake.shiro.RedisCacheManager 141 | 142 | # Principal id field name. The field which you can get unique id to identify this principal. 143 | # For example, if you use UserInfo as Principal class, the id field maybe `id`, `userId`, `email`, etc. 144 | # Remember to add getter to this id field. For example, `getId()`, `getUserId()`, `getEmail()`, etc. 145 | # Default value is id, that means your principal object must has a method called `getId()` 146 | cacheManager.principalIdFieldName = id 147 | 148 | # Use redisManager as cache manager 149 | cacheManager.redisManager = $redisManager 150 | 151 | securityManager.cacheManager = $cacheManager 152 | 153 | #========================================== 154 | # Redis cache manager [end] 155 | #========================================== 156 | 157 | #================================= 158 | # shiro-redis configuration [end] 159 | #================================= 160 | ``` 161 | 162 | For complete configurable options list, check [Configurable Options](#configurable-options). 163 | 164 | Here is a [tutorial project](https://github.com/alexxiyang/shiro-redis-tutorial) for you to understand how to configure `shiro-redis` in `shiro.ini`. 165 | 166 | ### Redis Sentinel 167 | if you're using Redis Sentinel, please replace the `redisManager` configuration of the standalone version into the following: 168 | ```properties 169 | #=================================== 170 | # Redis Manager [start] 171 | #=================================== 172 | 173 | # Create redisManager 174 | redisManager = org.crazycake.shiro.RedisSentinelManager 175 | 176 | # Sentinel host. If you don't specify host the default value is 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381 177 | redisManager.host = 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381 178 | 179 | # Sentinel master name 180 | redisManager.masterName = mymaster 181 | 182 | #=================================== 183 | # Redis Manager [end] 184 | #=================================== 185 | ``` 186 | 187 | For complete configurable options list, check [Configurable Options](#configurable-options). 188 | 189 | ### Redis Cluster 190 | If you're using redis cluster, please replace the `redisManager` configuration of the standalone version into the following: 191 | 192 | ```properties 193 | #=================================== 194 | # Redis Manager [start] 195 | #=================================== 196 | 197 | # Create redisManager 198 | redisManager = org.crazycake.shiro.RedisClusterManager 199 | 200 | # Redis host and port list 201 | redisManager.host = 192.168.21.3:7000,192.168.21.3:7001,192.168.21.3:7002,192.168.21.3:7003,192.168.21.3:7004,192.168.21.3:7005 202 | 203 | #=================================== 204 | # Redis Manager [end] 205 | #=================================== 206 | ``` 207 | 208 | For complete configurable options list, check [Configurable Options](#configurable-options). 209 | 210 | ## Spring 211 | If you are using Spring 212 | 213 | ### Redis Standalone 214 | If you are running Redis in Standalone mode 215 | 216 | ```xml 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | ``` 251 | 252 | For complete configurable options list, check [Configurable Options](#configurable-options). 253 | 254 | Here is a [tutorial project](https://github.com/alexxiyang/shiro-redis-spring-tutorial) for you to understand how to configure `shiro-redis` in spring configuration file. 255 | 256 | ### Redis Sentinel 257 | If you use redis sentinel, please replace the `redisManager` configuration of the standalone version into the following: 258 | ```xml 259 | 260 | 261 | 262 | 263 | 264 | 265 | ``` 266 | 267 | For complete configurable options list, check [Configurable Options](#configurable-options). 268 | 269 | ### Redis Cluster 270 | If you use redis cluster, please replace the `redisManager` configuration of the standalone version into the following: 271 | ```xml 272 | 273 | 274 | 275 | 276 | 277 | ``` 278 | 279 | For complete configurable options list, check [Configurable Options](#configurable-options). 280 | 281 | ## Serializer 282 | Since redis only accept `byte[]`, there comes a serializer problem. 283 | Shiro-redis is using `StringSerializer` as key serializer and `ObjectSerializer` as value serializer. 284 | You can use your own custom serializer, as long as this custom serializer implements `org.crazycake.shiro.serializer.RedisSerializer` 285 | 286 | For example, we can change the charset of keySerializer like this 287 | ```properties 288 | # If you want change charset of keySerializer or use your own custom serializer, you need to define serializer first 289 | # 290 | # cacheManagerKeySerializer = org.crazycake.shiro.serializer.StringSerializer 291 | 292 | # Supported encodings refer to https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html 293 | # UTF-8, UTF-16, UTF-32, ISO-8859-1, GBK, Big5, etc 294 | # 295 | # cacheManagerKeySerializer.charset = UTF-8 296 | 297 | # cacheManager.keySerializer = $cacheManagerKeySerializer 298 | ``` 299 | 300 | These 4 options that you can replace them with your cutom serializers: 301 | - cacheManager.keySerializer 302 | - cacheManager.valueSerializer 303 | - redisSessionDAO.keySerializer 304 | - redisSessionDAO.valueSerializer 305 | 306 | ## Configurable Options 307 | Here are all the available options you can use in `shiro-redis` configuration file. 308 | 309 | ### RedisManager 310 | 311 | | Title | Default | Description | 312 | | :------------------| :------------------- | :---------------------------| 313 | | host | `127.0.0.1:6379` | Redis host. If you don't specify host the default value is `127.0.0.1:6379`. If you run redis in sentinel mode or cluster mode, separate host names with comma, like `127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381` | 314 | | masterName | `mymaster` | **Only used for sentinel mode**
The master node of Redis sentinel mode | 315 | | timeout | `2000` | Redis connect timeout. Timeout for jedis try to connect to redis server(In milliseconds) | 316 | | soTimeout | `2000` | **Only used for sentinel mode or cluster mode**
The timeout for jedis try to read data from redis server | 317 | | maxAttempts | `3` | **Only used for cluster mode**
Max attempts to connect to server | 318 | | password | | Redis password | 319 | | database | `0` | Redis database. Default value is 0 | 320 | | jedisPoolConfig | `new redis.clients.jedis.JedisPoolConfig()` | JedisPoolConfig. You can create your own JedisPoolConfig instance and set attributes as you wish
Most of time, you don't need to set jedisPoolConfig
Here is an example.
`jedisPoolConfig = redis.clients.jedis.JedisPoolConfig`
`jedisPoolConfig.testWhileIdle = false`
`redisManager.jedisPoolConfig = jedisPoolConfig` | 321 | | count | `100` | Scan count. Shiro-redis use Scan to get keys, so you can define the number of elements returned at every iteration. | 322 | | jedisPool | `null` | **Only used for sentinel mode or single mode**
You can create your own JedisPool instance and set attributes as you wish | 323 | 324 | ### RedisSessionDAO 325 | 326 | | Title | Default | Description | 327 | | :------------------| :------------------- | :---------------------------| 328 | | redisManager | | RedisManager which you just configured above (Required) | 329 | | expire | `-2` | Redis cache key/value expire time. The expire time is in second.
Special values:
`-1`: no expire
`-2`: the same timeout with session
Default value: `-2`
**Note**: Make sure expire time is longer than session timeout. | 330 | | keyPrefix | `shiro:session:` | Custom your redis key prefix for session management
**Note**: Remember to add colon at the end of prefix. | 331 | | sessionInMemoryTimeout | `1000` | When we do signin, `doReadSession(sessionId)` will be called by shiro about 10 times. So shiro-redis save Session in ThreadLocal to remit this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal.
Most of time, you don't need to change it. | 332 | | sessionInMemoryEnabled | `true` | Whether or not enable temporary save session in ThreadLocal | 333 | | keySerializer | `org.crazycake.shiro.serializer.StringSerializer` | The key serializer of cache manager
You can change the implement of key serializer or the encoding of StringSerializer.
Supported encodings refer to [Supported Encodings](https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html). Such as `UTF-8`, `UTF-16`, `UTF-32`, `ISO-8859-1`, `GBK`, `Big5`, etc
For more detail, check [Serializer](#serializer) | 334 | | valueSerializer | `org.crazycake.shiro.serializer.ObjectSerializer` | The value serializer of cache manager
You can change the implement of value serializer
For more detail, check [Serializer](#serializer) | 335 | 336 | ### CacheManager 337 | 338 | | Title | Default | Description | 339 | | :--------------------| :------------------- | :---------------------------| 340 | | redisManager | | RedisManager which you just configured above (Required) | 341 | | principalIdFieldName | `id` | Principal id field name. The field which you can get unique id to identify this principal.
For example, if you use UserInfo as Principal class, the id field maybe `id`, `userId`, `email`, etc.
Remember to add getter to this id field. For example, `getId()`, `getUserId(`), `getEmail()`, etc.
Default value is `id`, that means your principal object must has a method called `getId()` | 342 | | expire | `1800` | Redis cache key/value expire time.
The expire time is in second. | 343 | | keyPrefix | `shiro:cache:` | Custom your redis key prefix for cache management
**Note**: Remember to add colon at the end of prefix. | 344 | | keySerializer | `org.crazycake.shiro.serializer.StringSerializer` | The key serializer of cache manager
You can change the implement of key serializer or the encoding of StringSerializer.
Supported encodings refer to [Supported Encodings](https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html). Such as `UTF-8`, `UTF-16`, `UTF-32`, `ISO-8859-1`, `GBK`, `Big5`, etc
For more detail, check [Serializer](#serializer) | 345 | | valueSerializer | `org.crazycake.shiro.serializer.ObjectSerializer` | The value serializer of cache manager
You can change the implement of value serializer
For more detail, check [Serializer](#serializer) | 346 | 347 | # Spring boot starter 348 | 349 | Using `Spring-Boot` integration is the easiest way to integrate `shiro-redis` into a Spring-base application. 350 | 351 | > Note: `shiro-redis-spring-boot-starter` version `3.2.1` is based on `shiro-spring-boot-web-starter` version `1.4.0-RC2` 352 | 353 | First include the `shiro-redis` Spring boot starter dependency in you application classpath 354 | 355 | ```xml 356 | 357 | org.crazycake 358 | shiro-redis-spring-boot-starter 359 | 3.3.1 360 | 361 | ``` 362 | 363 | The next step depends on whether you've created your own `SessionManager` or `SessionsSecurityManager`. 364 | ## If you haven't created your own `SessionManager` or `SessionsSecurityManager` 365 | If you don't have your own `SessionManager` or `SessionsSecurityManager` in your configuration, `shiro-redis-spring-boot-starter` will create `RedisSessionDAO` and `RedisCacheManager` for you. Then inject them into `SessionManager` and `SessionsSecurityManager` automatically. 366 | So, You are all set. Enjoy it! 367 | 368 | ## If you have created your own `SessionManager` or `SessionsSecurityManager` 369 | If you have created your own `SessionManager` or `SessionsSecurityManager` like this: 370 | ```java 371 | @Bean 372 | public SessionsSecurityManager securityManager(List realms) { 373 | DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realms); 374 | 375 | // other stuff... 376 | 377 | return securityManager; 378 | } 379 | ``` 380 | 381 | Then inject `redisSessionDAO` and `redisCacheManager` which created by `shiro-redis-spring-boot-starter` already 382 | ```java 383 | @Autowired 384 | RedisSessionDAO redisSessionDAO; 385 | 386 | @Autowired 387 | RedisCacheManager redisCacheManager; 388 | ``` 389 | 390 | Inject them into your own `SessionManager` and `SessionsSecurityManager` 391 | 392 | ```java 393 | @Bean 394 | public SessionManager sessionManager() { 395 | DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); 396 | 397 | // inject redisSessionDAO 398 | sessionManager.setSessionDAO(redisSessionDAO); 399 | 400 | // other stuff... 401 | 402 | return sessionManager; 403 | } 404 | 405 | @Bean 406 | public SessionsSecurityManager securityManager(List realms, SessionManager sessionManager) { 407 | DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realms); 408 | 409 | //inject sessionManager 410 | securityManager.setSessionManager(sessionManager); 411 | 412 | // inject redisCacheManager 413 | securityManager.setCacheManager(redisCacheManager); 414 | 415 | // other stuff... 416 | 417 | return securityManager; 418 | } 419 | ``` 420 | 421 | For full example, see [shiro-redis-spring-boot-tutorial](https://github.com/alexxiyang/shiro-redis-spring-boot-tutorial) 422 | 423 | ### Configuration Properties 424 | Here are all available options you can use in Spring-boot starter configuration 425 | 426 | | Title | Default | Description | 427 | | :--------------------------------------------------| :------------------- | :---------------------------| 428 | | shiro-redis.enabled | `true` | Enables shiro-redis’s Spring module | 429 | | shiro-redis.redis-manager.deploy-mode | `standalone` | Redis deploy mode. Options: `standalone`, `sentinel`, 'cluster' | 430 | | shiro-redis.redis-manager.host | `127.0.0.1:6379` | Redis host. If you don't specify host the default value is `127.0.0.1:6379`. If you run redis in sentinel mode or cluster mode, separate host names with comma, like `127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381` | 431 | | shiro-redis.redis-manager.master-name | `mymaster` | **Only used for sentinel mode**
The master node of Redis sentinel mode | 432 | | shiro-redis.redis-manager.timeout | `2000` | Redis connect timeout. Timeout for jedis try to connect to redis server(In milliseconds) | 433 | | shiro-redis.redis-manager.so-timeout | `2000` | **Only used for sentinel mode or cluster mode**
The timeout for jedis try to read data from redis server | 434 | | shiro-redis.redis-manager.max-attempts | `3` | **Only used for cluster mode**
Max attempts to connect to server | 435 | | shiro-redis.redis-manager.password | | Redis password | 436 | | shiro-redis.redis-manager.database | `0` | Redis database. Default value is 0 | 437 | | shiro-redis.redis-manager.count | `100` | Scan count. Shiro-redis use Scan to get keys, so you can define the number of elements returned at every iteration. | 438 | | shiro-redis.session-dao.expire | `-2` | Redis cache key/value expire time. The expire time is in second.
Special values:
`-1`: no expire
`-2`: the same timeout with session
Default value: `-2`
**Note**: Make sure expire time is longer than session timeout. | 439 | | shiro-redis.session-dao.key-prefix | `shiro:session:` | Custom your redis key prefix for session management
**Note**: Remember to add colon at the end of prefix. | 440 | | shiro-redis.session-dao.session-in-memory-timeout | `1000` | When we do signin, `doReadSession(sessionId)` will be called by shiro about 10 times. So shiro-redis save Session in ThreadLocal to remit this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal.
Most of time, you don't need to change it. | 441 | | shiro-redis.session-dao.session-in-memory-enabled | `true` | Whether or not enable temporary save session in ThreadLocal | 442 | | shiro-redis.cache-manager.principal-id-field-name | `id` | Principal id field name. The field which you can get unique id to identify this principal.
For example, if you use UserInfo as Principal class, the id field maybe `id`, `userId`, `email`, etc.
Remember to add getter to this id field. For example, `getId()`, `getUserId(`), `getEmail()`, etc.
Default value is `id`, that means your principal object must has a method called `getId()` | 443 | | shiro-redis.cache-manager.expire | `1800` | Redis cache key/value expire time.
The expire time is in second. | 444 | | shiro-redis.cache-manager.key-prefix | `shiro:cache:` | Custom your redis key prefix for cache management
**Note**: Remember to add colon at the end of prefix. | 445 | 446 | 447 | ## Working with `spring-boot-devtools` 448 | If you are using `shiro-redis` with `spring-boot-devtools`. Please add this line to `resources/META-INF/spring-devtools.properties` (Create it if there is no this file): 449 | ```ini 450 | restart.include.shiro-redis=/shiro-[\\w-\\.]+jar 451 | ``` 452 | 453 | # If you found any bugs 454 | 455 | Please create the issue 456 | 457 | 可以用中文 458 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-merlot 2 | show_downloads: true -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.crazycake 6 | shiro-redis 7 | 3.3.2 8 | jar 9 | 10 | shiro-redis 11 | shiro only provide the support of ehcache and concurrentHashMap. Here is an implement of redis cache can be used by shiro. Hope it will help you! 12 | https://github.com/alexxiyang/shiro-redis 13 | 14 | 15 | UTF-8 16 | 17 | 18 | 19 | 20 | The Apache Software License, Version 2.0 21 | http://www.apache.org/licenses/LICENSE-2.0.txt 22 | repo 23 | 24 | 25 | 26 | 27 | 28 | redis.clients 29 | jedis 30 | 3.6.0 31 | true 32 | 33 | 34 | io.lettuce 35 | lettuce-core 36 | 6.2.3.RELEASE 37 | true 38 | 39 | 40 | 41 | org.slf4j 42 | slf4j-api 43 | 1.7.30 44 | 45 | 46 | org.apache.shiro 47 | shiro-core 48 | 1.11.0 49 | 50 | 51 | 52 | 53 | org.junit.jupiter 54 | junit-jupiter-api 55 | 5.6.2 56 | test 57 | 58 | 59 | org.slf4j 60 | slf4j-simple 61 | 1.7.30 62 | test 63 | 64 | 65 | commons-logging 66 | commons-logging 67 | 1.2 68 | test 69 | 70 | 71 | org.mockito 72 | mockito-core 73 | 3.5.7 74 | test 75 | 76 | 77 | com.github.javafaker 78 | javafaker 79 | 1.0.2 80 | test 81 | 82 | 83 | org.hamcrest 84 | hamcrest 85 | 2.2 86 | test 87 | 88 | 89 | 90 | 91 | 92 | alexxiyang 93 | Alex Yang 94 | alexxiyang@gmail.com 95 | GMT-7 96 | https://github.com/alexxiyang 97 | 98 | 99 | 100 | 101 | 102 | scm:git:https://github.com/alexxiyang/shiro-redis.git 103 | scm:git:https://github.com/alexxiyang/shiro-redis.git 104 | https://github.com/alexxiyang/shiro-redis.git 105 | 106 | 107 | 108 | ossrh 109 | https://oss.sonatype.org/content/repositories/snapshots 110 | 111 | 112 | ossrh 113 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 114 | 115 | 116 | 117 | shiro-redis 118 | 119 | 120 | maven-compiler-plugin 121 | 3.8.0 122 | 123 | 1.8 124 | 1.8 125 | UTF-8 126 | -nowarn 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-checkstyle-plugin 132 | 3.1.0 133 | 134 | 135 | checkstyle 136 | validate 137 | 138 | check 139 | 140 | 141 | true 142 | true 143 | checkstyle.xml 144 | 145 | 146 | 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-surefire-plugin 151 | 2.22.0 152 | 153 | 154 | 155 | org.junit.platform 156 | junit-platform-surefire-provider 157 | 1.3.2 158 | 159 | 160 | org.junit.jupiter 161 | junit-jupiter-engine 162 | 5.6.2 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | release-sign-artifacts 172 | 173 | 174 | release 175 | true 176 | 177 | 178 | 179 | 180 | D688E942 181 | alexxiyang 182 | 183 | 184 | 185 | 186 | 187 | org.sonatype.plugins 188 | nexus-staging-maven-plugin 189 | 1.6.8 190 | true 191 | 192 | ossrh 193 | https://oss.sonatype.org/ 194 | true 195 | 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-source-plugin 200 | 3.2.1 201 | 202 | 203 | attach-sources 204 | 205 | jar-no-fork 206 | 207 | 208 | 209 | 210 | 211 | org.apache.maven.plugins 212 | maven-javadoc-plugin 213 | 3.2.0 214 | 215 | 216 | attach-javadocs 217 | 218 | jar 219 | 220 | 221 | 222 | 223 | 224 | org.apache.maven.plugins 225 | maven-gpg-plugin 226 | 1.6 227 | 228 | 229 | sign-artifacts 230 | verify 231 | 232 | sign 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /src/integration-test/java/org/crazycake/shiro/RedisSessionDAOIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.apache.shiro.session.Session; 4 | import org.apache.shiro.session.UnknownSessionException; 5 | import org.crazycake.shiro.common.SessionInMemory; 6 | import org.crazycake.shiro.exception.SerializationException; 7 | import org.crazycake.shiro.integration.fixture.model.FakeSession; 8 | import org.crazycake.shiro.serializer.ObjectSerializer; 9 | import org.crazycake.shiro.serializer.StringSerializer; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.io.Serializable; 16 | import java.util.Collection; 17 | import java.util.Map; 18 | 19 | import static org.crazycake.shiro.integration.fixture.TestFixture.*; 20 | import static org.hamcrest.CoreMatchers.*; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | 23 | /** 24 | * RedisSessionDAO integration test was put under org.crazycake.shiro 25 | * is because I want to test protected method `doReadSession` 26 | */ 27 | public class RedisSessionDAOIntegrationTest { 28 | 29 | private RedisSessionDAO redisSessionDAO; 30 | private FakeSession session1; 31 | private FakeSession session2; 32 | private FakeSession emptySession; 33 | private String name1; 34 | private String prefix; 35 | private StringSerializer keySerializer = new StringSerializer(); 36 | private ObjectSerializer valueSerializer = new ObjectSerializer(); 37 | 38 | private void blast() { 39 | blastRedis(); 40 | } 41 | 42 | private void scaffold() { 43 | prefix = scaffoldPrefix(); 44 | RedisManager redisManager = scaffoldStandaloneRedisManager(); 45 | redisSessionDAO = scaffoldRedisSessionDAO(redisManager, prefix); 46 | session1 = scaffoldSession(); 47 | session2 = scaffoldSession(); 48 | emptySession = scaffoldEmptySession(); 49 | name1 = scaffoldUsername(); 50 | } 51 | 52 | @BeforeEach 53 | public void setUp() { 54 | blast(); 55 | scaffold(); 56 | } 57 | 58 | @AfterEach 59 | public void tearDown() { 60 | blast(); 61 | } 62 | 63 | @Test 64 | public void testDoCreateNull() { 65 | Assertions.assertThrows(UnknownSessionException.class, () -> { 66 | redisSessionDAO.doCreate(null); 67 | }); 68 | } 69 | 70 | @Test 71 | public void testDoCreate() { 72 | redisSessionDAO.doCreate(session1); 73 | Session actualSession = redisSessionDAO.doReadSession(session1.getId()); 74 | assertSessionEquals(actualSession, session1); 75 | } 76 | 77 | @Test 78 | public void testDoCreateWithSessionTimeout() { 79 | doSetSessionDAOExpire(redisSessionDAO, -2); 80 | redisSessionDAO.doCreate(session2); 81 | assertEquals(getRedisTTL(prefix + session2.getId(), new StringSerializer()), 1800L); 82 | } 83 | 84 | @Test 85 | public void testUpdateNull() { 86 | Assertions.assertThrows(UnknownSessionException.class, () -> { 87 | redisSessionDAO.update(null); 88 | }); 89 | } 90 | 91 | @Test 92 | public void testUpdateEmptySession() { 93 | Assertions.assertThrows(UnknownSessionException.class, () -> { 94 | redisSessionDAO.update(emptySession); 95 | }); 96 | } 97 | 98 | @Test 99 | public void testUpdate() { 100 | redisSessionDAO.doCreate(session1); 101 | redisSessionDAO.doReadSession(session1.getId()); 102 | doChangeSessionName(session1, name1); 103 | redisSessionDAO.update(session1); 104 | FakeSession actualSession = (FakeSession)redisSessionDAO.doReadSession(session1.getId()); 105 | assertEquals(actualSession.getName(), name1); 106 | } 107 | 108 | @Test 109 | public void testUpdateWithoutSessionInMemory() { 110 | redisSessionDAO.setSessionInMemoryEnabled(false); 111 | redisSessionDAO.doCreate(session1); 112 | redisSessionDAO.doReadSession(session1.getId()); 113 | doChangeSessionName(session1, name1); 114 | redisSessionDAO.update(session1); 115 | FakeSession actualSession = (FakeSession)redisSessionDAO.doReadSession(session1.getId()); 116 | assertEquals(actualSession.getName(), name1); 117 | } 118 | 119 | @Test 120 | public void testDelete() { 121 | redisSessionDAO.doCreate(session1); 122 | redisSessionDAO.delete(session1); 123 | assertRedisEmpty(); 124 | } 125 | 126 | @Test 127 | public void testGetActiveSessions() { 128 | redisSessionDAO.doCreate(session1); 129 | redisSessionDAO.doCreate(session2); 130 | Collection activeSessions = redisSessionDAO.getActiveSessions(); 131 | assertEquals(activeSessions.size(), 2); 132 | } 133 | 134 | @Test 135 | public void testRemoveExpiredSessionInMemory() throws InterruptedException, SerializationException { 136 | redisSessionDAO.setSessionInMemoryTimeout(500L); 137 | redisSessionDAO.doCreate(session1); 138 | redisSessionDAO.doReadSession(session1.getId()); 139 | Thread.sleep(1000); 140 | redisSessionDAO.doCreate(session2); 141 | redisSessionDAO.doReadSession(session2.getId()); 142 | Map sessionMap = (Map) redisSessionDAO.getSessionsInThread().get(); 143 | assertEquals(sessionMap.size(), 1); 144 | } 145 | 146 | @Test 147 | public void testTurnOffSessionInMemoryEnabled() throws InterruptedException, SerializationException { 148 | redisSessionDAO.setSessionInMemoryTimeout(2000L); 149 | session1.setCompany("apple"); 150 | redisSessionDAO.doCreate(session1); 151 | // Load session into SessionInThread 152 | redisSessionDAO.doReadSession(session1.getId()); 153 | // Directly update session in Redis 154 | session1.setCompany("google"); 155 | RedisManager redisManager = scaffoldStandaloneRedisManager(); 156 | String sessionRedisKey = prefix + session1.getId(); 157 | redisManager.set(keySerializer.serialize(sessionRedisKey), valueSerializer.serialize(session1), 10); 158 | // Try to read session again 159 | Thread.sleep(500); 160 | FakeSession sessionFromThreadLocal = (FakeSession)redisSessionDAO.doReadSession(session1.getId()); 161 | // The company should be the old value 162 | assertThat(sessionFromThreadLocal.getCompany(), is("apple")); 163 | // Turn off sessionInMemoryEnabled 164 | redisSessionDAO.setSessionInMemoryEnabled(false); 165 | // Try to read session again. It should get the version in Redis 166 | FakeSession sessionFromRedis = (FakeSession)redisSessionDAO.doReadSession(session1.getId()); 167 | assertThat(sessionFromRedis.getCompany(), is("google")); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/integration-test/java/org/crazycake/shiro/integration/RedisCacheTest.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.integration; 2 | 3 | import com.github.javafaker.Faker; 4 | import org.apache.commons.lang3.math.NumberUtils; 5 | import org.apache.shiro.subject.PrincipalCollection; 6 | import org.apache.shiro.subject.SimplePrincipalCollection; 7 | import org.crazycake.shiro.RedisCache; 8 | import org.crazycake.shiro.RedisCacheManager; 9 | import org.crazycake.shiro.RedisManager; 10 | import org.crazycake.shiro.exception.CacheManagerPrincipalIdNotAssignedException; 11 | import org.crazycake.shiro.exception.PrincipalInstanceException; 12 | import org.crazycake.shiro.integration.fixture.model.FakeAuth; 13 | import org.crazycake.shiro.integration.fixture.model.UserInfo; 14 | import org.crazycake.shiro.serializer.ObjectSerializer; 15 | import org.crazycake.shiro.serializer.StringSerializer; 16 | import org.junit.jupiter.api.AfterEach; 17 | import org.junit.jupiter.api.Assertions; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.util.Properties; 22 | import java.util.Set; 23 | 24 | import static org.crazycake.shiro.integration.fixture.TestFixture.*; 25 | 26 | /** 27 | * input key, value (java) 28 | * output value (java) 29 | */ 30 | public class RedisCacheTest { 31 | 32 | private RedisCache redisCache; 33 | private RedisCache redisCacheWithPrincipalIdFieldName; 34 | private RedisCache redisCacheWithEmptyPrincipalIdFieldName; 35 | private RedisCache redisCacheWithStrings; 36 | 37 | private Properties properties = loadProperties("shiro-standalone.ini"); 38 | private PrincipalCollection user1; 39 | private PrincipalCollection user2; 40 | private PrincipalCollection user3; 41 | private PrincipalCollection user4; 42 | 43 | private Set users1_2_3; 44 | private String prefix; 45 | 46 | private void blast() { 47 | blastRedis(); 48 | } 49 | 50 | private void scaffold() { 51 | RedisManager redisManager = scaffoldStandaloneRedisManager(); 52 | prefix = scaffoldPrefix(); 53 | redisCache = scaffoldRedisCache(redisManager, new StringSerializer(), new ObjectSerializer(), prefix, NumberUtils.toInt(properties.getProperty("cacheManager.expire")), RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME); 54 | redisCacheWithPrincipalIdFieldName = scaffoldRedisCache(redisManager, new StringSerializer(), new ObjectSerializer(), prefix, NumberUtils.toInt(properties.getProperty("cacheManager.expire")), properties.getProperty("cacheManager.principalIdFieldName")); 55 | redisCacheWithEmptyPrincipalIdFieldName = scaffoldRedisCache(redisManager, new StringSerializer(), new ObjectSerializer(), prefix, NumberUtils.toInt(properties.getProperty("cacheManager.expire")), ""); 56 | redisCacheWithStrings = scaffoldRedisCache(redisManager, new StringSerializer(), new ObjectSerializer(), prefix, NumberUtils.toInt(properties.getProperty("cacheManager.expire")), properties.getProperty("cacheManager.principalIdFieldName")); 57 | user1 = scaffoldAuthKey(scaffoldUser()); 58 | user2 = scaffoldAuthKey(scaffoldUser()); 59 | user3 = scaffoldAuthKey(scaffoldUser()); 60 | user4 = new SimplePrincipalCollection(Faker.instance().gameOfThrones().character(), Faker.instance().gameOfThrones().city()); 61 | users1_2_3 = scaffoldKeys(user1, user2, user3); 62 | } 63 | 64 | 65 | @BeforeEach 66 | public void setUp() { 67 | blast(); 68 | scaffold(); 69 | } 70 | 71 | @AfterEach 72 | public void tearDown() { 73 | blast(); 74 | } 75 | 76 | 77 | @Test 78 | public void testInitialize() { 79 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 80 | new RedisCache(null, null, null, "abc:", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME); 81 | }); 82 | 83 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 84 | new RedisCache(new RedisManager(), null, null, "abc:", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME); 85 | }); 86 | 87 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 88 | new RedisCache(new RedisManager(), new StringSerializer(), null, "abc:", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME); 89 | }); 90 | 91 | RedisCache rc = new RedisCache(new RedisManager(), new StringSerializer(), new ObjectSerializer(), "abc", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME); 92 | assertEquals(rc.getKeyPrefix(), "abc"); 93 | } 94 | 95 | @Test 96 | public void testPutNull() { 97 | doPutAuth(redisCache, null); 98 | assertRedisEmpty(); 99 | } 100 | 101 | @Test 102 | public void testPut() { 103 | doPutAuth(redisCache, user1); 104 | FakeAuth fakeAuth = redisCache.get(user1); 105 | assertAuthEquals(fakeAuth, turnUserToFakeAuth((UserInfo)user1.getPrimaryPrincipal())); 106 | } 107 | 108 | @Test 109 | public void testPutString() { 110 | redisCacheWithStrings.put(user4, user4.getPrimaryPrincipal().toString()); 111 | String auth = redisCacheWithStrings.get(user4); 112 | assertEquals(auth, user4.getPrimaryPrincipal()); 113 | } 114 | 115 | @Test 116 | public void testSize() throws InterruptedException { 117 | doPutAuth(redisCache, user1); 118 | doPutAuth(redisCache, user2); 119 | assertEquals(redisCache.size(), 2); 120 | } 121 | 122 | @Test 123 | public void testPutInvalidPrincipal() { 124 | Assertions.assertThrows(PrincipalInstanceException.class, () -> { 125 | doPutAuth(redisCacheWithPrincipalIdFieldName, user3); 126 | }); 127 | } 128 | 129 | @Test 130 | public void testPutPrincipalWithEmptyIdFieldName() { 131 | Assertions.assertThrows(CacheManagerPrincipalIdNotAssignedException.class, () -> { 132 | doPutAuth(redisCacheWithEmptyPrincipalIdFieldName, user3); 133 | }); 134 | } 135 | 136 | @Test 137 | public void testRemove() { 138 | doPutAuth(redisCache, user1); 139 | doRemoveAuth(redisCache, user1); 140 | assertRedisEmpty(); 141 | } 142 | 143 | @Test 144 | public void testClear() { 145 | doClearAuth(redisCache); 146 | assertRedisEmpty(); 147 | } 148 | 149 | @Test 150 | public void testKeys() { 151 | doPutAuth(redisCache, user1); 152 | doPutAuth(redisCache, user2); 153 | doPutAuth(redisCache, user3); 154 | Set actualKeys = doKeysAuth(redisCache); 155 | assertKeysEquals(actualKeys, turnPrincipalCollectionToString(users1_2_3, prefix)); 156 | } 157 | 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/integration-test/java/org/crazycake/shiro/integration/RedisManagerTest.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.integration; 2 | 3 | import org.apache.shiro.subject.PrincipalCollection; 4 | import org.apache.shiro.subject.SimplePrincipalCollection; 5 | import org.crazycake.shiro.RedisManager; 6 | import org.crazycake.shiro.exception.SerializationException; 7 | import org.crazycake.shiro.integration.fixture.model.UserInfo; 8 | import org.crazycake.shiro.serializer.ObjectSerializer; 9 | import org.crazycake.shiro.serializer.StringSerializer; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.Set; 15 | 16 | import static org.crazycake.shiro.integration.fixture.TestFixture.*; 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.hamcrest.CoreMatchers.*; 19 | 20 | public class RedisManagerTest { 21 | private PrincipalCollection user1; 22 | private RedisManager redisManager; 23 | private StringSerializer keySerializer = new StringSerializer(); 24 | private ObjectSerializer valueSerializer = new ObjectSerializer(); 25 | 26 | private void scaffold() { 27 | redisManager = scaffoldStandaloneRedisManager(); 28 | user1 = scaffoldAuthKey(scaffoldUser()); 29 | } 30 | 31 | @BeforeEach 32 | public void setUp() { 33 | scaffold(); 34 | } 35 | 36 | @AfterEach 37 | public void tearDown() { 38 | blastRedis(); 39 | } 40 | 41 | @Test 42 | public void testSet() throws SerializationException, InterruptedException { 43 | UserInfo userInfo = (UserInfo)user1.getPrimaryPrincipal(); 44 | this.redisManager.set(this.keySerializer.serialize("user:" + userInfo.getId()), this.valueSerializer.serialize(user1), 1); 45 | assertThat(this.redisManager.get(this.keySerializer.serialize("user:" + userInfo.getId())), is(this.valueSerializer.serialize(user1))); 46 | Thread.sleep(1500); 47 | byte[] aaa = this.redisManager.get(this.keySerializer.serialize("user:" + userInfo.getId())); 48 | assertThat(valueSerializer.deserialize(this.redisManager.get(this.keySerializer.serialize("user:" + userInfo.getId()))), is(nullValue())); 49 | } 50 | 51 | @Test 52 | public void testDel() throws SerializationException { 53 | UserInfo userInfo = (UserInfo)user1.getPrimaryPrincipal(); 54 | this.redisManager.set(this.keySerializer.serialize("user:" + userInfo.getId()), this.valueSerializer.serialize(user1), 2); 55 | this.redisManager.del(this.keySerializer.serialize("user:" + userInfo.getId())); 56 | assertThat(this.redisManager.get(this.keySerializer.serialize("user:" + userInfo.getId())), is(nullValue())); 57 | } 58 | 59 | @Test 60 | public void testKeys() throws SerializationException { 61 | for (int i = 1; i < 121; i++) { 62 | UserInfo user = new UserInfo(); 63 | user.setId(i); 64 | SimplePrincipalCollection principal = new SimplePrincipalCollection(); 65 | principal.add(user, "student"); 66 | this.redisManager.set(this.keySerializer.serialize("user:" + user.getId()), this.valueSerializer.serialize(principal), 10); 67 | } 68 | 69 | Set keys = this.redisManager.keys(this.keySerializer.serialize("user:*")); 70 | assertThat(keys.size(), is(120)); 71 | } 72 | 73 | @Test 74 | public void testDbSize() throws SerializationException { 75 | for (int i = 1; i < 136; i++) { 76 | UserInfo user = new UserInfo(); 77 | user.setId(i); 78 | SimplePrincipalCollection principal = new SimplePrincipalCollection(); 79 | principal.add(user, "student"); 80 | this.redisManager.set(this.keySerializer.serialize("user:" + user.getId()), this.valueSerializer.serialize(principal), 10); 81 | } 82 | 83 | Long dbSize = this.redisManager.dbSize(this.keySerializer.serialize("user:*")); 84 | assertThat(dbSize, is(135L)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/integration-test/java/org/crazycake/shiro/integration/fixture/TestFixture.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.integration.fixture; 2 | 3 | import com.github.javafaker.Faker; 4 | import org.apache.commons.lang3.math.NumberUtils; 5 | import org.apache.shiro.session.Session; 6 | import org.apache.shiro.subject.PrincipalCollection; 7 | import org.apache.shiro.subject.SimplePrincipalCollection; 8 | import org.crazycake.shiro.RedisCache; 9 | import org.crazycake.shiro.RedisManager; 10 | import org.crazycake.shiro.RedisSessionDAO; 11 | import org.crazycake.shiro.exception.SerializationException; 12 | import org.crazycake.shiro.integration.fixture.model.FakeAuth; 13 | import org.crazycake.shiro.integration.fixture.model.FakeSession; 14 | import org.crazycake.shiro.integration.fixture.model.UserInfo; 15 | import org.crazycake.shiro.serializer.RedisSerializer; 16 | import redis.clients.jedis.Jedis; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.util.HashSet; 21 | import java.util.Properties; 22 | import java.util.Set; 23 | 24 | import static org.hamcrest.CoreMatchers.is; 25 | import static org.hamcrest.CoreMatchers.notNullValue; 26 | import static org.hamcrest.MatcherAssert.assertThat; 27 | import static org.hamcrest.core.StringContains.containsString; 28 | 29 | public class TestFixture { 30 | 31 | private static Properties properties = loadProperties("shiro-standalone.ini"); 32 | private static Faker faker = new Faker(); 33 | 34 | // /$$ /$$ /$$ 35 | // | $$ | $$ | $$ 36 | // | $$$$$$$ | $$ /$$$$$$ /$$$$$$$ /$$$$$$ 37 | // | $$__ $$| $$ |____ $$ /$$_____/|_ $$_/ 38 | // | $$ \ $$| $$ /$$$$$$$| $$$$$$ | $$ 39 | // | $$ | $$| $$ /$$__ $$ \____ $$ | $$ /$$ 40 | // | $$$$$$$/| $$| $$$$$$$ /$$$$$$$/ | $$$$/ 41 | // |_______/ |__/ \_______/|_______/ \___/ 42 | 43 | 44 | public static void blastRedis() { 45 | Jedis jedis = doGetRedisInstance(); 46 | jedis.flushAll(); 47 | jedis.close(); 48 | } 49 | 50 | // /$$$$$$ /$$$$$$ /$$ /$$ 51 | // /$$__ $$ /$$__ $$ | $$ | $$ 52 | // /$$$$$$$ /$$$$$$$ /$$$$$$ | $$ \__/| $$ \__//$$$$$$ | $$ /$$$$$$$ 53 | // /$$_____/ /$$_____/ |____ $$| $$$$ | $$$$ /$$__ $$| $$ /$$__ $$ 54 | //| $$$$$$ | $$ /$$$$$$$| $$_/ | $$_/ | $$ \ $$| $$| $$ | $$ 55 | // \____ $$| $$ /$$__ $$| $$ | $$ | $$ | $$| $$| $$ | $$ 56 | // /$$$$$$$/| $$$$$$$| $$$$$$$| $$ | $$ | $$$$$$/| $$| $$$$$$$ 57 | //|_______/ \_______/ \_______/|__/ |__/ \______/ |__/ \_______/ 58 | 59 | public static RedisCache scaffoldRedisCache(RedisManager redisManager, RedisSerializer keySerializer, RedisSerializer valueSerializer, String prefix, int expire, String principalIdFieldName) { 60 | return new RedisCache(redisManager, keySerializer, valueSerializer, prefix, expire, principalIdFieldName); 61 | } 62 | 63 | public static RedisSessionDAO scaffoldRedisSessionDAO(RedisManager redisManager, String prefix) { 64 | RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); 65 | redisSessionDAO.setRedisManager(redisManager); 66 | redisSessionDAO.setKeyPrefix(prefix); 67 | redisSessionDAO.setExpire(NumberUtils.toInt(properties.getProperty("redisSessionDAO.expire"))); 68 | return redisSessionDAO; 69 | } 70 | 71 | public static String scaffoldPrefix() { 72 | return faker.university().name().replace(" ", "_") + ":"; 73 | } 74 | 75 | public static RedisManager scaffoldStandaloneRedisManager() { 76 | RedisManager redisManager = new RedisManager(); 77 | redisManager.setHost(properties.getProperty("redisManager.host")); 78 | return redisManager; 79 | } 80 | 81 | public static UserInfo scaffoldUser() { 82 | UserInfo user = new UserInfo(); 83 | user.setId(faker.number().randomDigitNotZero()); 84 | user.setUsername(faker.name().username()); 85 | user.setAge(faker.number().numberBetween(18, 60)); 86 | user.setRole(faker.number().randomDigitNotZero()); 87 | return user; 88 | } 89 | 90 | public static FakeSession scaffoldSession() { 91 | return new FakeSession(faker.number().randomDigitNotZero(), faker.name().username()); 92 | } 93 | 94 | public static FakeSession scaffoldEmptySession() { 95 | return new FakeSession(); 96 | } 97 | 98 | public static String scaffoldUsername() { 99 | return faker.name().username(); 100 | } 101 | 102 | public static PrincipalCollection scaffoldAuthKey(UserInfo user) { 103 | SimplePrincipalCollection key = new SimplePrincipalCollection(); 104 | key.add(user, faker.beer().name()); 105 | return key; 106 | } 107 | 108 | public static Set scaffoldKeys(Object... users) { 109 | Set keys = new HashSet(); 110 | for (Object user : users) { 111 | keys.add(user); 112 | } 113 | return keys; 114 | } 115 | 116 | // /$$ /$$ 117 | // | $$ |__/ 118 | // /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$$ 119 | // |____ $$ /$$_____/|_ $$_/ | $$ /$$__ $$| $$__ $$ /$$_____/ 120 | // /$$$$$$$| $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$ 121 | // /$$__ $$| $$ | $$ /$$| $$| $$ | $$| $$ | $$ \____ $$ 122 | // | $$$$$$$| $$$$$$$ | $$$$/| $$| $$$$$$/| $$ | $$ /$$$$$$$/ 123 | // \_______/ \_______/ \___/ |__/ \______/ |__/ |__/|_______/ 124 | 125 | public static void doPutAuth(RedisCache redisCache, PrincipalCollection user) { 126 | if (user == null) { 127 | redisCache.put(null, null); 128 | return; 129 | } 130 | redisCache.put(user, turnUserToFakeAuth((UserInfo)user.getPrimaryPrincipal())); 131 | } 132 | 133 | public static void doRemoveAuth(RedisCache redisCache, PrincipalCollection user) { 134 | redisCache.remove(user); 135 | } 136 | 137 | public static void doClearAuth(RedisCache redisCache) { 138 | redisCache.clear(); 139 | } 140 | 141 | public static Set doKeysAuth(RedisCache redisCache) { 142 | return redisCache.keys(); 143 | } 144 | 145 | public static void doSetSessionDAOExpire(RedisSessionDAO redisSessionDAO, int expire) { 146 | redisSessionDAO.setExpire(expire); 147 | } 148 | 149 | public static void doChangeSessionName(FakeSession session, String name) { 150 | session.setName(name); 151 | } 152 | 153 | private static Jedis doGetRedisInstance() { 154 | return new Jedis(properties.getProperty("redisManager.host").split(":")[0]); 155 | } 156 | 157 | // /$$ 158 | // | $$ 159 | // /$$$$$$ /$$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ 160 | // |____ $$ /$$_____//$$_____/ /$$__ $$ /$$__ $$|_ $$_/ 161 | // /$$$$$$$| $$$$$$| $$$$$$ | $$$$$$$$| $$ \__/ | $$ 162 | // /$$__ $$ \____ $$\____ $$| $$_____/| $$ | $$ /$$ 163 | // | $$$$$$$ /$$$$$$$//$$$$$$$/| $$$$$$$| $$ | $$$$/ 164 | // \_______/|_______/|_______/ \_______/|__/ \___/ 165 | 166 | public static void assertRedisEmpty() { 167 | Jedis jedis = doGetRedisInstance(); 168 | assertThat("Redis should be empty",jedis.dbSize(), is(0L)); 169 | } 170 | 171 | public static void assertKeysEquals(Set actualKeys, Set expectKeys) { 172 | assertEquals(expectKeys, actualKeys); 173 | } 174 | 175 | public static void assertAuthEquals(FakeAuth actualAuth, FakeAuth expectAuth) { 176 | assertThat(actualAuth.getId(), is(expectAuth.getId())); 177 | assertThat(actualAuth.getRole(), is(expectAuth.getRole())); 178 | } 179 | 180 | public static void assertPrincipalInstanceException(Exception e) { 181 | assertThat(e, is(notNullValue())); 182 | assertThat(e.getMessage(), containsString("must has getter for field: " + properties.getProperty("cacheManager.principalIdFieldName"))); 183 | } 184 | 185 | public static void assertEquals(Object actual, Object expect) { 186 | assertThat(actual,is(expect)); 187 | } 188 | 189 | public static void assertSessionEquals(Session actualSession, Session expectSession) { 190 | assertThat(actualSession.getId(), is(expectSession.getId())); 191 | assertThat(((FakeSession)actualSession).getName(), is(((FakeSession)expectSession).getName())); 192 | } 193 | 194 | // /$$ /$$$$$$ 195 | // | $$ /$$__ $$ 196 | // /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$$| $$ \__//$$$$$$ /$$$$$$ 197 | // |_ $$_/ /$$__ $$|____ $$| $$__ $$ /$$_____/| $$$$ /$$__ $$ /$$__ $$ 198 | // | $$ | $$ \__/ /$$$$$$$| $$ \ $$| $$$$$$ | $$_/ | $$$$$$$$| $$ \__/ 199 | // | $$ /$$| $$ /$$__ $$| $$ | $$ \____ $$| $$ | $$_____/| $$ 200 | // | $$$$/| $$ | $$$$$$$| $$ | $$ /$$$$$$$/| $$ | $$$$$$$| $$ 201 | // \___/ |__/ \_______/|__/ |__/|_______/ |__/ \_______/|__/ 202 | // 203 | 204 | public static Set turnPrincipalCollectionToString(Set users, String prefix) { 205 | Set keys = new HashSet(); 206 | for (PrincipalCollection user : users) { 207 | keys.add(prefix + ((UserInfo)user.getPrimaryPrincipal()).getId()); 208 | } 209 | return keys; 210 | } 211 | 212 | public static FakeAuth turnUserToFakeAuth(UserInfo user) { 213 | FakeAuth auth = new FakeAuth(); 214 | auth.setId(user.getId()); 215 | auth.setRole(user.getRole()); 216 | return auth; 217 | } 218 | 219 | // /$$ /$$ 220 | // | $$ | $$ 221 | // /$$$$$$ /$$$$$$ /$$$$$$ | $$ /$$$$$$$ 222 | // |_ $$_/ /$$__ $$ /$$__ $$| $$ /$$_____/ 223 | // | $$ | $$ \ $$| $$ \ $$| $$| $$$$$$ 224 | // | $$ /$$| $$ | $$| $$ | $$| $$ \____ $$ 225 | // | $$$$/| $$$$$$/| $$$$$$/| $$ /$$$$$$$/ 226 | // \___/ \______/ \______/ |__/|_______/ 227 | 228 | public static Properties loadProperties(String propFileName) { 229 | 230 | Properties props = new Properties(); 231 | InputStream inputStream = TestFixture.class.getClassLoader() 232 | .getResourceAsStream(propFileName); 233 | 234 | if (inputStream != null) { 235 | try { 236 | props.load(inputStream); 237 | } catch (IOException e) { 238 | e.printStackTrace(); 239 | } 240 | } 241 | 242 | return props; 243 | } 244 | 245 | public static Long getRedisTTL(String key, RedisSerializer keySerializer) { 246 | Jedis jedis = doGetRedisInstance(); 247 | Long ttl = 0L; 248 | try { 249 | ttl = jedis.ttl(keySerializer.serialize(key)); 250 | } catch (SerializationException e) { 251 | e.printStackTrace(); 252 | } 253 | jedis.close(); 254 | return ttl; 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /src/integration-test/java/org/crazycake/shiro/integration/fixture/model/FakeAuth.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.integration.fixture.model; 2 | 3 | import java.io.Serializable; 4 | 5 | public class FakeAuth implements Serializable{ 6 | private Integer id; 7 | private Integer role; 8 | 9 | public FakeAuth() {} 10 | 11 | public FakeAuth(Integer id, Integer role) { 12 | this.id = id; 13 | this.role = role; 14 | } 15 | 16 | public Integer getId() { 17 | return id; 18 | } 19 | 20 | public void setId(Integer id) { 21 | this.id = id; 22 | } 23 | 24 | public Integer getRole() { 25 | return role; 26 | } 27 | 28 | public void setRole(Integer role) { 29 | this.role = role; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/integration-test/java/org/crazycake/shiro/integration/fixture/model/FakeSession.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.integration.fixture.model; 2 | 3 | import org.apache.shiro.session.InvalidSessionException; 4 | import org.apache.shiro.session.Session; 5 | import org.apache.shiro.session.mgt.SimpleSession; 6 | 7 | import java.io.Serializable; 8 | import java.util.Collection; 9 | import java.util.Date; 10 | 11 | public class FakeSession extends SimpleSession implements Serializable, Session{ 12 | private Integer id; 13 | private String name; 14 | private String company; 15 | 16 | public FakeSession() {} 17 | 18 | public FakeSession(Integer id, String name) { 19 | this.id = id; 20 | this.name = name; 21 | } 22 | 23 | public Integer getId() { 24 | return id; 25 | } 26 | 27 | public void setId(Integer id) { 28 | this.id = id; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public void setName(String name) { 36 | this.name = name; 37 | } 38 | 39 | public String getCompany() { 40 | return company; 41 | } 42 | 43 | public void setCompany(String company) { 44 | this.company = company; 45 | } 46 | 47 | @Override 48 | public Date getStartTimestamp() { 49 | return null; 50 | } 51 | 52 | @Override 53 | public Date getLastAccessTime() { 54 | return null; 55 | } 56 | 57 | @Override 58 | public void setTimeout(long l) throws InvalidSessionException { 59 | 60 | } 61 | 62 | @Override 63 | public String getHost() { 64 | return null; 65 | } 66 | 67 | @Override 68 | public void touch() throws InvalidSessionException { 69 | 70 | } 71 | 72 | @Override 73 | public void stop() throws InvalidSessionException { 74 | 75 | } 76 | 77 | @Override 78 | public Collection getAttributeKeys() throws InvalidSessionException { 79 | return null; 80 | } 81 | 82 | @Override 83 | public Object getAttribute(Object o) throws InvalidSessionException { 84 | return null; 85 | } 86 | 87 | @Override 88 | public void setAttribute(Object o, Object o1) throws InvalidSessionException { 89 | 90 | } 91 | 92 | @Override 93 | public Object removeAttribute(Object o) throws InvalidSessionException { 94 | return null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/integration-test/java/org/crazycake/shiro/integration/fixture/model/UserInfo.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.integration.fixture.model; 2 | 3 | import java.io.Serializable; 4 | 5 | public class UserInfo implements Serializable { 6 | 7 | private Integer id; 8 | 9 | private String username; 10 | 11 | private Integer age; 12 | 13 | private String city; 14 | 15 | private Integer role; 16 | 17 | public Integer getId() { 18 | return id; 19 | } 20 | 21 | public void setId(Integer id) { 22 | this.id = id; 23 | } 24 | 25 | public String getCity() { 26 | return city; 27 | } 28 | 29 | public void setCity(String city) { 30 | this.city = city; 31 | } 32 | 33 | public String getUsername() { 34 | return username; 35 | } 36 | 37 | public void setUsername(String username) { 38 | this.username = username; 39 | } 40 | 41 | public Integer getAge() { 42 | return age; 43 | } 44 | 45 | public void setAge(Integer age) { 46 | this.age = age; 47 | } 48 | 49 | public Integer getRole() { 50 | return role; 51 | } 52 | 53 | public void setRole(Integer role) { 54 | this.role = role; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/integration-test/java/shiro-standalone.ini: -------------------------------------------------------------------------------- 1 | redisManager.host = 127.0.0.1:6379 2 | redisSessionDAO.expire = 3000 3 | cacheManager.expire = 3000 4 | cacheManager.principalIdFieldName = userId -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/IRedisManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * redisManager interface 7 | * 8 | **/ 9 | 10 | public interface IRedisManager { 11 | 12 | /** 13 | * get value from redis 14 | * @param key key 15 | * @return value 16 | */ 17 | byte[] get(byte[] key); 18 | 19 | /** 20 | * set value 21 | * @param key key 22 | * @param value value 23 | * @param expire expire 24 | * @return value 25 | */ 26 | byte[] set(byte[] key, byte[] value, int expire); 27 | 28 | /** 29 | * del 30 | * @param key key 31 | */ 32 | void del(byte[] key); 33 | 34 | /** 35 | * dbsize 36 | * @param pattern pattern 37 | * @return key-value size 38 | */ 39 | Long dbSize(byte[] pattern); 40 | 41 | /** 42 | * keys 43 | * @param pattern key pattern 44 | * @return key set 45 | */ 46 | Set keys(byte[] pattern); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/LettuceRedisClusterManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import io.lettuce.core.*; 4 | import io.lettuce.core.cluster.ClusterClientOptions; 5 | import io.lettuce.core.cluster.RedisClusterClient; 6 | import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; 7 | import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; 8 | import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; 9 | import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; 10 | import io.lettuce.core.cluster.api.sync.RedisClusterCommands; 11 | import io.lettuce.core.cluster.models.partitions.ClusterPartitionParser; 12 | import io.lettuce.core.cluster.models.partitions.Partitions; 13 | import io.lettuce.core.codec.ByteArrayCodec; 14 | import io.lettuce.core.support.ConnectionPoolSupport; 15 | import org.apache.commons.pool2.impl.GenericObjectPool; 16 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig; 17 | import org.crazycake.shiro.exception.PoolException; 18 | 19 | import java.time.Duration; 20 | import java.util.HashSet; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.Set; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.concurrent.atomic.AtomicLong; 26 | import java.util.stream.Collectors; 27 | 28 | /** 29 | * @author Teamo 30 | * @since 2022/05/19 31 | */ 32 | public class LettuceRedisClusterManager implements IRedisManager { 33 | 34 | /** 35 | * Comma-separated list of "host:port" pairs to bootstrap from. This represents an 36 | * "initial" list of cluster nodes and is required to have at least one entry. 37 | */ 38 | private List nodes; 39 | 40 | /** 41 | * Default value of count. 42 | */ 43 | private static final int DEFAULT_COUNT = 100; 44 | 45 | /** 46 | * timeout for RedisClient try to connect to redis server, not expire time! unit seconds. 47 | */ 48 | private Duration timeout = RedisURI.DEFAULT_TIMEOUT_DURATION; 49 | 50 | /** 51 | * Redis database. 52 | */ 53 | private int database = 0; 54 | 55 | /** 56 | * Redis password. 57 | */ 58 | private String password; 59 | 60 | /** 61 | * Whether to enable async. 62 | */ 63 | private boolean isAsync = true; 64 | 65 | /** 66 | * The number of elements returned at every iteration. 67 | */ 68 | private int count = DEFAULT_COUNT; 69 | 70 | /** 71 | * genericObjectPoolConfig used to initialize GenericObjectPoolConfig object. 72 | */ 73 | private GenericObjectPoolConfig> genericObjectPoolConfig = new GenericObjectPoolConfig<>(); 74 | 75 | /** 76 | * GenericObjectPool. 77 | */ 78 | private volatile GenericObjectPool> genericObjectPool; 79 | 80 | /** 81 | * ClusterClientOptions used to initialize RedisClient. 82 | */ 83 | private ClusterClientOptions clusterClientOptions = ClusterClientOptions.create(); 84 | 85 | private void initialize() { 86 | if (genericObjectPool == null) { 87 | synchronized (LettuceRedisClusterManager.class) { 88 | if (genericObjectPool == null) { 89 | RedisClusterClient redisClusterClient = RedisClusterClient.create(getClusterRedisURI()); 90 | redisClusterClient.setOptions(clusterClientOptions); 91 | StatefulRedisClusterConnection connect = redisClusterClient.connect(new ByteArrayCodec()); 92 | genericObjectPool = ConnectionPoolSupport.createGenericObjectPool(() -> connect, genericObjectPoolConfig); 93 | } 94 | } 95 | } 96 | } 97 | 98 | private StatefulRedisClusterConnection getStatefulConnection() { 99 | if (genericObjectPool == null) { 100 | initialize(); 101 | } 102 | try { 103 | return genericObjectPool.borrowObject(); 104 | } catch (Exception e) { 105 | throw new PoolException("Could not get a resource from the pool", e); 106 | } 107 | } 108 | 109 | private List getClusterRedisURI() { 110 | Objects.requireNonNull(nodes, "nodes must not be null!"); 111 | return nodes.stream().map(node -> { 112 | String[] hostAndPort = node.split(":"); 113 | RedisURI.Builder builder = RedisURI.builder() 114 | .withHost(hostAndPort[0]) 115 | .withPort(Integer.parseInt(hostAndPort[1])) 116 | .withDatabase(database) 117 | .withTimeout(timeout); 118 | if (password != null) { 119 | builder.withPassword(password.toCharArray()); 120 | } 121 | return builder.build(); 122 | }).collect(Collectors.toList()); 123 | } 124 | 125 | @Override 126 | public byte[] get(byte[] key) { 127 | if (key == null) { 128 | return null; 129 | } 130 | byte[] value = null; 131 | try (StatefulRedisClusterConnection connection = getStatefulConnection()) { 132 | if (isAsync) { 133 | RedisAdvancedClusterAsyncCommands async = connection.async(); 134 | value = LettuceFutures.awaitOrCancel(async.get(key), timeout.getSeconds(), TimeUnit.SECONDS); 135 | } else { 136 | RedisAdvancedClusterCommands sync = connection.sync(); 137 | value = sync.get(key); 138 | } 139 | } 140 | return value; 141 | } 142 | 143 | @Override 144 | public byte[] set(byte[] key, byte[] value, int expire) { 145 | if (key == null) { 146 | return null; 147 | } 148 | try (StatefulRedisClusterConnection connection = getStatefulConnection()) { 149 | if (isAsync) { 150 | RedisAdvancedClusterAsyncCommands async = connection.async(); 151 | if (expire > 0) { 152 | async.set(key, value, SetArgs.Builder.ex(expire)); 153 | } else { 154 | async.set(key, value); 155 | } 156 | } else { 157 | RedisAdvancedClusterCommands sync = connection.sync(); 158 | if (expire > 0) { 159 | sync.set(key, value, SetArgs.Builder.ex(expire)); 160 | } else { 161 | sync.set(key, value); 162 | } 163 | } 164 | } 165 | return value; 166 | } 167 | 168 | @Override 169 | public void del(byte[] key) { 170 | try (StatefulRedisClusterConnection connection = getStatefulConnection()) { 171 | if (isAsync) { 172 | RedisAdvancedClusterAsyncCommands async = connection.async(); 173 | async.del(key); 174 | } else { 175 | RedisAdvancedClusterCommands sync = connection.sync(); 176 | sync.del(key); 177 | } 178 | } 179 | } 180 | 181 | @Override 182 | public Long dbSize(byte[] pattern) { 183 | AtomicLong dbSize = new AtomicLong(0L); 184 | 185 | try (StatefulRedisClusterConnection connection = getStatefulConnection()) { 186 | if (isAsync) { 187 | RedisAdvancedClusterAsyncCommands async = connection.async(); 188 | Partitions parse = ClusterPartitionParser.parse(LettuceFutures.awaitOrCancel(async.clusterNodes(), timeout.getSeconds(), TimeUnit.SECONDS)); 189 | 190 | parse.forEach(redisClusterNode -> { 191 | RedisClusterAsyncCommands clusterAsyncCommands = async.getConnection(redisClusterNode.getNodeId()); 192 | 193 | KeyScanCursor scanCursor = new KeyScanCursor<>(); 194 | scanCursor.setCursor(ScanCursor.INITIAL.getCursor()); 195 | ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(count); 196 | while (!scanCursor.isFinished()) { 197 | scanCursor = LettuceFutures.awaitOrCancel(clusterAsyncCommands.scan(scanCursor, scanArgs), timeout.getSeconds(), TimeUnit.SECONDS); 198 | dbSize.addAndGet(scanCursor.getKeys().size()); 199 | } 200 | }); 201 | } else { 202 | RedisAdvancedClusterCommands sync = connection.sync(); 203 | Partitions parse = ClusterPartitionParser.parse(sync.clusterNodes()); 204 | 205 | parse.forEach(redisClusterNode -> { 206 | RedisClusterCommands clusterCommands = sync.getConnection(redisClusterNode.getNodeId()); 207 | 208 | KeyScanCursor scanCursor = new KeyScanCursor<>(); 209 | scanCursor.setCursor(ScanCursor.INITIAL.getCursor()); 210 | ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(count); 211 | while (!scanCursor.isFinished()) { 212 | scanCursor = clusterCommands.scan(scanCursor, scanArgs); 213 | dbSize.addAndGet(scanCursor.getKeys().size()); 214 | } 215 | }); 216 | } 217 | } 218 | return dbSize.get(); 219 | } 220 | 221 | @Override 222 | public Set keys(byte[] pattern) { 223 | Set keys = new HashSet<>(); 224 | 225 | try (StatefulRedisClusterConnection connection = getStatefulConnection()) { 226 | if (isAsync) { 227 | RedisAdvancedClusterAsyncCommands async = connection.async(); 228 | Partitions parse = ClusterPartitionParser.parse(LettuceFutures.awaitOrCancel(async.clusterNodes(), timeout.getSeconds(), TimeUnit.SECONDS)); 229 | 230 | parse.forEach(redisClusterNode -> { 231 | RedisClusterAsyncCommands clusterAsyncCommands = async.getConnection(redisClusterNode.getNodeId()); 232 | 233 | KeyScanCursor scanCursor = new KeyScanCursor<>(); 234 | scanCursor.setCursor(ScanCursor.INITIAL.getCursor()); 235 | ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(count); 236 | while (!scanCursor.isFinished()) { 237 | scanCursor = LettuceFutures.awaitOrCancel(clusterAsyncCommands.scan(scanCursor, scanArgs), timeout.getSeconds(), TimeUnit.SECONDS); 238 | keys.addAll(scanCursor.getKeys()); 239 | } 240 | }); 241 | } else { 242 | RedisAdvancedClusterCommands sync = connection.sync(); 243 | Partitions parse = ClusterPartitionParser.parse(sync.clusterNodes()); 244 | 245 | parse.forEach(redisClusterNode -> { 246 | RedisClusterCommands clusterCommands = sync.getConnection(redisClusterNode.getNodeId()); 247 | 248 | KeyScanCursor scanCursor = new KeyScanCursor<>(); 249 | scanCursor.setCursor(ScanCursor.INITIAL.getCursor()); 250 | ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(count); 251 | while (!scanCursor.isFinished()) { 252 | scanCursor = clusterCommands.scan(scanCursor, scanArgs); 253 | keys.addAll(scanCursor.getKeys()); 254 | } 255 | }); 256 | } 257 | } 258 | return keys; 259 | } 260 | 261 | public List getNodes() { 262 | return nodes; 263 | } 264 | 265 | public void setNodes(List nodes) { 266 | this.nodes = nodes; 267 | } 268 | 269 | public ClusterClientOptions getClusterClientOptions() { 270 | return clusterClientOptions; 271 | } 272 | 273 | public void setClusterClientOptions(ClusterClientOptions clusterClientOptions) { 274 | this.clusterClientOptions = clusterClientOptions; 275 | } 276 | 277 | public Duration getTimeout() { 278 | return timeout; 279 | } 280 | 281 | public void setTimeout(Duration timeout) { 282 | this.timeout = timeout; 283 | } 284 | 285 | public int getDatabase() { 286 | return database; 287 | } 288 | 289 | public void setDatabase(int database) { 290 | this.database = database; 291 | } 292 | 293 | public String getPassword() { 294 | return password; 295 | } 296 | 297 | public void setPassword(String password) { 298 | this.password = password; 299 | } 300 | 301 | public boolean isAsync() { 302 | return isAsync; 303 | } 304 | 305 | public void setIsAsync(boolean isAsync) { 306 | this.isAsync = isAsync; 307 | } 308 | 309 | public int getCount() { 310 | return count; 311 | } 312 | 313 | public void setCount(int count) { 314 | this.count = count; 315 | } 316 | 317 | public GenericObjectPoolConfig> getGenericObjectPoolConfig() { 318 | return genericObjectPoolConfig; 319 | } 320 | 321 | public void setGenericObjectPoolConfig(GenericObjectPoolConfig> genericObjectPoolConfig) { 322 | this.genericObjectPoolConfig = genericObjectPoolConfig; 323 | } 324 | 325 | public GenericObjectPool> getGenericObjectPool() { 326 | return genericObjectPool; 327 | } 328 | 329 | public void setGenericObjectPool(GenericObjectPool> genericObjectPool) { 330 | this.genericObjectPool = genericObjectPool; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/LettuceRedisManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import io.lettuce.core.RedisClient; 4 | import io.lettuce.core.RedisURI; 5 | import io.lettuce.core.api.StatefulRedisConnection; 6 | import io.lettuce.core.codec.ByteArrayCodec; 7 | import io.lettuce.core.support.ConnectionPoolSupport; 8 | import org.apache.commons.pool2.impl.GenericObjectPool; 9 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig; 10 | import org.crazycake.shiro.common.AbstractLettuceRedisManager; 11 | import org.crazycake.shiro.exception.PoolException; 12 | 13 | /** 14 | * Singleton lettuce redis 15 | * 16 | * @author Teamo 17 | * @since 2022/05/18 18 | */ 19 | public class LettuceRedisManager extends AbstractLettuceRedisManager { 20 | 21 | /** 22 | * Redis server host. 23 | */ 24 | private String host = "localhost"; 25 | 26 | /** 27 | * Redis server port. 28 | */ 29 | private int port = RedisURI.DEFAULT_REDIS_PORT; 30 | 31 | /** 32 | * GenericObjectPool. 33 | */ 34 | private volatile GenericObjectPool> genericObjectPool; 35 | 36 | @SuppressWarnings({"unchecked", "rawtypes"}) 37 | private void initialize() { 38 | if (genericObjectPool == null) { 39 | synchronized (LettuceRedisManager.class) { 40 | if (genericObjectPool == null) { 41 | RedisClient redisClient = RedisClient.create(createRedisURI()); 42 | redisClient.setOptions(getClientOptions()); 43 | GenericObjectPoolConfig genericObjectPoolConfig = getGenericObjectPoolConfig(); 44 | genericObjectPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connect(new ByteArrayCodec()), genericObjectPoolConfig); 45 | } 46 | } 47 | } 48 | } 49 | 50 | private RedisURI createRedisURI() { 51 | RedisURI.Builder builder = RedisURI.builder() 52 | .withHost(getHost()) 53 | .withPort(getPort()) 54 | .withDatabase(getDatabase()) 55 | .withTimeout(getTimeout()); 56 | String password = getPassword(); 57 | if (password != null) { 58 | builder.withPassword(password.toCharArray()); 59 | } 60 | return builder.build(); 61 | } 62 | 63 | @Override 64 | protected StatefulRedisConnection getStatefulConnection() { 65 | if (genericObjectPool == null) { 66 | initialize(); 67 | } 68 | try { 69 | return genericObjectPool.borrowObject(); 70 | } catch (Exception e) { 71 | throw new PoolException("Could not get a resource from the pool", e); 72 | } 73 | } 74 | 75 | public String getHost() { 76 | return host; 77 | } 78 | 79 | public void setHost(String host) { 80 | this.host = host; 81 | } 82 | 83 | public int getPort() { 84 | return port; 85 | } 86 | 87 | public void setPort(int port) { 88 | this.port = port; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/LettuceRedisSentinelManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import io.lettuce.core.ReadFrom; 4 | import io.lettuce.core.RedisClient; 5 | import io.lettuce.core.RedisURI; 6 | import io.lettuce.core.codec.ByteArrayCodec; 7 | import io.lettuce.core.masterreplica.MasterReplica; 8 | import io.lettuce.core.masterreplica.StatefulRedisMasterReplicaConnection; 9 | import io.lettuce.core.support.ConnectionPoolSupport; 10 | import org.apache.commons.pool2.impl.GenericObjectPool; 11 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig; 12 | import org.crazycake.shiro.common.AbstractLettuceRedisManager; 13 | import org.crazycake.shiro.exception.PoolException; 14 | 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | /** 19 | * @author Teamo 20 | * @since 2022/05/19 21 | */ 22 | public class LettuceRedisSentinelManager extends AbstractLettuceRedisManager { 23 | private static final String DEFAULT_MASTER_NAME = "mymaster"; 24 | 25 | private String masterName = DEFAULT_MASTER_NAME; 26 | 27 | private List nodes; 28 | 29 | private String sentinelPassword; 30 | 31 | private ReadFrom readFrom = ReadFrom.REPLICA_PREFERRED; 32 | 33 | /** 34 | * GenericObjectPool. 35 | */ 36 | private volatile GenericObjectPool> genericObjectPool; 37 | 38 | @SuppressWarnings({"unchecked", "rawtypes"}) 39 | private void initialize() { 40 | if (genericObjectPool == null) { 41 | synchronized (LettuceRedisSentinelManager.class) { 42 | if (genericObjectPool == null) { 43 | RedisURI redisURI = this.createSentinelRedisURI(); 44 | RedisClient redisClient = RedisClient.create(redisURI); 45 | redisClient.setOptions(getClientOptions()); 46 | StatefulRedisMasterReplicaConnection connect = MasterReplica.connect(redisClient, new ByteArrayCodec(), redisURI); 47 | connect.setReadFrom(readFrom); 48 | GenericObjectPoolConfig genericObjectPoolConfig = getGenericObjectPoolConfig(); 49 | genericObjectPool = ConnectionPoolSupport.createGenericObjectPool(() -> connect, genericObjectPoolConfig); 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Override 56 | protected StatefulRedisMasterReplicaConnection getStatefulConnection() { 57 | if (genericObjectPool == null) { 58 | initialize(); 59 | } 60 | try { 61 | return genericObjectPool.borrowObject(); 62 | } catch (Exception e) { 63 | throw new PoolException("Could not get a resource from the pool", e); 64 | } 65 | } 66 | 67 | private RedisURI createSentinelRedisURI() { 68 | Objects.requireNonNull(nodes, "nodes must not be null!"); 69 | 70 | RedisURI.Builder builder = RedisURI.builder(); 71 | for (String node : nodes) { 72 | String[] hostAndPort = node.split(":"); 73 | 74 | RedisURI.Builder sentinelBuilder = RedisURI.Builder.redis(hostAndPort[0], Integer.parseInt(hostAndPort[1])); 75 | 76 | if (sentinelPassword != null) { 77 | sentinelBuilder.withPassword(sentinelPassword.toCharArray()); 78 | } 79 | 80 | builder.withSentinel(sentinelBuilder.build()); 81 | } 82 | 83 | String password = getPassword(); 84 | if (password != null) { 85 | builder.withPassword(password.toCharArray()); 86 | } 87 | return builder.withSentinelMasterId(masterName).withDatabase(getDatabase()).build(); 88 | } 89 | 90 | public String getMasterName() { 91 | return masterName; 92 | } 93 | 94 | public void setMasterName(String masterName) { 95 | this.masterName = masterName; 96 | } 97 | 98 | public List getNodes() { 99 | return nodes; 100 | } 101 | 102 | public void setNodes(List nodes) { 103 | this.nodes = nodes; 104 | } 105 | 106 | public String getSentinelPassword() { 107 | return sentinelPassword; 108 | } 109 | 110 | public void setSentinelPassword(String sentinelPassword) { 111 | this.sentinelPassword = sentinelPassword; 112 | } 113 | 114 | public ReadFrom getReadFrom() { 115 | return readFrom; 116 | } 117 | 118 | public void setReadFrom(ReadFrom readFrom) { 119 | this.readFrom = readFrom; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/RedisCache.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.apache.shiro.cache.Cache; 4 | import org.apache.shiro.cache.CacheException; 5 | import org.apache.shiro.subject.PrincipalCollection; 6 | import org.apache.shiro.util.CollectionUtils; 7 | import org.crazycake.shiro.exception.CacheManagerPrincipalIdNotAssignedException; 8 | import org.crazycake.shiro.exception.PrincipalIdNullException; 9 | import org.crazycake.shiro.exception.PrincipalInstanceException; 10 | import org.crazycake.shiro.exception.SerializationException; 11 | import org.crazycake.shiro.serializer.RedisSerializer; 12 | import org.crazycake.shiro.serializer.StringSerializer; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.lang.reflect.InvocationTargetException; 17 | import java.lang.reflect.Method; 18 | import java.util.*; 19 | 20 | /** 21 | * Used for setting/getting authorization information from Redis 22 | * @param 23 | * @param 24 | */ 25 | public class RedisCache implements Cache { 26 | 27 | private static Logger logger = LoggerFactory.getLogger(RedisCache.class); 28 | 29 | private RedisSerializer keySerializer; 30 | private RedisSerializer valueSerializer; 31 | private IRedisManager redisManager; 32 | private String keyPrefix = RedisCacheManager.DEFAULT_CACHE_KEY_PREFIX; 33 | private int expire; 34 | private String principalIdFieldName = RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME; 35 | 36 | /** 37 | * 38 | * @param redisManager redisManager 39 | * @param keySerializer keySerializer 40 | * @param valueSerializer valueSerializer 41 | * @param prefix authorization prefix 42 | * @param expire expire 43 | * @param principalIdFieldName id field name of principal object 44 | */ 45 | public RedisCache(IRedisManager redisManager, RedisSerializer keySerializer, RedisSerializer valueSerializer, String prefix, int expire, String principalIdFieldName) { 46 | if (redisManager == null) { 47 | throw new IllegalArgumentException("redisManager cannot be null."); 48 | } 49 | this.redisManager = redisManager; 50 | if (keySerializer == null) { 51 | throw new IllegalArgumentException("keySerializer cannot be null."); 52 | } 53 | this.keySerializer = keySerializer; 54 | if (valueSerializer == null) { 55 | throw new IllegalArgumentException("valueSerializer cannot be null."); 56 | } 57 | this.valueSerializer = valueSerializer; 58 | if (prefix != null && !"".equals(prefix)) { 59 | this.keyPrefix = prefix; 60 | } 61 | this.expire = expire; 62 | if (principalIdFieldName != null) { 63 | this.principalIdFieldName = principalIdFieldName; 64 | } 65 | } 66 | 67 | /** 68 | * get shiro authorization redis key-value 69 | * @param key key 70 | * @return value 71 | * @throws CacheException get cache exception 72 | */ 73 | @Override 74 | public V get(K key) throws CacheException { 75 | logger.debug("get key [" + key + "]"); 76 | 77 | if (key == null) { 78 | return null; 79 | } 80 | 81 | try { 82 | Object redisCacheKey = getRedisCacheKey(key); 83 | byte[] rawValue = redisManager.get(keySerializer.serialize(redisCacheKey)); 84 | if (rawValue == null) { 85 | return null; 86 | } 87 | V value = (V) valueSerializer.deserialize(rawValue); 88 | return value; 89 | } catch (SerializationException e) { 90 | throw new CacheException(e); 91 | } 92 | } 93 | 94 | @Override 95 | public V put(K key, V value) throws CacheException { 96 | if (key == null) { 97 | logger.warn("Saving a null key is meaningless, return value directly without call Redis."); 98 | return value; 99 | } 100 | try { 101 | Object redisCacheKey = getRedisCacheKey(key); 102 | logger.debug("put key [" + redisCacheKey + "]"); 103 | redisManager.set(keySerializer.serialize(redisCacheKey), value != null ? valueSerializer.serialize(value) : null, expire); 104 | return value; 105 | } catch (SerializationException e) { 106 | throw new CacheException(e); 107 | } 108 | } 109 | 110 | @Override 111 | public V remove(K key) throws CacheException { 112 | logger.debug("remove key [" + key + "]"); 113 | if (key == null) { 114 | return null; 115 | } 116 | try { 117 | Object redisCacheKey = getRedisCacheKey(key); 118 | byte[] rawValue = redisManager.get(keySerializer.serialize(redisCacheKey)); 119 | V previous = (V) valueSerializer.deserialize(rawValue); 120 | redisManager.del(keySerializer.serialize(redisCacheKey)); 121 | return previous; 122 | } catch (SerializationException e) { 123 | throw new CacheException(e); 124 | } 125 | } 126 | 127 | /** 128 | * get the full Redis key including prefix by Redis key 129 | * @param key 130 | * @return 131 | */ 132 | private Object getRedisCacheKey(K key) { 133 | if (key == null) { 134 | return null; 135 | } 136 | if (keySerializer instanceof StringSerializer) { 137 | return this.keyPrefix + getStringRedisKey(key); 138 | } 139 | return key; 140 | } 141 | 142 | /** 143 | * get Redis key (not including prefix) 144 | * @param key 145 | * @return 146 | */ 147 | private String getStringRedisKey(K key) { 148 | String redisKey; 149 | if (key instanceof PrincipalCollection) { 150 | redisKey = getRedisKeyFromPrincipalIdField((PrincipalCollection) key); 151 | } else { 152 | redisKey = key.toString(); 153 | } 154 | return redisKey; 155 | } 156 | 157 | /** 158 | * get the Redis key (not including prefix) by PrincipalCollection 159 | * @param key 160 | * @return 161 | */ 162 | private String getRedisKeyFromPrincipalIdField(PrincipalCollection key) { 163 | Object principalObject = key.getPrimaryPrincipal(); 164 | if (principalObject instanceof String) { 165 | return principalObject.toString(); 166 | } 167 | Method pincipalIdGetter = getPrincipalIdGetter(principalObject); 168 | return getIdObj(principalObject, pincipalIdGetter); 169 | } 170 | 171 | private String getIdObj(Object principalObject, Method pincipalIdGetter) { 172 | String redisKey; 173 | try { 174 | Object idObj = pincipalIdGetter.invoke(principalObject); 175 | if (idObj == null) { 176 | throw new PrincipalIdNullException(principalObject.getClass(), this.principalIdFieldName); 177 | } 178 | redisKey = idObj.toString(); 179 | } catch (IllegalAccessException e) { 180 | throw new PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName, e); 181 | } catch (InvocationTargetException e) { 182 | throw new PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName, e); 183 | } 184 | return redisKey; 185 | } 186 | 187 | private Method getPrincipalIdGetter(Object principalObject) { 188 | Method pincipalIdGetter = null; 189 | String principalIdMethodName = this.getPrincipalIdMethodName(); 190 | try { 191 | pincipalIdGetter = principalObject.getClass().getMethod(principalIdMethodName); 192 | } catch (NoSuchMethodException e) { 193 | throw new PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName); 194 | } 195 | return pincipalIdGetter; 196 | } 197 | 198 | private String getPrincipalIdMethodName() { 199 | if (this.principalIdFieldName == null || "".equals(this.principalIdFieldName)) { 200 | throw new CacheManagerPrincipalIdNotAssignedException(); 201 | } 202 | return "get" + this.principalIdFieldName.substring(0, 1).toUpperCase() + this.principalIdFieldName.substring(1); 203 | } 204 | 205 | 206 | @Override 207 | public void clear() throws CacheException { 208 | logger.debug("clear cache"); 209 | Set keys = null; 210 | try { 211 | keys = redisManager.keys(keySerializer.serialize(this.keyPrefix + "*")); 212 | } catch (SerializationException e) { 213 | logger.error("get keys error", e); 214 | } 215 | if (keys == null || keys.size() == 0) { 216 | return; 217 | } 218 | for (byte[] key: keys) { 219 | redisManager.del(key); 220 | } 221 | } 222 | 223 | /** 224 | * get all authorization key-value quantity 225 | * @return key-value size 226 | */ 227 | @Override 228 | public int size() { 229 | Long longSize = 0L; 230 | try { 231 | longSize = new Long(redisManager.dbSize(keySerializer.serialize(this.keyPrefix + "*"))); 232 | } catch (SerializationException e) { 233 | logger.error("get keys error", e); 234 | } 235 | return longSize.intValue(); 236 | } 237 | 238 | @SuppressWarnings("unchecked") 239 | @Override 240 | public Set keys() { 241 | Set keys = null; 242 | try { 243 | keys = redisManager.keys(keySerializer.serialize(this.keyPrefix + "*")); 244 | } catch (SerializationException e) { 245 | logger.error("get keys error", e); 246 | return Collections.emptySet(); 247 | } 248 | 249 | if (CollectionUtils.isEmpty(keys)) { 250 | return Collections.emptySet(); 251 | } 252 | 253 | Set convertedKeys = new HashSet(); 254 | for (byte[] key:keys) { 255 | try { 256 | convertedKeys.add((K) keySerializer.deserialize(key)); 257 | } catch (SerializationException e) { 258 | logger.error("deserialize keys error", e); 259 | } 260 | } 261 | return convertedKeys; 262 | } 263 | 264 | @Override 265 | public Collection values() { 266 | Set keys = null; 267 | try { 268 | keys = redisManager.keys(keySerializer.serialize(this.keyPrefix + "*")); 269 | } catch (SerializationException e) { 270 | logger.error("get values error", e); 271 | return Collections.emptySet(); 272 | } 273 | 274 | if (CollectionUtils.isEmpty(keys)) { 275 | return Collections.emptySet(); 276 | } 277 | 278 | List values = new ArrayList(keys.size()); 279 | for (byte[] key : keys) { 280 | V value = null; 281 | try { 282 | value = (V) valueSerializer.deserialize(redisManager.get(key)); 283 | } catch (SerializationException e) { 284 | logger.error("deserialize values= error", e); 285 | } 286 | if (value != null) { 287 | values.add(value); 288 | } 289 | } 290 | return Collections.unmodifiableList(values); 291 | } 292 | 293 | public String getKeyPrefix() { 294 | return keyPrefix; 295 | } 296 | 297 | public void setKeyPrefix(String keyPrefix) { 298 | this.keyPrefix = keyPrefix; 299 | } 300 | 301 | public String getPrincipalIdFieldName() { 302 | return principalIdFieldName; 303 | } 304 | 305 | public void setPrincipalIdFieldName(String principalIdFieldName) { 306 | this.principalIdFieldName = principalIdFieldName; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/RedisCacheManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.apache.shiro.cache.Cache; 4 | import org.apache.shiro.cache.CacheException; 5 | import org.apache.shiro.cache.CacheManager; 6 | import org.crazycake.shiro.serializer.ObjectSerializer; 7 | import org.crazycake.shiro.serializer.RedisSerializer; 8 | import org.crazycake.shiro.serializer.StringSerializer; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.concurrent.ConcurrentMap; 14 | 15 | public class RedisCacheManager implements CacheManager { 16 | 17 | private final Logger logger = LoggerFactory.getLogger(RedisCacheManager.class); 18 | 19 | // fast lookup by name map 20 | private final ConcurrentMap caches = new ConcurrentHashMap<>(); 21 | private RedisSerializer keySerializer = new StringSerializer(); 22 | private RedisSerializer valueSerializer = new ObjectSerializer(); 23 | 24 | private IRedisManager redisManager; 25 | 26 | // expire time in seconds 27 | public static final int DEFAULT_EXPIRE = 1800; 28 | private int expire = DEFAULT_EXPIRE; 29 | 30 | /** 31 | * The Redis key prefix for caches 32 | */ 33 | public static final String DEFAULT_CACHE_KEY_PREFIX = "shiro:cache:"; 34 | private String keyPrefix = DEFAULT_CACHE_KEY_PREFIX; 35 | 36 | public static final String DEFAULT_PRINCIPAL_ID_FIELD_NAME = "id"; 37 | private String principalIdFieldName = DEFAULT_PRINCIPAL_ID_FIELD_NAME; 38 | 39 | @Override 40 | public Cache getCache(String name) throws CacheException { 41 | logger.debug("get cache, name=" + name); 42 | 43 | Cache cache = caches.get(name); 44 | 45 | if (cache == null) { 46 | cache = new RedisCache(redisManager, keySerializer, valueSerializer, keyPrefix + name + ":", expire, principalIdFieldName); 47 | caches.put(name, cache); 48 | } 49 | return cache; 50 | } 51 | 52 | public IRedisManager getRedisManager() { 53 | return redisManager; 54 | } 55 | 56 | public void setRedisManager(IRedisManager redisManager) { 57 | this.redisManager = redisManager; 58 | } 59 | 60 | public String getKeyPrefix() { 61 | return keyPrefix; 62 | } 63 | 64 | public void setKeyPrefix(String keyPrefix) { 65 | this.keyPrefix = keyPrefix; 66 | } 67 | 68 | public RedisSerializer getKeySerializer() { 69 | return keySerializer; 70 | } 71 | 72 | public void setKeySerializer(RedisSerializer keySerializer) { 73 | this.keySerializer = keySerializer; 74 | } 75 | 76 | public RedisSerializer getValueSerializer() { 77 | return valueSerializer; 78 | } 79 | 80 | public void setValueSerializer(RedisSerializer valueSerializer) { 81 | this.valueSerializer = valueSerializer; 82 | } 83 | 84 | public int getExpire() { 85 | return expire; 86 | } 87 | 88 | public void setExpire(int expire) { 89 | this.expire = expire; 90 | } 91 | 92 | public String getPrincipalIdFieldName() { 93 | return principalIdFieldName; 94 | } 95 | 96 | public void setPrincipalIdFieldName(String principalIdFieldName) { 97 | this.principalIdFieldName = principalIdFieldName; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/RedisClusterManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import redis.clients.jedis.*; 4 | 5 | import java.util.HashSet; 6 | import java.util.Iterator; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | public class RedisClusterManager implements IRedisManager { 11 | 12 | private static final int DEFAULT_COUNT = 100; 13 | private static final int DEFAULT_MAX_ATTEMPTS = 3; 14 | private static final String DEFAULT_HOST = "127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002"; 15 | private String host = DEFAULT_HOST; 16 | 17 | // timeout for jedis try to connect to redis server, not expire time! In milliseconds 18 | private int timeout = Protocol.DEFAULT_TIMEOUT; 19 | 20 | // timeout for jedis try to read data from redis server 21 | private int soTimeout = Protocol.DEFAULT_TIMEOUT; 22 | 23 | private String password; 24 | 25 | private int database = Protocol.DEFAULT_DATABASE; 26 | 27 | private int count = DEFAULT_COUNT; 28 | 29 | private int maxAttempts = DEFAULT_MAX_ATTEMPTS; 30 | 31 | /** 32 | * JedisPoolConfig used to initialize JedisPool. 33 | */ 34 | private JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 35 | 36 | private volatile JedisCluster jedisCluster = null; 37 | 38 | private void init() { 39 | if (jedisCluster == null) { 40 | synchronized (RedisClusterManager.class) { 41 | if (jedisCluster == null) { 42 | jedisCluster = new JedisCluster(getHostAndPortSet(), timeout, soTimeout, maxAttempts, password, getJedisPoolConfig()); 43 | } 44 | } 45 | } 46 | } 47 | 48 | private Set getHostAndPortSet() { 49 | String[] hostAndPortArr = host.split(","); 50 | Set hostAndPorts = new HashSet(); 51 | for (String hostAndPortStr : hostAndPortArr) { 52 | String[] hostAndPort = hostAndPortStr.split(":"); 53 | hostAndPorts.add(new HostAndPort(hostAndPort[0], Integer.parseInt(hostAndPort[1]))); 54 | } 55 | return hostAndPorts; 56 | } 57 | 58 | 59 | protected JedisCluster getJedisCluster() { 60 | if (jedisCluster == null) { 61 | init(); 62 | } 63 | return jedisCluster; 64 | } 65 | 66 | @Override 67 | public byte[] get(byte[] key) { 68 | if (key == null) { 69 | return null; 70 | } 71 | return getJedisCluster().get(key); 72 | } 73 | 74 | @Override 75 | public byte[] set(byte[] key, byte[] value, int expireTime) { 76 | if (key == null) { 77 | return null; 78 | } 79 | getJedisCluster().set(key, value); 80 | if (expireTime >= 0) { 81 | getJedisCluster().expire(key, expireTime); 82 | } 83 | return value; 84 | } 85 | 86 | @Override 87 | public void del(byte[] key) { 88 | if (key == null) { 89 | return; 90 | } 91 | getJedisCluster().del(key); 92 | } 93 | 94 | @Override 95 | public Long dbSize(byte[] pattern) { 96 | long dbSize = 0L; 97 | Map clusterNodes = getJedisCluster().getClusterNodes(); 98 | Iterator> nodeIt = clusterNodes.entrySet().iterator(); 99 | while (nodeIt.hasNext()) { 100 | Map.Entry node = nodeIt.next(); 101 | long nodeDbSize = getDbSizeFromClusterNode(node.getValue(), pattern); 102 | if (nodeDbSize == 0L) { 103 | continue; 104 | } 105 | dbSize += nodeDbSize; 106 | } 107 | return dbSize; 108 | } 109 | 110 | @Override 111 | public Set keys(byte[] pattern) { 112 | Set keys = new HashSet(); 113 | Map clusterNodes = getJedisCluster().getClusterNodes(); 114 | Iterator> nodeIt = clusterNodes.entrySet().iterator(); 115 | while (nodeIt.hasNext()) { 116 | Map.Entry node = nodeIt.next(); 117 | Set nodeKeys = getKeysFromClusterNode(node.getValue(), pattern); 118 | if (nodeKeys.size() == 0) { 119 | continue; 120 | } 121 | keys.addAll(nodeKeys); 122 | } 123 | 124 | return keys; 125 | } 126 | 127 | private Set getKeysFromClusterNode(JedisPool jedisPool, byte[] pattern) { 128 | Set keys = new HashSet(); 129 | Jedis jedis = jedisPool.getResource(); 130 | 131 | try { 132 | ScanParams params = new ScanParams(); 133 | params.count(count); 134 | params.match(pattern); 135 | byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY; 136 | ScanResult scanResult; 137 | do { 138 | scanResult = jedis.scan(cursor, params); 139 | keys.addAll(scanResult.getResult()); 140 | cursor = scanResult.getCursorAsBytes(); 141 | } while (scanResult.getCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0); 142 | } finally { 143 | jedis.close(); 144 | } 145 | return keys; 146 | } 147 | 148 | private long getDbSizeFromClusterNode(JedisPool jedisPool, byte[] pattern) { 149 | long dbSize = 0L; 150 | Jedis jedis = jedisPool.getResource(); 151 | 152 | try { 153 | ScanParams params = new ScanParams(); 154 | params.count(count); 155 | params.match(pattern); 156 | byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY; 157 | ScanResult scanResult; 158 | do { 159 | scanResult = jedis.scan(cursor, params); 160 | dbSize++; 161 | cursor = scanResult.getCursorAsBytes(); 162 | } while (scanResult.getCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0); 163 | } finally { 164 | jedis.close(); 165 | } 166 | return dbSize; 167 | } 168 | 169 | public int getMaxAttempts() { 170 | return maxAttempts; 171 | } 172 | 173 | public void setMaxAttempts(int maxAttempts) { 174 | this.maxAttempts = maxAttempts; 175 | } 176 | 177 | public String getHost() { 178 | return host; 179 | } 180 | 181 | public void setHost(String host) { 182 | this.host = host; 183 | } 184 | 185 | public int getTimeout() { 186 | return timeout; 187 | } 188 | 189 | public void setTimeout(int timeout) { 190 | this.timeout = timeout; 191 | } 192 | 193 | public int getSoTimeout() { 194 | return soTimeout; 195 | } 196 | 197 | public void setSoTimeout(int soTimeout) { 198 | this.soTimeout = soTimeout; 199 | } 200 | 201 | public String getPassword() { 202 | return password; 203 | } 204 | 205 | public void setPassword(String password) { 206 | this.password = password; 207 | } 208 | 209 | public int getDatabase() { 210 | return database; 211 | } 212 | 213 | public void setDatabase(int database) { 214 | this.database = database; 215 | } 216 | 217 | public int getCount() { 218 | return count; 219 | } 220 | 221 | public void setCount(int count) { 222 | this.count = count; 223 | } 224 | 225 | public void setJedisCluster(JedisCluster jedisCluster) { 226 | this.jedisCluster = jedisCluster; 227 | } 228 | 229 | public JedisPoolConfig getJedisPoolConfig() { 230 | return jedisPoolConfig; 231 | } 232 | 233 | public void setJedisPoolConfig(JedisPoolConfig jedisPoolConfig) { 234 | this.jedisPoolConfig = jedisPoolConfig; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/RedisManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.crazycake.shiro.common.WorkAloneRedisManager; 4 | import redis.clients.jedis.Jedis; 5 | import redis.clients.jedis.JedisPool; 6 | import redis.clients.jedis.Protocol; 7 | 8 | public class RedisManager extends WorkAloneRedisManager implements IRedisManager { 9 | 10 | private static final String DEFAULT_HOST = "127.0.0.1:6379"; 11 | private String host = DEFAULT_HOST; 12 | 13 | // timeout for jedis try to connect to redis server, not expire time! In milliseconds 14 | private int timeout = Protocol.DEFAULT_TIMEOUT; 15 | 16 | private String password; 17 | 18 | private int database = Protocol.DEFAULT_DATABASE; 19 | 20 | private volatile JedisPool jedisPool; 21 | 22 | private void init() { 23 | if (jedisPool == null) { 24 | synchronized (RedisManager.class) { 25 | if (jedisPool == null) { 26 | String[] hostAndPort = host.split(":"); 27 | jedisPool = new JedisPool(getJedisPoolConfig(), hostAndPort[0], Integer.parseInt(hostAndPort[1]), timeout, password, database); 28 | } 29 | } 30 | } 31 | } 32 | 33 | @Override 34 | protected Jedis getJedis() { 35 | if (jedisPool == null) { 36 | init(); 37 | } 38 | return jedisPool.getResource(); 39 | } 40 | 41 | public String getHost() { 42 | return host; 43 | } 44 | 45 | public void setHost(String host) { 46 | this.host = host; 47 | } 48 | 49 | public int getTimeout() { 50 | return timeout; 51 | } 52 | 53 | public void setTimeout(int timeout) { 54 | this.timeout = timeout; 55 | } 56 | 57 | public String getPassword() { 58 | return password; 59 | } 60 | 61 | public void setPassword(String password) { 62 | this.password = password; 63 | } 64 | 65 | public int getDatabase() { 66 | return database; 67 | } 68 | 69 | public void setDatabase(int database) { 70 | this.database = database; 71 | } 72 | 73 | public JedisPool getJedisPool() { 74 | return jedisPool; 75 | } 76 | 77 | public void setJedisPool(JedisPool jedisPool) { 78 | this.jedisPool = jedisPool; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/RedisSentinelManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.crazycake.shiro.common.WorkAloneRedisManager; 4 | import redis.clients.jedis.Jedis; 5 | import redis.clients.jedis.JedisSentinelPool; 6 | import redis.clients.jedis.Protocol; 7 | 8 | import java.util.Collections; 9 | import java.util.HashSet; 10 | import java.util.Set; 11 | 12 | public class RedisSentinelManager extends WorkAloneRedisManager implements IRedisManager { 13 | 14 | private static final String DEFAULT_HOST = "127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381"; 15 | private String host = DEFAULT_HOST; 16 | 17 | private static final String DEFAULT_MASTER_NAME = "mymaster"; 18 | private String masterName = DEFAULT_MASTER_NAME; 19 | 20 | // timeout for jedis try to connect to redis server, not expire time! In milliseconds 21 | private int timeout = Protocol.DEFAULT_TIMEOUT; 22 | 23 | // timeout for jedis try to read data from redis server 24 | private int soTimeout = Protocol.DEFAULT_TIMEOUT; 25 | 26 | private String password; 27 | 28 | private int database = Protocol.DEFAULT_DATABASE; 29 | 30 | private volatile JedisSentinelPool jedisPool; 31 | 32 | @Override 33 | protected Jedis getJedis() { 34 | if (jedisPool == null) { 35 | init(); 36 | } 37 | return jedisPool.getResource(); 38 | } 39 | 40 | private void init() { 41 | if (jedisPool == null) { 42 | synchronized (RedisSentinelManager.class) { 43 | if (jedisPool == null) { 44 | String[] sentinelHosts = host.split(",\\s*"); 45 | Set sentinels = new HashSet(); 46 | Collections.addAll(sentinels, sentinelHosts); 47 | jedisPool = new JedisSentinelPool(masterName, sentinels, getJedisPoolConfig(), timeout, soTimeout, password, database); 48 | } 49 | } 50 | } 51 | } 52 | 53 | public String getHost() { 54 | return host; 55 | } 56 | 57 | public void setHost(String host) { 58 | this.host = host; 59 | } 60 | 61 | public int getTimeout() { 62 | return timeout; 63 | } 64 | 65 | public void setTimeout(int timeout) { 66 | this.timeout = timeout; 67 | } 68 | 69 | public String getPassword() { 70 | return password; 71 | } 72 | 73 | public void setPassword(String password) { 74 | this.password = password; 75 | } 76 | 77 | public int getDatabase() { 78 | return database; 79 | } 80 | 81 | public void setDatabase(int database) { 82 | this.database = database; 83 | } 84 | 85 | public String getMasterName() { 86 | return masterName; 87 | } 88 | 89 | public void setMasterName(String masterName) { 90 | this.masterName = masterName; 91 | } 92 | 93 | public int getSoTimeout() { 94 | return soTimeout; 95 | } 96 | 97 | public void setSoTimeout(int soTimeout) { 98 | this.soTimeout = soTimeout; 99 | } 100 | 101 | public JedisSentinelPool getJedisPool() { 102 | return jedisPool; 103 | } 104 | 105 | public void setJedisPool(JedisSentinelPool jedisPool) { 106 | this.jedisPool = jedisPool; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/RedisSessionDAO.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.apache.shiro.session.Session; 4 | import org.apache.shiro.session.UnknownSessionException; 5 | import org.apache.shiro.session.mgt.eis.AbstractSessionDAO; 6 | import org.crazycake.shiro.common.SessionInMemory; 7 | import org.crazycake.shiro.exception.SerializationException; 8 | import org.crazycake.shiro.serializer.ObjectSerializer; 9 | import org.crazycake.shiro.serializer.RedisSerializer; 10 | import org.crazycake.shiro.serializer.StringSerializer; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.io.Serializable; 15 | import java.util.*; 16 | 17 | /** 18 | * Used for setting/getting authentication information from Redis 19 | */ 20 | public class RedisSessionDAO extends AbstractSessionDAO { 21 | 22 | private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class); 23 | 24 | private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:"; 25 | private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX; 26 | 27 | /** 28 | * doReadSession be called about 10 times when login. 29 | * Save Session in ThreadLocal to resolve this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal. 30 | * The default value is 1000 milliseconds (1s). 31 | * Most of time, you don't need to change it. 32 | * 33 | * You can turn it off by setting sessionInMemoryEnabled to false 34 | */ 35 | private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 1000L; 36 | private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT; 37 | 38 | private static final boolean DEFAULT_SESSION_IN_MEMORY_ENABLED = true; 39 | private boolean sessionInMemoryEnabled = DEFAULT_SESSION_IN_MEMORY_ENABLED; 40 | 41 | private static ThreadLocal sessionsInThread = new ThreadLocal(); 42 | 43 | /** 44 | * expire time in seconds. 45 | * NOTE: Please make sure expire is longer than session.getTimeout(), 46 | * otherwise you might need the issue that session in Redis got erased when the Session is still available 47 | * 48 | * DEFAULT_EXPIRE: use the timeout of session instead of setting it by yourself 49 | * NO_EXPIRE: never expire 50 | */ 51 | private static final int DEFAULT_EXPIRE = -2; 52 | private static final int NO_EXPIRE = -1; 53 | private int expire = DEFAULT_EXPIRE; 54 | 55 | private static final int MILLISECONDS_IN_A_SECOND = 1000; 56 | 57 | /** 58 | * redisManager used for communicate with Redis 59 | */ 60 | private IRedisManager redisManager; 61 | 62 | /** 63 | * Serializer of key 64 | */ 65 | private RedisSerializer keySerializer = new StringSerializer(); 66 | 67 | /** 68 | * Serializer of value 69 | */ 70 | private RedisSerializer valueSerializer = new ObjectSerializer(); 71 | 72 | /** 73 | * save/update session 74 | * @param session 75 | * @throws UnknownSessionException 76 | */ 77 | @Override 78 | public void update(Session session) throws UnknownSessionException { 79 | if (this.sessionInMemoryEnabled) { 80 | this.removeExpiredSessionInMemory(); 81 | } 82 | this.saveSession(session); 83 | if (this.sessionInMemoryEnabled) { 84 | this.setSessionToThreadLocal(session.getId(), session); 85 | } 86 | } 87 | 88 | private void saveSession(Session session) throws UnknownSessionException { 89 | if (session == null || session.getId() == null) { 90 | logger.error("session or session id is null"); 91 | throw new UnknownSessionException("session or session id is null"); 92 | } 93 | byte[] key; 94 | byte[] value; 95 | try { 96 | key = keySerializer.serialize(getRedisSessionKey(session.getId())); 97 | value = valueSerializer.serialize(session); 98 | } catch (SerializationException e) { 99 | logger.error("serialize session error. session id=" + session.getId()); 100 | throw new UnknownSessionException(e); 101 | } 102 | if (expire == DEFAULT_EXPIRE) { 103 | redisManager.set(key, value, (int) (session.getTimeout() / MILLISECONDS_IN_A_SECOND)); 104 | return; 105 | } 106 | if (expire != NO_EXPIRE && expire * MILLISECONDS_IN_A_SECOND < session.getTimeout()) { 107 | logger.warn("Redis session expire time: " 108 | + (expire * MILLISECONDS_IN_A_SECOND) 109 | + " is less than Session timeout: " 110 | + session.getTimeout() 111 | + " . It may cause some problems."); 112 | } 113 | redisManager.set(key, value, expire); 114 | } 115 | 116 | /** 117 | * delete session 118 | * @param session 119 | */ 120 | @Override 121 | public void delete(Session session) { 122 | if (this.sessionInMemoryEnabled) { 123 | this.removeExpiredSessionInMemory(); 124 | } 125 | if (session == null || session.getId() == null) { 126 | logger.error("session or session id is null"); 127 | return; 128 | } 129 | if (this.sessionInMemoryEnabled) { 130 | this.delSessionFromThreadLocal(session.getId()); 131 | } 132 | try { 133 | redisManager.del(keySerializer.serialize(getRedisSessionKey(session.getId()))); 134 | } catch (SerializationException e) { 135 | logger.error("delete session error. session id=" + session.getId()); 136 | } 137 | } 138 | 139 | /** 140 | * get all active sessions 141 | * @return 142 | */ 143 | @Override 144 | public Collection getActiveSessions() { 145 | if (this.sessionInMemoryEnabled) { 146 | this.removeExpiredSessionInMemory(); 147 | } 148 | Set sessions = new HashSet(); 149 | try { 150 | Set keys = redisManager.keys(keySerializer.serialize(this.keyPrefix + "*")); 151 | if (keys != null && keys.size() > 0) { 152 | for (byte[] key:keys) { 153 | Object deserialize = valueSerializer.deserialize(redisManager.get(key)); 154 | if (deserialize == null) { 155 | continue; 156 | } 157 | sessions.add((Session) deserialize); 158 | } 159 | } 160 | } catch (SerializationException e) { 161 | logger.error("get active sessions error."); 162 | } 163 | return sessions; 164 | } 165 | 166 | @Override 167 | protected Serializable doCreate(Session session) { 168 | if (this.sessionInMemoryEnabled) { 169 | this.removeExpiredSessionInMemory(); 170 | } 171 | if (session == null) { 172 | logger.error("session is null"); 173 | throw new UnknownSessionException("session is null"); 174 | } 175 | Serializable sessionId = this.generateSessionId(session); 176 | this.assignSessionId(session, sessionId); 177 | this.saveSession(session); 178 | return sessionId; 179 | } 180 | 181 | /** 182 | * I change 183 | * @param sessionId 184 | * @return 185 | */ 186 | @Override 187 | protected Session doReadSession(Serializable sessionId) { 188 | if (this.sessionInMemoryEnabled) { 189 | this.removeExpiredSessionInMemory(); 190 | } 191 | if (sessionId == null) { 192 | logger.warn("session id is null"); 193 | return null; 194 | } 195 | if (this.sessionInMemoryEnabled) { 196 | Session session = getSessionFromThreadLocal(sessionId); 197 | if (session != null) { 198 | return session; 199 | } 200 | } 201 | Session session = null; 202 | try { 203 | String sessionRedisKey = getRedisSessionKey(sessionId); 204 | logger.debug("read session: " + sessionRedisKey + " from Redis"); 205 | session = (Session) valueSerializer.deserialize(redisManager.get(keySerializer.serialize(sessionRedisKey))); 206 | if (this.sessionInMemoryEnabled) { 207 | setSessionToThreadLocal(sessionId, session); 208 | } 209 | } catch (SerializationException e) { 210 | logger.error("read session error. sessionId: " + sessionId); 211 | } 212 | return session; 213 | } 214 | 215 | private void setSessionToThreadLocal(Serializable sessionId, Session session) { 216 | this.initSessionsInThread(); 217 | Map sessionMap = (Map) sessionsInThread.get(); 218 | sessionMap.put(sessionId, this.createSessionInMemory(session)); 219 | } 220 | 221 | private void delSessionFromThreadLocal(Serializable sessionId) { 222 | Map sessionMap = (Map) sessionsInThread.get(); 223 | if (sessionMap == null) { 224 | return; 225 | } 226 | sessionMap.remove(sessionId); 227 | } 228 | 229 | private SessionInMemory createSessionInMemory(Session session) { 230 | SessionInMemory sessionInMemory = new SessionInMemory(); 231 | sessionInMemory.setCreateTime(new Date()); 232 | sessionInMemory.setSession(session); 233 | return sessionInMemory; 234 | } 235 | 236 | private void initSessionsInThread() { 237 | Map sessionMap = (Map) sessionsInThread.get(); 238 | if (sessionMap == null) { 239 | sessionMap = new HashMap(); 240 | sessionsInThread.set(sessionMap); 241 | } 242 | } 243 | 244 | private void removeExpiredSessionInMemory() { 245 | Map sessionMap = (Map) sessionsInThread.get(); 246 | if (sessionMap == null) { 247 | return; 248 | } 249 | Iterator it = sessionMap.keySet().iterator(); 250 | while (it.hasNext()) { 251 | Serializable sessionId = it.next(); 252 | SessionInMemory sessionInMemory = sessionMap.get(sessionId); 253 | if (sessionInMemory == null) { 254 | it.remove(); 255 | continue; 256 | } 257 | long liveTime = getSessionInMemoryLiveTime(sessionInMemory); 258 | if (liveTime > sessionInMemoryTimeout) { 259 | it.remove(); 260 | } 261 | } 262 | if (sessionMap.size() == 0) { 263 | sessionsInThread.remove(); 264 | } 265 | } 266 | 267 | private Session getSessionFromThreadLocal(Serializable sessionId) { 268 | if (sessionsInThread.get() == null) { 269 | return null; 270 | } 271 | 272 | Map sessionMap = (Map) sessionsInThread.get(); 273 | SessionInMemory sessionInMemory = sessionMap.get(sessionId); 274 | if (sessionInMemory == null) { 275 | return null; 276 | } 277 | 278 | logger.debug("read session from memory"); 279 | return sessionInMemory.getSession(); 280 | } 281 | 282 | private long getSessionInMemoryLiveTime(SessionInMemory sessionInMemory) { 283 | Date now = new Date(); 284 | return now.getTime() - sessionInMemory.getCreateTime().getTime(); 285 | } 286 | 287 | private String getRedisSessionKey(Serializable sessionId) { 288 | return this.keyPrefix + sessionId; 289 | } 290 | 291 | public IRedisManager getRedisManager() { 292 | return redisManager; 293 | } 294 | 295 | public void setRedisManager(IRedisManager redisManager) { 296 | this.redisManager = redisManager; 297 | } 298 | 299 | public String getKeyPrefix() { 300 | return keyPrefix; 301 | } 302 | 303 | public void setKeyPrefix(String keyPrefix) { 304 | this.keyPrefix = keyPrefix; 305 | } 306 | 307 | public RedisSerializer getKeySerializer() { 308 | return keySerializer; 309 | } 310 | 311 | public void setKeySerializer(RedisSerializer keySerializer) { 312 | this.keySerializer = keySerializer; 313 | } 314 | 315 | public RedisSerializer getValueSerializer() { 316 | return valueSerializer; 317 | } 318 | 319 | public void setValueSerializer(RedisSerializer valueSerializer) { 320 | this.valueSerializer = valueSerializer; 321 | } 322 | 323 | public long getSessionInMemoryTimeout() { 324 | return sessionInMemoryTimeout; 325 | } 326 | 327 | public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) { 328 | this.sessionInMemoryTimeout = sessionInMemoryTimeout; 329 | } 330 | 331 | public int getExpire() { 332 | return expire; 333 | } 334 | 335 | public void setExpire(int expire) { 336 | this.expire = expire; 337 | } 338 | 339 | public boolean getSessionInMemoryEnabled() { 340 | return sessionInMemoryEnabled; 341 | } 342 | 343 | public void setSessionInMemoryEnabled(boolean sessionInMemoryEnabled) { 344 | this.sessionInMemoryEnabled = sessionInMemoryEnabled; 345 | } 346 | 347 | public static ThreadLocal getSessionsInThread() { 348 | return sessionsInThread; 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/common/AbstractLettuceRedisManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.common; 2 | 3 | import io.lettuce.core.*; 4 | import io.lettuce.core.api.StatefulRedisConnection; 5 | import io.lettuce.core.api.async.RedisAsyncCommands; 6 | import io.lettuce.core.api.sync.RedisCommands; 7 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig; 8 | import org.crazycake.shiro.IRedisManager; 9 | 10 | import java.time.Duration; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * @author Teamo 17 | * @since 2022/05/19 18 | */ 19 | public abstract class AbstractLettuceRedisManager implements IRedisManager { 20 | 21 | /** 22 | * Default value of count. 23 | */ 24 | private static final int DEFAULT_COUNT = 100; 25 | 26 | /** 27 | * timeout for RedisClient try to connect to redis server, not expire time! unit seconds. 28 | */ 29 | private Duration timeout = RedisURI.DEFAULT_TIMEOUT_DURATION; 30 | 31 | /** 32 | * Redis database. 33 | */ 34 | private int database = 0; 35 | 36 | /** 37 | * Redis password. 38 | */ 39 | private String password; 40 | 41 | /** 42 | * Whether to enable async. 43 | */ 44 | private boolean isAsync = true; 45 | 46 | /** 47 | * The number of elements returned at every iteration. 48 | */ 49 | private int count = DEFAULT_COUNT; 50 | 51 | /** 52 | * ClientOptions used to initialize RedisClient. 53 | */ 54 | private ClientOptions clientOptions = ClientOptions.create(); 55 | 56 | /** 57 | * genericObjectPoolConfig used to initialize GenericObjectPoolConfig object. 58 | */ 59 | @SuppressWarnings("rawtypes") 60 | private GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig<>(); 61 | 62 | /** 63 | * Get a stateful connection. 64 | * 65 | * @return T 66 | */ 67 | @SuppressWarnings("rawtypes") 68 | protected abstract StatefulRedisConnection getStatefulConnection(); 69 | 70 | public Duration getTimeout() { 71 | return timeout; 72 | } 73 | 74 | public void setTimeout(Duration timeout) { 75 | this.timeout = timeout; 76 | } 77 | 78 | public int getDatabase() { 79 | return database; 80 | } 81 | 82 | public void setDatabase(int database) { 83 | this.database = database; 84 | } 85 | 86 | public String getPassword() { 87 | return password; 88 | } 89 | 90 | public void setPassword(String password) { 91 | this.password = password; 92 | } 93 | 94 | public boolean isAsync() { 95 | return isAsync; 96 | } 97 | 98 | public void setIsAsync(boolean isAsync) { 99 | this.isAsync = isAsync; 100 | } 101 | 102 | public int getCount() { 103 | return count; 104 | } 105 | 106 | public void setCount(int count) { 107 | this.count = count; 108 | } 109 | 110 | public ClientOptions getClientOptions() { 111 | return clientOptions; 112 | } 113 | 114 | public void setClientOptions(ClientOptions clientOptions) { 115 | this.clientOptions = clientOptions; 116 | } 117 | 118 | public GenericObjectPoolConfig getGenericObjectPoolConfig() { 119 | return genericObjectPoolConfig; 120 | } 121 | 122 | public void setGenericObjectPoolConfig(GenericObjectPoolConfig genericObjectPoolConfig) { 123 | this.genericObjectPoolConfig = genericObjectPoolConfig; 124 | } 125 | 126 | @Override 127 | @SuppressWarnings("unchecked") 128 | public byte[] get(byte[] key) { 129 | if (key == null) { 130 | return null; 131 | } 132 | byte[] value = null; 133 | try (StatefulRedisConnection connect = getStatefulConnection()) { 134 | if (isAsync) { 135 | RedisAsyncCommands async = connect.async(); 136 | RedisFuture redisFuture = async.get(key); 137 | value = LettuceFutures.awaitOrCancel(redisFuture, timeout.getSeconds(), TimeUnit.SECONDS); 138 | } else { 139 | RedisCommands sync = connect.sync(); 140 | value = sync.get(key); 141 | } 142 | } 143 | return value; 144 | } 145 | 146 | @Override 147 | @SuppressWarnings({"unchecked"}) 148 | public byte[] set(byte[] key, byte[] value, int expire) { 149 | if (key == null) { 150 | return null; 151 | } 152 | try (StatefulRedisConnection connect = getStatefulConnection()) { 153 | if (isAsync) { 154 | RedisAsyncCommands async = connect.async(); 155 | if (expire > 0) { 156 | async.set(key, value, SetArgs.Builder.ex(expire)); 157 | } else { 158 | async.set(key, value); 159 | } 160 | } else { 161 | RedisCommands sync = connect.sync(); 162 | if (expire > 0) { 163 | sync.set(key, value, SetArgs.Builder.ex(expire)); 164 | } else { 165 | sync.set(key, value); 166 | } 167 | } 168 | } 169 | return value; 170 | } 171 | 172 | @Override 173 | @SuppressWarnings("unchecked") 174 | public void del(byte[] key) { 175 | try (StatefulRedisConnection connect = getStatefulConnection()) { 176 | if (isAsync) { 177 | RedisAsyncCommands async = connect.async(); 178 | async.del(key); 179 | } else { 180 | RedisCommands sync = connect.sync(); 181 | sync.del(key); 182 | } 183 | } 184 | } 185 | 186 | @Override 187 | @SuppressWarnings("unchecked") 188 | public Long dbSize(byte[] pattern) { 189 | long dbSize = 0L; 190 | KeyScanCursor scanCursor = new KeyScanCursor<>(); 191 | scanCursor.setCursor(ScanCursor.INITIAL.getCursor()); 192 | ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(count); 193 | try (StatefulRedisConnection connect = getStatefulConnection()) { 194 | while (!scanCursor.isFinished()) { 195 | scanCursor = getKeyScanCursor(connect, scanCursor, scanArgs); 196 | dbSize += scanCursor.getKeys().size(); 197 | } 198 | } 199 | return dbSize; 200 | } 201 | 202 | @Override 203 | @SuppressWarnings("unchecked") 204 | public Set keys(byte[] pattern) { 205 | Set keys = new HashSet<>(); 206 | KeyScanCursor scanCursor = new KeyScanCursor<>(); 207 | scanCursor.setCursor(ScanCursor.INITIAL.getCursor()); 208 | ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(count); 209 | try (StatefulRedisConnection connect = getStatefulConnection()) { 210 | while (!scanCursor.isFinished()) { 211 | scanCursor = getKeyScanCursor(connect, scanCursor, scanArgs); 212 | keys.addAll(scanCursor.getKeys()); 213 | } 214 | } 215 | return keys; 216 | } 217 | 218 | /** 219 | * get scan cursor result 220 | * 221 | * @param connect connection 222 | * @param scanCursor scan cursor 223 | * @param scanArgs scan param 224 | * @return KeyScanCursor 225 | */ 226 | private KeyScanCursor getKeyScanCursor(final StatefulRedisConnection connect, 227 | KeyScanCursor scanCursor, 228 | ScanArgs scanArgs) { 229 | if (isAsync) { 230 | RedisAsyncCommands async = connect.async(); 231 | scanCursor = LettuceFutures.awaitOrCancel(async.scan(scanCursor, scanArgs), timeout.getSeconds(), TimeUnit.SECONDS); 232 | } else { 233 | RedisCommands sync = connect.sync(); 234 | scanCursor = sync.scan(scanCursor, scanArgs); 235 | } 236 | return scanCursor; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/common/SessionInMemory.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.common; 2 | 3 | import org.apache.shiro.session.Session; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * Use ThreadLocal as a temporary storage of Session, so that shiro wouldn't keep read redis several times while a request coming. 9 | */ 10 | public class SessionInMemory { 11 | private Session session; 12 | private Date createTime; 13 | 14 | public Session getSession() { 15 | return session; 16 | } 17 | 18 | public void setSession(Session session) { 19 | this.session = session; 20 | } 21 | 22 | public Date getCreateTime() { 23 | return createTime; 24 | } 25 | 26 | public void setCreateTime(Date createTime) { 27 | this.createTime = createTime; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/common/WorkAloneRedisManager.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.common; 2 | 3 | import org.crazycake.shiro.IRedisManager; 4 | import redis.clients.jedis.Jedis; 5 | import redis.clients.jedis.JedisPoolConfig; 6 | import redis.clients.jedis.ScanParams; 7 | import redis.clients.jedis.ScanResult; 8 | 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | /** 14 | * Abstract class of RedisManager. 15 | */ 16 | public abstract class WorkAloneRedisManager implements IRedisManager { 17 | 18 | /** 19 | * We are going to operate redis by acquiring Jedis object. 20 | * The subclass should realizes the way to get Jedis objects by implement the getJedis(). 21 | * @return Jedis 22 | */ 23 | protected abstract Jedis getJedis(); 24 | 25 | /** 26 | * Default value of count. 27 | */ 28 | protected static final int DEFAULT_COUNT = 100; 29 | 30 | /** 31 | * The number of elements returned at every iteration. 32 | */ 33 | private int count = DEFAULT_COUNT; 34 | 35 | /** 36 | * JedisPoolConfig used to initialize JedisPool. 37 | */ 38 | private JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 39 | 40 | /** 41 | * get value from redis 42 | * @param key key 43 | * @return value 44 | */ 45 | @Override 46 | public byte[] get(byte[] key) { 47 | if (key == null) { 48 | return null; 49 | } 50 | byte[] value; 51 | Jedis jedis = getJedis(); 52 | try { 53 | value = jedis.get(key); 54 | } finally { 55 | jedis.close(); 56 | } 57 | return value; 58 | } 59 | 60 | /** 61 | * set 62 | * @param key key 63 | * @param value value 64 | * @param expireTime expire time in second 65 | * @return value 66 | */ 67 | @Override 68 | public byte[] set(byte[] key, byte[] value, int expireTime) { 69 | if (key == null) { 70 | return null; 71 | } 72 | Jedis jedis = getJedis(); 73 | try { 74 | jedis.set(key, value); 75 | // -1 and 0 is not a valid expire time in Jedis 76 | if (expireTime > 0) { 77 | jedis.expire(key, expireTime); 78 | } 79 | } finally { 80 | jedis.close(); 81 | } 82 | return value; 83 | } 84 | 85 | /** 86 | * Delete a key-value pair. 87 | * @param key key 88 | */ 89 | @Override 90 | public void del(byte[] key) { 91 | if (key == null) { 92 | return; 93 | } 94 | Jedis jedis = getJedis(); 95 | try { 96 | jedis.del(key); 97 | } finally { 98 | jedis.close(); 99 | } 100 | } 101 | 102 | /** 103 | * Return the size of redis db. 104 | * @param pattern key pattern 105 | * @return key-value size 106 | */ 107 | @Override 108 | public Long dbSize(byte[] pattern) { 109 | long dbSize = 0L; 110 | Jedis jedis = getJedis(); 111 | try { 112 | ScanParams params = new ScanParams(); 113 | params.count(count); 114 | params.match(pattern); 115 | byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY; 116 | ScanResult scanResult; 117 | do { 118 | scanResult = jedis.scan(cursor, params); 119 | List results = scanResult.getResult(); 120 | dbSize += results.size(); 121 | cursor = scanResult.getCursorAsBytes(); 122 | } while (scanResult.getCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0); 123 | } finally { 124 | jedis.close(); 125 | } 126 | return dbSize; 127 | } 128 | 129 | /** 130 | * Return all the keys of Redis db. Filtered by pattern. 131 | * @param pattern key pattern 132 | * @return key set 133 | */ 134 | @Override 135 | public Set keys(byte[] pattern) { 136 | Set keys = new HashSet(); 137 | Jedis jedis = getJedis(); 138 | 139 | try { 140 | ScanParams params = new ScanParams(); 141 | params.count(count); 142 | params.match(pattern); 143 | byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY; 144 | ScanResult scanResult; 145 | do { 146 | scanResult = jedis.scan(cursor, params); 147 | keys.addAll(scanResult.getResult()); 148 | cursor = scanResult.getCursorAsBytes(); 149 | } while (scanResult.getCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0); 150 | } finally { 151 | jedis.close(); 152 | } 153 | return keys; 154 | 155 | } 156 | 157 | public int getCount() { 158 | return count; 159 | } 160 | 161 | public void setCount(int count) { 162 | this.count = count; 163 | } 164 | 165 | public JedisPoolConfig getJedisPoolConfig() { 166 | return jedisPoolConfig; 167 | } 168 | 169 | public void setJedisPoolConfig(JedisPoolConfig jedisPoolConfig) { 170 | this.jedisPoolConfig = jedisPoolConfig; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/exception/CacheManagerPrincipalIdNotAssignedException.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.exception; 2 | 3 | public class CacheManagerPrincipalIdNotAssignedException extends RuntimeException { 4 | 5 | private static final String MESSAGE = "CacheManager didn't assign Principal Id field name!"; 6 | 7 | public CacheManagerPrincipalIdNotAssignedException() { 8 | super(MESSAGE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/exception/PoolException.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.exception; 2 | 3 | /** 4 | * @author Teamo 5 | * @since 2022/05/18 6 | */ 7 | public class PoolException extends RuntimeException { 8 | /** 9 | * Constructs a new LettucePoolException instance. 10 | * 11 | * @param msg the detail message. 12 | */ 13 | public PoolException(String msg) { 14 | super(msg); 15 | } 16 | 17 | /** 18 | * Constructs a new LettucePoolException instance. 19 | * 20 | * @param msg the detail message. 21 | * @param cause the nested exception. 22 | */ 23 | public PoolException(String msg, Throwable cause) { 24 | super(msg, cause); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/exception/PrincipalIdNullException.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.exception; 2 | 3 | public class PrincipalIdNullException extends RuntimeException { 4 | 5 | private static final String MESSAGE = "Principal Id shouldn't be null!"; 6 | 7 | public PrincipalIdNullException(Class clazz, String idMethodName) { 8 | super(clazz + " id field: " + idMethodName + ", value is null\n" + MESSAGE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/exception/PrincipalInstanceException.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.exception; 2 | 3 | public class PrincipalInstanceException extends RuntimeException { 4 | 5 | private static final String MESSAGE = "We need a field to identify this Cache Object in Redis. " 6 | + "So you need to defined an id field which you can get unique id to identify this principal. " 7 | + "For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. " 8 | + "For example, getUserId(), getUserName(), getEmail(), etc.\n" 9 | + "Default value is \"id\", that means your principal object has a method called \"getId()\""; 10 | 11 | public PrincipalInstanceException(Class clazz, String idMethodName) { 12 | super(clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE); 13 | } 14 | 15 | public PrincipalInstanceException(Class clazz, String idMethodName, Exception e) { 16 | super(clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE, e); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/exception/SerializationException.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.exception; 2 | 3 | public class SerializationException extends Exception { 4 | public SerializationException(String msg) { 5 | super(msg); 6 | } 7 | public SerializationException(String msg, Throwable cause) { 8 | super(msg, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/serializer/MultiClassLoaderObjectInputStream.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.serializer; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.ObjectInputStream; 6 | import java.io.ObjectStreamClass; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | /** 12 | * For fixing https://github.com/alexxiyang/shiro-redis/issues/84 13 | */ 14 | public class MultiClassLoaderObjectInputStream extends ObjectInputStream { 15 | private static Logger logger = LoggerFactory.getLogger(MultiClassLoaderObjectInputStream.class); 16 | 17 | MultiClassLoaderObjectInputStream(InputStream str) throws IOException { 18 | super(str); 19 | } 20 | 21 | /** 22 | * Try : 23 | * 1. thread class loader 24 | * 2. application class loader 25 | * 3. system class loader 26 | * @param desc 27 | * @return 28 | * @throws IOException 29 | * @throws ClassNotFoundException 30 | */ 31 | @Override 32 | protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { 33 | String name = desc.getName(); 34 | 35 | try { 36 | ClassLoader cl = Thread.currentThread().getContextClassLoader(); 37 | return Class.forName(name, false, cl); 38 | } catch (Throwable ex) { 39 | logger.debug("Cannot access thread context ClassLoader!", ex); 40 | } 41 | 42 | try { 43 | ClassLoader cl = MultiClassLoaderObjectInputStream.class.getClassLoader(); 44 | return Class.forName(name, false, cl); 45 | } catch (Throwable ex) { 46 | logger.debug("Cannot access application ClassLoader", ex); 47 | } 48 | 49 | try { 50 | ClassLoader cl = ClassLoader.getSystemClassLoader(); 51 | return Class.forName(name, false, cl); 52 | } catch (Throwable ex) { 53 | logger.debug("Cannot access system ClassLoader", ex); 54 | } 55 | 56 | return super.resolveClass(desc); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/serializer/ObjectSerializer.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.serializer; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.io.ObjectInputStream; 7 | import java.io.ObjectOutputStream; 8 | import java.io.Serializable; 9 | 10 | import org.crazycake.shiro.exception.SerializationException; 11 | 12 | public class ObjectSerializer implements RedisSerializer { 13 | 14 | public static final int BYTE_ARRAY_OUTPUT_STREAM_SIZE = 128; 15 | 16 | @Override 17 | public byte[] serialize(Object object) throws SerializationException { 18 | byte[] result = new byte[0]; 19 | 20 | if (object == null) { 21 | return result; 22 | } 23 | ByteArrayOutputStream byteStream = new ByteArrayOutputStream(BYTE_ARRAY_OUTPUT_STREAM_SIZE); 24 | if (!(object instanceof Serializable)) { 25 | throw new SerializationException("requires a Serializable payload " 26 | + "but received an object of type [" + object.getClass().getName() + "]"); 27 | } 28 | try { 29 | ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream); 30 | objectOutputStream.writeObject(object); 31 | objectOutputStream.flush(); 32 | result = byteStream.toByteArray(); 33 | } catch (IOException e) { 34 | throw new SerializationException("serialize error, object=" + object, e); 35 | } 36 | 37 | return result; 38 | } 39 | 40 | @Override 41 | public Object deserialize(byte[] bytes) throws SerializationException { 42 | Object result = null; 43 | 44 | if (bytes == null || bytes.length == 0) { 45 | return result; 46 | } 47 | 48 | try { 49 | ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes); 50 | ObjectInputStream objectInputStream = new MultiClassLoaderObjectInputStream(byteStream); 51 | result = objectInputStream.readObject(); 52 | } catch (IOException e) { 53 | throw new SerializationException("deserialize error", e); 54 | } catch (ClassNotFoundException e) { 55 | throw new SerializationException("deserialize error", e); 56 | } 57 | 58 | return result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/serializer/RedisSerializer.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.serializer; 2 | 3 | import org.crazycake.shiro.exception.SerializationException; 4 | 5 | public interface RedisSerializer { 6 | 7 | byte[] serialize(T t) throws SerializationException; 8 | 9 | T deserialize(byte[] bytes) throws SerializationException; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/crazycake/shiro/serializer/StringSerializer.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro.serializer; 2 | 3 | import org.crazycake.shiro.exception.SerializationException; 4 | 5 | import java.io.UnsupportedEncodingException; 6 | 7 | public class StringSerializer implements RedisSerializer { 8 | 9 | private static final String DEFAULT_CHARSET = "UTF-8"; 10 | 11 | /** 12 | * Refer to https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html 13 | * UTF-8, UTF-16, UTF-32, ISO-8859-1, GBK, Big5, etc 14 | */ 15 | private String charset = DEFAULT_CHARSET; 16 | 17 | @Override 18 | public byte[] serialize(String s) throws SerializationException { 19 | try { 20 | return (s == null ? null : s.getBytes(charset)); 21 | } catch (UnsupportedEncodingException e) { 22 | throw new SerializationException("serialize error, string=" + s, e); 23 | } 24 | } 25 | 26 | @Override 27 | public String deserialize(byte[] bytes) throws SerializationException { 28 | try { 29 | return (bytes == null ? null : new String(bytes, charset)); 30 | } catch (UnsupportedEncodingException e) { 31 | throw new SerializationException("deserialize error", e); 32 | } 33 | } 34 | 35 | public String getCharset() { 36 | return charset; 37 | } 38 | 39 | public void setCharset(String charset) { 40 | this.charset = charset; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/org/crazycake/shiro/RedisCacheManagerTest.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.apache.shiro.cache.Cache; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.CoreMatchers.is; 9 | import static org.mockito.Mockito.*; 10 | 11 | public class RedisCacheManagerTest { 12 | 13 | private IRedisManager redisManager; 14 | private RedisCacheManager redisCacheManager; 15 | 16 | @BeforeEach 17 | public void setUp() { 18 | redisManager = mock(IRedisManager.class); 19 | } 20 | 21 | @Test 22 | public void testInitWithoutSettingRedisManager() { 23 | redisCacheManager = new RedisCacheManager(); 24 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 25 | redisCacheManager.getCache("testCache"); 26 | }); 27 | } 28 | 29 | @Test 30 | public void testInit() { 31 | redisCacheManager = new RedisCacheManager(); 32 | redisCacheManager.setRedisManager(redisManager); 33 | redisCacheManager.setKeyPrefix("testRedisManager1:"); 34 | redisCacheManager.setPrincipalIdFieldName("id"); 35 | Cache testCache = redisCacheManager.getCache("testCache"); 36 | assertThat(testCache.getClass().getName(), is("org.crazycake.shiro.RedisCache")); 37 | RedisCache redisTestCache = (RedisCache) testCache; 38 | assertThat(redisTestCache.getKeyPrefix(), is("testRedisManager1:testCache:")); 39 | assertThat(redisTestCache.getPrincipalIdFieldName(), is("id")); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/org/crazycake/shiro/RedisCacheTest.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.apache.shiro.subject.PrincipalCollection; 4 | import org.crazycake.shiro.exception.SerializationException; 5 | import org.crazycake.shiro.serializer.ObjectSerializer; 6 | import org.crazycake.shiro.serializer.StringSerializer; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.Collection; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.Set; 15 | 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.CoreMatchers.*; 18 | import static org.mockito.Mockito.*; 19 | 20 | public class RedisCacheTest { 21 | private IRedisManager redisManager; 22 | private StringSerializer keySerializer = new StringSerializer(); 23 | private ObjectSerializer valueSerializer = new ObjectSerializer(); 24 | 25 | @BeforeEach 26 | public void setUp() { 27 | redisManager = mock(IRedisManager.class); 28 | } 29 | 30 | private RedisCache mountRedisCache() { 31 | return new RedisCache(redisManager, new StringSerializer(), new ObjectSerializer(), "employee:", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME); 32 | } 33 | 34 | @Test 35 | public void testInitialize() { 36 | Assertions.assertThrows(IllegalArgumentException.class, () -> new RedisCache(null, null, null, "abc:", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME)); 37 | Assertions.assertThrows(IllegalArgumentException.class, () -> new RedisCache(new RedisManager(), null, null, "abc:", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME)); 38 | Assertions.assertThrows(IllegalArgumentException.class, () -> new RedisCache(new RedisManager(), new StringSerializer(), null, "abc:", 1, RedisCacheManager.DEFAULT_PRINCIPAL_ID_FIELD_NAME)); 39 | } 40 | 41 | @Test 42 | public void testPut() throws SerializationException { 43 | RedisCache rc = mountRedisCache(); 44 | Object value = rc.put("foo", "bar"); 45 | assertThat(value, is("bar")); 46 | verify(redisManager).set(keySerializer.serialize("employee:foo"), valueSerializer.serialize("bar"), 1); 47 | 48 | PrincipalCollection principal = new EmployeePrincipal(3); 49 | rc.put(principal, "account information"); 50 | 51 | verify(redisManager).set(keySerializer.serialize("employee:3"), valueSerializer.serialize("account information"), 1); 52 | } 53 | } 54 | 55 | class Employee { 56 | private int id; 57 | 58 | public Employee(int id) { 59 | this.id = id; 60 | } 61 | 62 | public int getId() { 63 | return this.id; 64 | } 65 | } 66 | 67 | class EmployeePrincipal implements PrincipalCollection { 68 | 69 | private Employee primaryPrincipal; 70 | 71 | public EmployeePrincipal(int id) { 72 | this.primaryPrincipal = new Employee(id); 73 | } 74 | 75 | @Override 76 | public Employee getPrimaryPrincipal() { 77 | return this.primaryPrincipal; 78 | } 79 | 80 | @Override 81 | public T oneByType(Class aClass) { 82 | return null; 83 | } 84 | 85 | @Override 86 | public Collection byType(Class aClass) { 87 | return null; 88 | } 89 | 90 | @Override 91 | public List asList() { 92 | return null; 93 | } 94 | 95 | @Override 96 | public Set asSet() { 97 | return null; 98 | } 99 | 100 | @Override 101 | public Collection fromRealm(String s) { 102 | return null; 103 | } 104 | 105 | @Override 106 | public Set getRealmNames() { 107 | return null; 108 | } 109 | 110 | @Override 111 | public boolean isEmpty() { 112 | return false; 113 | } 114 | 115 | @Override 116 | public Iterator iterator() { 117 | return null; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/test/java/org/crazycake/shiro/RedisClusterManagerTest.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import redis.clients.jedis.JedisCluster; 6 | 7 | import static org.hamcrest.CoreMatchers.is; 8 | import static org.hamcrest.CoreMatchers.nullValue; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.mockito.Mockito.*; 11 | 12 | public class RedisClusterManagerTest { 13 | 14 | private RedisClusterManager redisClusterManager; 15 | private JedisCluster jedisCluster; 16 | 17 | @BeforeEach 18 | public void setUp() { 19 | jedisCluster = mock(JedisCluster.class); 20 | redisClusterManager = new RedisClusterManager(); 21 | redisClusterManager.setJedisCluster(jedisCluster); 22 | } 23 | 24 | @Test 25 | public void get() { 26 | byte[] value = redisClusterManager.get(null); 27 | assertThat(value, is(nullValue())); 28 | byte[] testKey = "123".getBytes(); 29 | byte[] expectValue = "abc".getBytes(); 30 | when(jedisCluster.get(testKey)).thenReturn(expectValue); 31 | byte[] testValue = redisClusterManager.get(testKey); 32 | assertThat(testValue, is(expectValue)); 33 | } 34 | 35 | @Test 36 | public void set() { 37 | redisClusterManager.set(null, null, -1); 38 | verify(jedisCluster, times(0)).set(any(byte[].class), any(byte[].class)); 39 | redisClusterManager.set(new byte[0], new byte[0], -1); 40 | verify(jedisCluster, times(1)).set(any(byte[].class), any(byte[].class)); 41 | verify(jedisCluster, times(0)).expire(any(byte[].class), any(int.class)); 42 | redisClusterManager.set(new byte[0], new byte[0], 0); 43 | verify(jedisCluster, times(1)).expire(any(byte[].class), any(int.class)); 44 | redisClusterManager.set(new byte[0], new byte[0], 1); 45 | verify(jedisCluster, times(2)).expire(any(byte[].class), any(int.class)); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/org/crazycake/shiro/RedisSessionDAOTest.java: -------------------------------------------------------------------------------- 1 | package org.crazycake.shiro; 2 | 3 | import org.apache.shiro.session.InvalidSessionException; 4 | import org.apache.shiro.session.Session; 5 | import org.crazycake.shiro.exception.SerializationException; 6 | import org.crazycake.shiro.serializer.ObjectSerializer; 7 | import org.crazycake.shiro.serializer.StringSerializer; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.io.Serializable; 12 | import java.util.Collection; 13 | import java.util.Date; 14 | import java.util.HashSet; 15 | import java.util.Set; 16 | 17 | import static org.mockito.Mockito.mock; 18 | import static org.mockito.Mockito.verify; 19 | import static org.mockito.Mockito.when; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.hamcrest.CoreMatchers.*; 22 | 23 | public class RedisSessionDAOTest { 24 | private IRedisManager redisManager; 25 | private StringSerializer keySerializer = new StringSerializer(); 26 | private ObjectSerializer valueSerializer = new ObjectSerializer(); 27 | 28 | @BeforeEach 29 | public void setUp() { 30 | redisManager = mock(IRedisManager.class); 31 | } 32 | 33 | private RedisSessionDAO mountRedisSessionDAO(Integer expire) { 34 | RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); 35 | if (expire != null) { 36 | redisSessionDAO.setExpire(expire); 37 | } 38 | redisSessionDAO.setKeyPrefix("student:"); 39 | redisSessionDAO.setRedisManager(redisManager); 40 | return redisSessionDAO; 41 | } 42 | 43 | @Test 44 | public void testUpdate() throws SerializationException { 45 | RedisSessionDAO sessionDAO = mountRedisSessionDAO(null); 46 | StudentSession session = new StudentSession(99, 2000); 47 | sessionDAO.update(session); 48 | verify(redisManager).set(keySerializer.serialize("student:99"), valueSerializer.serialize(session), 2); 49 | } 50 | 51 | @Test 52 | public void testUpdateByCustomExpire() throws SerializationException { 53 | RedisSessionDAO sessionDAO = mountRedisSessionDAO(3); 54 | StudentSession session = new StudentSession(98, 2000); 55 | sessionDAO.update(session); 56 | verify(redisManager).set(keySerializer.serialize("student:98"), valueSerializer.serialize(session), 3); 57 | } 58 | 59 | @Test 60 | public void testUpdateByNoExpire() throws SerializationException { 61 | RedisSessionDAO sessionDAO = mountRedisSessionDAO(-1); 62 | StudentSession session = new StudentSession(97, 2000); 63 | sessionDAO.update(session); 64 | verify(redisManager).set(keySerializer.serialize("student:97"), valueSerializer.serialize(session), -1); 65 | } 66 | 67 | @Test 68 | public void testDelete() throws SerializationException { 69 | RedisSessionDAO sessionDAO = mountRedisSessionDAO(null); 70 | StudentSession session = new StudentSession(96, 1000); 71 | sessionDAO.delete(session); 72 | verify(redisManager).del(keySerializer.serialize("student:96")); 73 | } 74 | 75 | @Test 76 | public void testGetActiveSessions() throws SerializationException { 77 | Set mockKeys = new HashSet(); 78 | mockKeys.add(keySerializer.serialize("student:1")); 79 | mockKeys.add(keySerializer.serialize("student:2")); 80 | when(redisManager.keys(keySerializer.serialize("student:*"))).thenReturn(mockKeys); 81 | 82 | StudentSession mockSession1 = new StudentSession(1, 2000); 83 | StudentSession mockSession2 = new StudentSession(2, 2000); 84 | when(redisManager.get(keySerializer.serialize("student:1"))).thenReturn(valueSerializer.serialize(mockSession1)); 85 | when(redisManager.get(keySerializer.serialize("student:2"))).thenReturn(valueSerializer.serialize(mockSession2)); 86 | 87 | RedisSessionDAO sessionDAO = mountRedisSessionDAO(null); 88 | assertThat(sessionDAO.getActiveSessions().size(), is(2)); 89 | } 90 | } 91 | 92 | class StudentSession implements Session, Serializable { 93 | private Integer id; 94 | private long timeout; 95 | 96 | public StudentSession(Integer id, long timeout) { 97 | this.id = id; 98 | this.timeout = timeout; 99 | } 100 | 101 | @Override 102 | public Serializable getId() { 103 | return id; 104 | } 105 | 106 | @Override 107 | public Date getStartTimestamp() { 108 | return null; 109 | } 110 | 111 | @Override 112 | public Date getLastAccessTime() { 113 | return null; 114 | } 115 | 116 | @Override 117 | public long getTimeout() throws InvalidSessionException { 118 | return timeout; 119 | } 120 | 121 | @Override 122 | public void setTimeout(long l) throws InvalidSessionException { 123 | 124 | } 125 | 126 | @Override 127 | public String getHost() { 128 | return null; 129 | } 130 | 131 | @Override 132 | public void touch() throws InvalidSessionException { 133 | 134 | } 135 | 136 | @Override 137 | public void stop() throws InvalidSessionException { 138 | 139 | } 140 | 141 | @Override 142 | public Collection getAttributeKeys() throws InvalidSessionException { 143 | return null; 144 | } 145 | 146 | @Override 147 | public Object getAttribute(Object o) throws InvalidSessionException { 148 | return null; 149 | } 150 | 151 | @Override 152 | public void setAttribute(Object o, Object o1) throws InvalidSessionException { 153 | 154 | } 155 | 156 | @Override 157 | public Object removeAttribute(Object o) throws InvalidSessionException { 158 | return null; 159 | } 160 | } 161 | --------------------------------------------------------------------------------