Nepxion Permission Feign

Nepxion Permission is a permission system based on Redis with Nepxion Matrix AOP framework and Aquarius framework

License

License

Categories

Categories

Feign Net HTTP Clients
GroupId

GroupId

com.nepxion
ArtifactId

ArtifactId

permission-feign
Last Version

Last Version

3.0.8
Release Date

Release Date

Type

Type

jar
Description

Description

Nepxion Permission Feign
Nepxion Permission is a permission system based on Redis with Nepxion Matrix AOP framework and Aquarius framework
Project URL

Project URL

http://www.nepxion.com
Project Organization

Project Organization

Nepxion

Download permission-feign

How to add to project

<!-- https://jarcasting.com/artifacts/com.nepxion/permission-feign/ -->
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>permission-feign</artifactId>
    <version>3.0.8</version>
</dependency>
// https://jarcasting.com/artifacts/com.nepxion/permission-feign/
implementation 'com.nepxion:permission-feign:3.0.8'
// https://jarcasting.com/artifacts/com.nepxion/permission-feign/
implementation ("com.nepxion:permission-feign:3.0.8")
'com.nepxion:permission-feign:jar:3.0.8'
<dependency org="com.nepxion" name="permission-feign" rev="3.0.8">
  <artifact name="permission-feign" type="jar" />
</dependency>
@Grapes(
@Grab(group='com.nepxion', module='permission-feign', version='3.0.8')
)
libraryDependencies += "com.nepxion" % "permission-feign" % "3.0.8"
[com.nepxion/permission-feign "3.0.8"]

Dependencies

compile (2)

Group / Artifact Type Version
org.springframework.cloud : spring-cloud-starter-openfeign jar
org.springframework.boot : spring-boot-starter-web jar

Project Modules

There are no modules declared in this project.

Nepxion Permission

Total lines License Maven Central Javadocs Build Status Codacy Badge Stars Stars

Nepxion Permission是一款基于Spring Cloud的微服务API权限框架,并通过Redis分布式缓存进行权限缓存。它采用Nepxion Matrix AOP框架进行切面实现,支持注解调用方式,也支持Rest调用方式

请联系我

微信、钉钉、公众号和文档

简介

注意

Nepxion Permission提供简单易用的AOP框架(参考permission-springcloud-client-example),并非是全面的权限管理和调用系统,鉴于不同公司有不同权限架构,那么使用者需要自行去实现如下模块(参考permission-springcloud-service-example):

  • 实现基于权限-角色-用户三层体系的数据库模型(Pojo类已在permission-entity里实现),并提供相关的增删改查接口
  • 实现基于界面的权限-角色-用户的操作功能
  • 实现和相关用户系统等多对接
  • 实现基于权限验证的分布式缓存功能,例如验证缓存和失效(如果使用者有这样的需求)
  • 实现基于Token的权限验证功能,和相关单点登录系统等做对接(如果使用者有这样的需求)
  • 实现提供UI权限和API GATEWAY权限的接入(如果使用者有这样的需求)

兼容

版本兼容情况

  • Spring Cloud F版,请采用3.x.x版本,具体代码参考master分支
  • Spring Cloud E版,请采用2.x.x版本,具体代码参考2.0.x分支

依赖

AOP依赖

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>permission-aop-starter</artifactId>
    <version>${permission.version}</version>
</dependency>

Feign依赖

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>permission-feign-starter</artifactId>
    <version>${permission.version}</version>
</dependency>

示例

权限服务端

服务端配置

# Spring cloud config
spring.application.name=permission-springcloud-service-example
server.port=4321
eureka.instance.metadataMap.owner=Haojun Ren
eureka.client.serviceUrl.defaultZone=http://localhost:9528/eureka/

# Permission config
# 权限服务开启和关闭,不加这行,视为开启
permission.service.enabled=true

# Datasource config
database.driverClassName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://127.0.0.1:3306/permission?useUnicode=true&amp;characterEncoding=UTF8&amp;zeroDateTimeBehavior=convertToNull&amp;autoReconnect=true&amp;failOverReadOnly=false
database.username=root
database.password=111111
pool.init=10
pool.min=10
pool.max=20
pool.max.wait=60000
pool.time.between.eviction.runs.millis=60000
pool.min.evictable.idle.time.millis=300000
pool.remove.abandoned.timeout=120

# Cache config
prefix=permission
cache.enabled=true
cache.type=redisCache
# 当切面拦截出现异常,如果忽略该异常,则不影响当前业务方法调用,否则中断当前业务方法调用,缺省为true
# cache.aop.exception.ignore=true
# 全局缓存过期值,单位毫秒(小于等于零,表示永不过期),当注解上没配置该值的时候,以全局值为准,缺省为-1
# cache.expire=-1
# 扫描含有@Cacheable,@CacheEvict,@CachePut等注解的接口或者类所在目录
cache.scan.packages=com.nepxion.permission

# Redis config
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0

# Frequent log print
frequent.log.print=true

SpringCloud应用入口,需要加上@EnablePermissionSerivce注解激活权限服务(当然也可以在配置文件里面permission.service.enabled=false关闭它),@EnableCache从缓存获取权限数据

package com.nepxion.permission.example.service;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import com.nepxion.aquarius.cache.annotation.EnableCache;
import com.nepxion.permission.service.annotation.EnablePermissionSerivce;

@SpringBootApplication
@EnableDiscoveryClient
@EnablePermissionSerivce
@EnableCache
public class PermissionApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(PermissionApplication.class).run(args);
    }
}

需要实现permission-api的两个Feign接口PermissionResource和UserResource 模拟实现权限对数据库的相关接口,请自行实现相关和数据库,缓存等操作逻辑

package com.nepxion.permission.service.impl;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.nepxion.permission.api.PermissionResource;
import com.nepxion.permission.entity.PermissionEntity;

// 该接口实现提供给调用端的Feign接口,需要实现的逻辑是权限数据入库,验证,以及缓存的操作
@RestController
public class PermissionResourceImpl implements PermissionResource {
    private static final Logger LOG = LoggerFactory.getLogger(PermissionResourceImpl.class);

    // 权限列表入库
    @Override
    public void persist(@RequestBody List<PermissionEntity> permissions) {
        for (PermissionEntity permission : permissions) {
            permission.validateName();
        }

        // 实现权限扫描结果到数据库的入库
        // 需要注意,权限的重复入库问题,一般遵循“不存在则插入,存在则覆盖”的原则
        LOG.info("权限列表入库:{}", permissions);
    }

    // 权限验证
    @Override
    public boolean authorize(@PathVariable(value = "userId") String userId, @PathVariable(value = "userType") String userType, @PathVariable(value = "permissionName") String permissionName, @PathVariable(value = "permissionType") String permissionType, @PathVariable(value = "serviceName") String serviceName) {
        LOG.info("权限获取: userId={}, userType={}, permissionName={}, permissionType={}, serviceName={}", userId, userType, permissionName, permissionType, serviceName);
        // 验证用户是否有权限
        // 需要和用户系统做对接,userId一般为登录名,userType为用户系统类型。目前支持多用户类型,所以通过userType来区分同名登录用户,例如财务系统有用户叫zhangsan,支付系统也有用户叫zhangsan
        // permissionName即在@Permission注解上定义的name,permissionType为权限类型,目前支持接口权限(API),网关权限(GATEWAY),界面权限(UI)三种类型的权限(参考PermissionType.java类的定义)
        // serviceName即服务名,在application.properties里定义的spring.application.name
        // 对于验证结果,在后端实现分布式缓存,可以避免频繁调用数据库而出现性能问题
        // 示例描述用户zhangsan有权限,用户lisi没权限
        if (StringUtils.equals(userId, "zhangsan")) {
            return true;
        } else if (StringUtils.equals(userId, "lisi")) {
            return false;
        }

        return true;
    }
}
package com.nepxion.permission.service.impl;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.nepxion.permission.api.UserResource;
import com.nepxion.permission.entity.UserEntity;

@RestController
public class UserResourceImpl implements UserResource {
    private static final Logger LOG = LoggerFactory.getLogger(UserResourceImpl.class);

    // 根据Token获取User实体
    @Override
    public UserEntity getUser(@PathVariable(value = "token") String token) {
        // 当前端登录后,它希望送token到后端,查询出用户信息(并以此调用authorize接口做权限验证,permission-aop已经实现,使用者并不需要关心)
        // 需要和单点登录系统,例如OAuth或者JWT等系统做对接
        // 示例描述token为abcd1234对应的用户为lisi
        LOG.info("Token:{}", token);
        if (StringUtils.equals(token, "abcd1234")) {
            UserEntity user = new UserEntity();
            user.setUserId("lisi");
            user.setUserType("LDAP");

            return user;
        }

        return null;
    }
}

模拟业务服务端

业务服务端配置

# Spring cloud config
spring.application.name=permission-springcloud-my-service-example
server.port=1234
eureka.instance.metadataMap.owner=Haojun Ren
eureka.client.serviceUrl.defaultZone=http://10.0.75.1:9528/eureka/

# Ribbon config
ribbon.ReadTimeout=60000
ribbon.ConnectTimeout=60000

# Permission config
# 权限拦截开启和关闭,不加这行,视为开启
permission.enabled=true
# 权限系统的服务名,作为Feign的寻址名
permission.service.name=permission-springcloud-service-example
# 扫描含有@Permission注解的接口或者类所在目录
permission.scan.packages=com.nepxion.permission.example.client.service
# 如果开启,默认每次服务启动时候,会往权限系统的数据库插入权限(权限不存在则插入,权限存在则覆盖)
permission.automatic.persist.enabled=true
# 权限自动入库第一次失败后,还有重试的机会。下面配置项为重试的次数
permission.automatic.persist.retry.times=5
# 权限自动入库第一次失败后,还有重试的机会。下面配置项为每次重试的间隔时间
permission.automatic.persist.retry.interval=10000
# 权限系统验证拦截的用户类型白名单(例如用户类型是LDAP,那么对LDAP的用户做权限验证拦截),多个值以“;”分隔
permission.user.type.whitelist=LDAP

# Cache config
prefix=permission
cache.enabled=true
cache.type=redisCache
# 当切面拦截出现异常,如果忽略该异常,则不影响当前业务方法调用,否则中断当前业务方法调用,缺省为true
# cache.aop.exception.ignore=true
# 全局缓存过期值,单位毫秒(小于等于零,表示永不过期),当注解上没配置该值的时候,以全局值为准,缺省为-1
# cache.expire=-1
# 扫描含有@Cacheable,@CacheEvict,@CachePut等注解的接口或者类所在目录
cache.scan.packages=com.nepxion.permission

# Redis config
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0

# Frequent log print
frequent.log.print=true

SpringCloud应用入口,需要加上@EnablePermission注解激活权限拦截功能(当然也可以在配置文件里面permission.enabled=false关闭它),@EnableCache从缓存获取权限数据

package com.nepxion.permission.example.service;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.ConfigurableApplicationContext;

import com.nepxion.aquarius.cache.annotation.EnableCache;
import com.nepxion.permission.annotation.EnablePermission;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = { "com.nepxion.permission.api" })
@EnablePermission
@EnableCache
public class MyApplication {
    private static final Logger LOG = LoggerFactory.getLogger(MyApplication.class);

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(MyApplication.class, args);

        MyController myController = applicationContext.getBean(MyController.class);
        try {
            LOG.info("Result : {}", myController.doA("zhangsan", "LDAP", "valueA"));
        } catch (Exception e) {
            LOG.error("Error", e);
        }

        try {
            LOG.info("Result : {}", myController.doB("abcd1234", "valueB"));
        } catch (Exception e) {
            LOG.error("Error", e);
        }
    }
}

在RestController添加@Permission注解,实现API权限验证功能

package com.nepxion.permission.example.service;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.nepxion.permission.annotation.Permission;
import com.nepxion.permission.annotation.Token;
import com.nepxion.permission.annotation.UserId;
import com.nepxion.permission.annotation.UserType;

@RestController
public class MyController {
    private static final Logger LOG = LoggerFactory.getLogger(MyController.class);

    // 显式基于UserId和UserType注解的权限验证,参数通过注解传递
    @RequestMapping(path = "/doA/{userId}/{userType}/{value}", method = RequestMethod.GET)
    @Permission(name = "A-Permission", label = "A权限", description = "A权限的描述")
    public int doA(@PathVariable(value = "userId") @UserId String userId, @PathVariable(value = "userType") @UserType String userType, @PathVariable(value = "value") String value) {
        LOG.info("===== doA被调用");

        return 123;
    }

    // 显式基于Token注解的权限验证,参数通过注解传递
    @RequestMapping(path = "/doB/{token}/{value}", method = RequestMethod.GET)
    @Permission(name = "B-Permission", label = "B权限", description = "B权限的描述")
    public String doB(@PathVariable(value = "token") @Token String token, @PathVariable(value = "value") String value) {
        LOG.info("----- doB被调用");

        return "abc";
    }

    // 隐式基于Rest请求的权限验证,参数通过Header传递
    @RequestMapping(path = "/doC/{value}", method = RequestMethod.GET)
    @Permission(name = "C-Permission", label = "C权限", description = "C权限的描述")
    public boolean doC(@PathVariable(value = "value") String value) {
        LOG.info("----- doC被调用");

        return true;
    }
}

模拟业务客户端,基于Feign调用

业务客户端配置

# Spring cloud config
spring.application.name=permission-springcloud-my-client-example
server.port=1212
eureka.client.serviceUrl.defaultZone=http://10.0.75.1:9528/eureka/

# Ribbon config
ribbon.ReadTimeout=60000
ribbon.ConnectTimeout=60000

# Permission config
# 权限Feign拦截开启和关闭,不加这行,视为开启
permission.feign.enabled=true

SpringCloud应用入口,需要加上@EnablePermissionFeign注解激活权限Feign拦截功能(当然也可以在配置文件里面permission.feign.enabled=false关闭它),该注解可以把Rest调用的Header数据传送到后端业务服务来

package com.nepxion.permission.example.client;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

import com.nepxion.permission.feign.annotation.EnablePermissionFeign;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnablePermissionFeign
public class MyApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(MyApplication.class).run(args);
    }
}

基于Feign的调用

package com.nepxion.permission.example.client;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(value = "permission-springcloud-my-service-example")
public interface MyFeign {
    @RequestMapping(path = "/doA/{userId}/{userType}/{value}", method = RequestMethod.GET)
    int doA(@PathVariable(value = "userId") String userId, @PathVariable(value = "userType") String userType, @PathVariable(value = "value") String value);

    @RequestMapping(path = "/doB/{token}/{value}", method = RequestMethod.GET)
    String doB(@PathVariable(value = "token") String token, @PathVariable(value = "value") String value);

    @RequestMapping(path = "/doC/{value}", method = RequestMethod.GET)
    boolean doC(@PathVariable(value = "value") String value);
}
package com.nepxion.permission.example.client;

/**
 * <p>Title: Nepxion Permission</p>
 * <p>Description: Nepxion Permission</p>
 * <p>Copyright: Copyright (c) 2017-2050</p>
 * <p>Company: Nepxion</p>
 * @author Haojun Ren
 * @version 1.0
 */

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    @Autowired
    private MyFeign myFeign;

    @RequestMapping(path = "/doA/{userId}/{userType}/{value}", method = RequestMethod.GET)
    public int doA(@PathVariable(value = "userId") String userId, @PathVariable(value = "userType") String userType, @PathVariable(value = "value") String value) {
        return myFeign.doA(userId, userType, value);
    }

    @RequestMapping(path = "/doB/{token}/{value}", method = RequestMethod.GET)
    public String doB(@PathVariable(value = "token") String token, @PathVariable(value = "value") String value) {
        return myFeign.doB(token, value);
    }

    @RequestMapping(path = "/doC/{value}", method = RequestMethod.GET)
    public boolean doC(@PathVariable(value = "value") String value) {
        return myFeign.doC(value);
    }
}

基于注解调用结果

permission 2018-01-18 17:18:33,382 INFO [main] c.n.p.a.PermissionInterceptor [PermissionInterceptor.java:103] - Intercepted for annotation - Permission [name=A-Permission, label=A权限, description=, proxyType=Reflective Aop Proxy, proxiedClass=com.nepxion.permission.service.MyServiceImpl, method=doA]
permission 2018-01-18 17:18:33,442 INFO [main] c.n.a.c.a.CacheInterceptor [CacheInterceptor.java:120] - Intercepted for annotation - Cacheable [key=permission_cache_zhangsan_LDAP_A-Permission_SERVICE_permission-springcloud-example, expire=-1, proxyType=Cglib Aop Proxy, proxiedClass=com.nepxion.permission.aop.PermissionAuthorization, method=authorizeCache]
permission 2018-01-18 17:18:33,582 INFO [main] c.n.a.c.r.i.RedisCacheDelegateImpl [RedisCacheDelegateImpl.java:54] - Before invocation, Cacheable key=permission_cache_zhangsan_LDAP_A-Permission_SERVICE_permission-springcloud-example, cache=true in Redis
permission 2018-01-18 17:18:33,582 INFO [main] c.n.p.s.MyServiceImpl [MyServiceImpl.java:22] - ===== doA被调用
permission 2018-01-18 17:18:33,582 INFO [main] c.n.p.MyApplication [MyApplication.java:30] - Result : 123
permission 2018-01-18 17:18:33,582 INFO [main] c.n.p.a.PermissionInterceptor [PermissionInterceptor.java:103] - Intercepted for annotation - Permission [name=B-Permission, label=B权限, description=B权限的描述, proxyType=Reflective Aop Proxy, proxiedClass=com.nepxion.permission.service.MyServiceImpl, method=doB]
permission 2018-01-18 17:18:33,584 INFO [main] c.n.a.c.a.CacheInterceptor [CacheInterceptor.java:120] - Intercepted for annotation - Cacheable [key=permission_cache_lisi_LDAP_B-Permission_SERVICE_permission-springcloud-example, expire=-1, proxyType=Cglib Aop Proxy, proxiedClass=com.nepxion.permission.aop.PermissionAuthorization, method=authorizeCache]
permission 2018-01-18 17:18:33,585 INFO [main] c.n.a.c.r.i.RedisCacheDelegateImpl [RedisCacheDelegateImpl.java:54] - Before invocation, Cacheable key=permission_cache_lisi_LDAP_B-Permission_SERVICE_permission-springcloud-example, cache=false in Redis
Exception in thread "main" com.nepxion.permission.exception.PermissionException: No permision to proceed method [name=doB, parameterTypes=java.lang.String,java.lang.String], permissionName=B-Permission, permissionLabel=B权限
	at com.nepxion.permission.aop.PermissionInterceptor.invokePermission(PermissionInterceptor.java:131)
	at com.nepxion.permission.aop.PermissionInterceptor.invoke(PermissionInterceptor.java:73)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
	at com.sun.proxy.$Proxy64.doB(Unknown Source)
	at com.nepxion.permission.MyApplication.main(MyApplication.java:31)

基于Rest调用结果

基于UserId和UserType的权限验证。如图所示,该用户对该API有权限

基于Token的权限验证。如图所示,该Token对应的用户对该API无权限

Star走势图

Stargazers over time

com.nepxion

Nepxion

基于 Spring & Spring Boot & Spring Cloud 框架,依托和集成 阿里巴巴中间件 和 Spring Cloud Alibaba 等技术栈,企业级云原生微服务的开源解决方案。微信 : 1394997

Versions

Version
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.0.17
2.0.16
2.0.15
2.0.14
2.0.13
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1