diff --git a/pom.xml b/pom.xml index 6cdbeb88..6d7b000f 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,8 @@ sop-test sop-sdk sop-website - + sop-openapi + 1.8 diff --git a/sop-gateway/pom.xml b/sop-gateway/pom.xml index 385e8a82..dc4ad9b2 100644 --- a/sop-gateway/pom.xml +++ b/sop-gateway/pom.xml @@ -67,6 +67,16 @@ 1.18.4 provided + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.apache.commons + commons-lang3 + diff --git a/sop-gateway/src/main/java/com/gitee/sop/gateway/config/MyConfig.java b/sop-gateway/src/main/java/com/gitee/sop/gateway/config/MyConfig.java index 9d32bba9..d9d0475a 100644 --- a/sop-gateway/src/main/java/com/gitee/sop/gateway/config/MyConfig.java +++ b/sop-gateway/src/main/java/com/gitee/sop/gateway/config/MyConfig.java @@ -1,23 +1,48 @@ package com.gitee.sop.gateway.config; +import com.alibaba.fastjson.JSONObject; import com.gitee.sop.gatewaycommon.bean.ApiConfig; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +@Slf4j @Component public class MyConfig { + @Autowired + private StringRedisTemplate redisTemplate; + private static String APP_AUTH_TOKEN_PREFIX = "com.gitee.sop.oauth2_access_token:"; + + public static String getAccessTokenKey(String accessToken) { + return APP_AUTH_TOKEN_PREFIX + accessToken; + } + + @PostConstruct public void after() { ApiConfig.getInstance().setTokenValidator(apiParam -> { // 获取客户端传递过来的token - String token = apiParam.fetchAccessToken(); - return !StringUtils.isBlank(token); - // TODO: 校验token有效性,可以从redis中读取 + try { + String token = apiParam.fetchAccessToken(); + if (StringUtils.isBlank(token)) { + return Boolean.FALSE; + } + String json = redisTemplate.opsForValue().get(getAccessTokenKey(token)); + if(StringUtils.isEmpty(json)) { + return Boolean.FALSE; + } - // 返回true表示这个token真实、有效 + JSONObject userInfo = JSONObject.parseObject(json); + return userInfo != null; + } catch (Exception e) { + log.error("redisError", e); + return Boolean.FALSE; + } }); } } \ No newline at end of file diff --git a/sop-gateway/src/main/resources/application-dev.properties b/sop-gateway/src/main/resources/application-dev.properties index 0ba27c4c..e0420b8f 100644 --- a/sop-gateway/src/main/resources/application-dev.properties +++ b/sop-gateway/src/main/resources/application-dev.properties @@ -13,4 +13,14 @@ register.url=127.0.0.1:8848 # 如果需要传输大文本可以把值设置高一点 spring.codec.max-in-memory-size=262144 -logging.level.com.gitee=debug \ No newline at end of file +logging.level.com.gitee=debug + +spring.redis.host=localhost +spring.redis.port=6379 +spring.redis.jedis.pool.min-idle=1 +spring.redis.jedis.pool.max-idle=5 +spring.redis.jedis.pool.max-wait=-1 +spring.redis.jedis.pool.max-active=8 +spring.redis.timeout=60000 +spring.redis.database=0 +spring.redis.password= \ No newline at end of file diff --git a/sop-openapi/pom.xml b/sop-openapi/pom.xml new file mode 100644 index 00000000..c231dc6c --- /dev/null +++ b/sop-openapi/pom.xml @@ -0,0 +1,132 @@ + + + + 4.0.0 + + com.gitee.sop + sop-parent + 4.1.0-SNAPSHOT + ../pom.xml + + com.gitee.sop.openapi + sop-openapi + sop-openapi + + + 1.1.0 + + + + + com.gitee.sop + sop-service-common + 4.1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + com.github.xiaoymin + swagger-bootstrap-ui + 1.9.5 + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.apache.commons + commons-lang3 + + + + mysql + mysql-connector-java + runtime + + + + net.oschina.durcframework + fastmybatis-spring-boot-starter + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.ctrip.framework.apollo + apollo-client + ${apollo.version} + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + true + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/SopOpenapiApplication.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/SopOpenapiApplication.java new file mode 100644 index 00000000..29675788 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/SopOpenapiApplication.java @@ -0,0 +1,14 @@ +package com.gitee.sop.openapi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@EnableDiscoveryClient +@SpringBootApplication +public class SopOpenapiApplication { + + public static void main(String[] args) { + SpringApplication.run(SopOpenapiApplication.class, args); + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/config/ApolloApplicationConfig.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/ApolloApplicationConfig.java new file mode 100644 index 00000000..4b3adc8c --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/ApolloApplicationConfig.java @@ -0,0 +1,41 @@ +package com.gitee.sop.openapi.config; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.model.ConfigChange; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +/*** + * + * @author wangping created on 2020/11/5 16:06 + */ +@Slf4j +@Component +public class ApolloApplicationConfig implements BeanPostProcessor { + + + @PostConstruct + public void init() { + loadApplication(); + } + + private void loadApplication() { + Config config = ConfigService.getAppConfig(); + config.addChangeListener(this::getChangedEvent); + } + + + private void getChangedEvent(ConfigChangeEvent changeEvent) { + for (String key : changeEvent.changedKeys()) { + ConfigChange change = changeEvent.getChange(key); + log.info("Found change - key: {}, oldValue: {}, newValue: {}, changeType:{}", + change.getPropertyName(), change.getOldValue(), + change.getNewValue(), change.getChangeType()); + } + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/config/OpenapiConfig.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/OpenapiConfig.java new file mode 100644 index 00000000..a0a597fa --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/OpenapiConfig.java @@ -0,0 +1,33 @@ +package com.gitee.sop.openapi.config; + +import com.gitee.sop.servercommon.bean.ServiceConfig; +import com.gitee.sop.servercommon.swagger.SwaggerSupport; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +public class OpenapiConfig { + + static { + ServiceConfig.getInstance().getI18nModules().add("i18n/isp/auth_error"); + } + + + /** + * 开启文档,本地微服务文档地址:http://localhost:2222/doc.html + * http://ip:port/v2/api-docs + */ + @Configuration + @EnableSwagger2 + public static class Swagger2 extends SwaggerSupport { + @Override + protected String getDocTitle() { + return "开放平台API"; + } + + @Override + protected boolean swaggerAccessProtected() { + return false; + } + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/config/RedisConfig.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/RedisConfig.java new file mode 100644 index 00000000..84b6b23b --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/RedisConfig.java @@ -0,0 +1,43 @@ +package com.gitee.sop.openapi.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + + @Bean + public RedisTemplate redisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory) { + + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + jackson2JsonRedisSerializer.setObjectMapper(objectMapper); + + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + + // key采用String的序列化方式 + redisTemplate.setKeySerializer(stringRedisSerializer); + // hash的key也采用String的序列化方式 + redisTemplate.setHashKeySerializer(stringRedisSerializer); + // value采用jackson序列化方式 + redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); + // hash的value采用jackson序列化方式 + redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/config/RedisOperator.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/RedisOperator.java new file mode 100644 index 00000000..862b0afa --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/config/RedisOperator.java @@ -0,0 +1,80 @@ +package com.gitee.sop.openapi.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/*** + * + * @author wangping created on 2020/11/2 17:22 + */ +@Slf4j +@Component +public class RedisOperator { + + private final RedisTemplate redisTemplate; + + public RedisOperator(final RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public boolean expire(String key, long time) { + try { + if (time >= 0) { + redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + return true; + } catch (Exception e) { + log.error("redisTemplate.expire.Error", e); + return false; + } + } + + + public long getExpire(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + public Object get(String key) { + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + public boolean set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + return true; + } catch (Exception e) { + log.error("redisTemplate.set.Error", e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error("redisTemplate.setAndExpire.Error", e); + return false; + } + } + + + public boolean del(String key) { + return redisTemplate.delete(key); + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/controller/AuthCodeController.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/controller/AuthCodeController.java new file mode 100644 index 00000000..323ba832 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/controller/AuthCodeController.java @@ -0,0 +1,68 @@ +package com.gitee.sop.openapi.controller; + +import com.gitee.sop.openapi.config.RedisOperator; +import com.gitee.sop.openapi.po.AuthCodePo; +import com.gitee.sop.openapi.vo.AuthCodeVo; +import com.gitee.sop.openapi.service.AuthCodeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotBlank; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RestController +@Api(tags = "超浏授权接口") +public class AuthCodeController { + + + private final AuthCodeService authCodeService; + private final RedisOperator redisOperator; + + + @Value("${test.redis}") + private String testRedis; + + public void setTestRedis(String testRedis) { + this.testRedis = testRedis; + } + + public String getTestRedis() { + return testRedis; + } + + public AuthCodeController(final AuthCodeService authCodeService, final RedisOperator redisOperator) { + this.authCodeService = authCodeService; + this.redisOperator = redisOperator; + } + + + @ApiOperation(value = "获取code") + @PostMapping("/auth/code") + public AuthCodeVo getCode(@RequestBody AuthCodePo authCodePo) { + return authCodeService.getCode(authCodePo); + } + + + @ApiOperation(value = "解除授权") + @PostMapping("/auth/cancel") + public void cancelToken(@NotBlank(message = "{isp.auth.user_id_empty_error}") Long userId) { + authCodeService.cancelToken(userId); + } + + + @ApiOperation(value = "测试接口") + @GetMapping("/test") + public Map test() { + Map map = new HashMap<>(); + map.put("code", 0); + map.put("msg", "success"); + redisOperator.set("test11", testRedis); + return map; + } + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/controller/AuthTokenController.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/controller/AuthTokenController.java new file mode 100644 index 00000000..68330c9e --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/controller/AuthTokenController.java @@ -0,0 +1,27 @@ +package com.gitee.sop.openapi.controller; + +import com.gitee.sop.openapi.po.AuthTokePo; +import com.gitee.sop.servercommon.annotation.Open; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@Api(tags = "插件应用获取token接口") +public class AuthTokenController { + + + + @Open(value = "auth.get.token", ignoreValidate = true, mergeResult = false, needToken = false) + @PostMapping(value = "/token") + @ApiOperation(value = "code换token", notes = "插件服务器获取token") + public String getToken(@RequestBody AuthTokePo tokePo) { + + + return ""; + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/BaseEntity.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/BaseEntity.java new file mode 100644 index 00000000..86d61b0d --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/BaseEntity.java @@ -0,0 +1,61 @@ +package com.gitee.sop.openapi.entity; + +import com.google.common.base.MoreObjects; +import lombok.Data; + +import javax.persistence.*; +import java.util.Date; + +/*** + * + * @author wangping created on 2020/11/4 18:59 + */ +@Data +@MappedSuperclass +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +public abstract class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "gmt_create", nullable = false) + private Date gmtCreate; + + @Column(name = "gmt_modified") + private Date gmtModified; + + @PrePersist + protected void prePersist() { + if (this.gmtCreate == null) { + gmtCreate = new Date(); + } + if (this.gmtModified == null) { + gmtModified = new Date(); + } + } + + @PreUpdate + protected void preUpdate() { + this.gmtModified = new Date(); + } + + @PreRemove + protected void preRemove() { + this.gmtModified = new Date(); + } + + protected MoreObjects.ToStringHelper toStringHelper() { + return MoreObjects.toStringHelper(this) + .omitNullValues() + .add("id", id) + .add("gmtCreate", gmtCreate) + .add("gmtModified", gmtModified); + } + + @Override + public String toString() { + return toStringHelper().toString(); + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/IsvAuthTokenEntity.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/IsvAuthTokenEntity.java new file mode 100644 index 00000000..2058f25b --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/IsvAuthTokenEntity.java @@ -0,0 +1,42 @@ +package com.gitee.sop.openapi.entity; + +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/*** + * + * @author wangping created on 2020/11/2 16:57 + */ + +@Data +@Entity +@Table(name = "isv_auth_token") +public class IsvAuthTokenEntity extends BaseEntity { + + /** appKey, 数据库字段:app_key */ + @Column(name = "app_key") + private String appKey; + + /** userId, 数据库字段:user_id */ + @Column(name = "user_id") + private Long userId; + + /** 1启用,2禁用, 数据库字段:status */ + @Column(name = "status", columnDefinition = "Bit default '1'") + private Boolean status = true; + + /** appAuthCode 数据库字段:app_auth_code */ + @Column(name = "app_auth_code") + private String appAuthCode; + + /** appAuthToken 数据库字段:app_auth_token */ + @Column(name = "app_auth_token") + private String appAuthToken; + + /** appRefreshToken 数据库字段:app_refresh_token */ + @Column(name = "app_refresh_token") + private String appRefreshToken; +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/IsvInfoEntity.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/IsvInfoEntity.java new file mode 100644 index 00000000..81eb2761 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/entity/IsvInfoEntity.java @@ -0,0 +1,33 @@ +package com.gitee.sop.openapi.entity; + +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/*** + * + * @author wangping created on 2020/11/2 16:22 + */ +@Data +@Entity +@Table(name = "isv_info") +public class IsvInfoEntity extends BaseEntity { + + /** appKey, 数据库字段:app_key */ + @Column(name = "app_key") + private String appKey; + + /** 1启用,2禁用, 数据库字段:status */ + @Column(name = "status") + private Byte status; + + /** 1:RSA2,2:MD5 */ + @Column(name = "sign_type") + private Byte signType; + + @Column(name = "remark") + private String remark; + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/po/AuthCodePo.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/po/AuthCodePo.java new file mode 100644 index 00000000..54211c81 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/po/AuthCodePo.java @@ -0,0 +1,26 @@ +package com.gitee.sop.openapi.po; + +import com.gitee.sop.servercommon.param.validation.Group1; +import com.gitee.sop.servercommon.param.validation.Group2; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + + +@Data +public class AuthCodePo { + + + @ApiModelProperty(value = "应用ID", required = true, example = "test1", position = 1) + @NotBlank(message = "{isp.auth.app_id_empty_error}", groups = Group1.class) + @Length(min = 1, max = 64, message = "{isp.auth.app_id_length_error}=1,64", groups = Group2.class) + private String appId; + + @ApiModelProperty(value = "用户ID", required = true, example = "1", position = 2) + @NotNull(message = "{isp.auth.user_id_empty_error}") + private Long userId; + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/po/AuthTokePo.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/po/AuthTokePo.java new file mode 100644 index 00000000..e2d88e23 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/po/AuthTokePo.java @@ -0,0 +1,45 @@ +package com.gitee.sop.openapi.po; + +import com.gitee.sop.servercommon.param.validation.Group1; +import com.gitee.sop.servercommon.param.validation.Group2; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; + +/*** + * + * @author wangping created on 2020/11/2 18:31 + */ + +@Data +public class AuthTokePo { + + + + /** + * 授权码.与refresh_token二选一,用户对应用授权后得到,即第一步中开发者获取到的app_auth_code值 + */ + @ApiModelProperty(value = "应用ID", example = "test1", position = 1) + @NotBlank(message = "{isp.auth.app_id_empty_error}", groups = Group1.class) + @Length(min = 1, max = 64, message = "{isp.auth.app_id_length_error}=1,64", groups = Group2.class) + private String code; + + /** + * 授权类型。
+ * 如果使用app_auth_code换取token,则为authorization_code, + * 如果使用refresh_token换取新的token,则为refresh_token + */ + @NotBlank(message = "{isp.auth.user_token_empty_error}", groups = Group1.class) + @Length(min = 1, max = 64, message = "{isp.auth.user_token_length_error}=1,64", groups = Group2.class) + @ApiModelProperty(value = "授权类型不能为空", required = true, example = "1111", position = 2) + private String grantType; + + + /** + * 刷新令牌.与code二选一,可为空,刷新令牌时使用 + */ + private String refreshToken; + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/repository/IsvAuthTokenRepository.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/repository/IsvAuthTokenRepository.java new file mode 100644 index 00000000..534b806f --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/repository/IsvAuthTokenRepository.java @@ -0,0 +1,17 @@ +package com.gitee.sop.openapi.repository; + +import com.gitee.sop.openapi.entity.IsvAuthTokenEntity; +import org.springframework.data.repository.PagingAndSortingRepository; + +/*** + * + * @author wangping created on 2020/11/4 19:12 + */ +public interface IsvAuthTokenRepository extends PagingAndSortingRepository { + + IsvAuthTokenEntity findFirstByAppKeyAndUserIdAndStatusTrue(String appKey, Long userId); + + IsvAuthTokenEntity findFirstByUserIdAndStatusTrue(Long userId); + + IsvAuthTokenEntity findFirstByAppAuthCode(String appAuthCode); +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/repository/IsvInfoRepository.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/repository/IsvInfoRepository.java new file mode 100644 index 00000000..218915b2 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/repository/IsvInfoRepository.java @@ -0,0 +1,14 @@ +package com.gitee.sop.openapi.repository; + +import com.gitee.sop.openapi.entity.IsvInfoEntity; +import org.springframework.data.repository.PagingAndSortingRepository; + +/*** + * + * @author wangping created on 2020/11/4 19:11 + */ +public interface IsvInfoRepository extends PagingAndSortingRepository { + + IsvInfoEntity findFirstByAppKey(String appKey); + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthCodeService.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthCodeService.java new file mode 100644 index 00000000..1daa402e --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthCodeService.java @@ -0,0 +1,106 @@ +package com.gitee.sop.openapi.service; + +import com.alibaba.fastjson.JSONObject; +import com.gitee.fastmybatis.core.query.Query; +import com.gitee.sop.openapi.config.RedisOperator; +import com.gitee.sop.openapi.entity.IsvAuthTokenEntity; +import com.gitee.sop.openapi.entity.IsvInfoEntity; +import com.gitee.sop.openapi.po.AuthCodePo; +import com.gitee.sop.openapi.repository.IsvAuthTokenRepository; +import com.gitee.sop.openapi.repository.IsvInfoRepository; +import com.gitee.sop.openapi.vo.AuthCodeVo; +import com.gitee.sop.openapi.util.MD5GeneratorUtil; +import com.gitee.sop.openapi.util.RedisKeyUtil; +import com.gitee.sop.servercommon.exception.ServiceException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/*** + * + * @author wangping created on 2020/11/2 16:28 + */ +@Slf4j +@Service +public class AuthCodeService implements AuthService { + + private final IsvInfoRepository isvInfoRepository; + private final IsvAuthTokenRepository isvAuthTokenRepository; + private final RedisOperator redisOperator; + + public AuthCodeService(final IsvInfoRepository isvInfoRepository, + final IsvAuthTokenRepository isvAuthTokenRepository, + final RedisOperator redisOperator) { + this.isvInfoRepository = isvInfoRepository; + this.isvAuthTokenRepository = isvAuthTokenRepository; + this.redisOperator = redisOperator; + } + + + @Transactional + public AuthCodeVo getCode(AuthCodePo authCodePo) { + + IsvInfoEntity isvInfo = isvInfoRepository.findFirstByAppKey(authCodePo.getAppId()); + if (isvInfo == null || isvInfo.getStatus().intValue() == FORBIDDEN) { + log.error("appId已禁用:{}", authCodePo.getAppId()); + throw new ServiceException("应用ID未申请或已被禁用"); + } + + String authCode = MD5GeneratorUtil.generateValue(); + + IsvAuthTokenEntity authTokenEntity = isvAuthTokenRepository.findFirstByAppKeyAndUserIdAndStatusTrue(authCodePo.getAppId(), authCodePo.getUserId()); + AuthCodeVo authCodeVo = new AuthCodeVo(); + authCodeVo.setCode(authCode); + if (authTokenEntity == null) { + authCodeVo.setState(UNAUTHORIZED); + authTokenEntity = new IsvAuthTokenEntity(); + authTokenEntity.setAppAuthCode(authCode); + authTokenEntity.setAppKey(authCodePo.getAppId()); + authTokenEntity.setUserId(authCodePo.getUserId()); + } + authTokenEntity.setAppAuthCode(authCode); + isvAuthTokenRepository.save(authTokenEntity); + + // 写入redis缓存 + try { + String codeKey = RedisKeyUtil.getCodeKey(authCode); + redisOperator.set(codeKey, JSONObject.toJSON(authCodePo), CODE_TIMEOUT); + } catch (Exception e) { + throw new ServiceException("获取code失败"); + } + + return authCodeVo; + } + + + @Transactional + public void cancelToken(Long userId) { + // todo check userToken and get userId + + Query query = new Query() + .eq("user_id", userId) + .eq("status", ENABLE_TOKEN); + + IsvAuthTokenEntity authTokenEntity = isvAuthTokenRepository.findFirstByUserIdAndStatusTrue(userId); + if (authTokenEntity == null) { + throw new ServiceException("用户未授权插件应用"); + } + + try { + authTokenEntity.setStatus(true); + isvAuthTokenRepository.save(authTokenEntity); + } catch (Exception e) { + throw new ServiceException("解除授权失败,系统忙,请稍后!"); + } + + String tokenKey = RedisKeyUtil.getAuthTokenKey(authTokenEntity.getAppAuthToken()); + String refreshKey = RedisKeyUtil.getRefreshTokenKey(authTokenEntity.getAppRefreshToken()); + try { + redisOperator.del(tokenKey); + redisOperator.del(refreshKey); + } catch (Exception e) { + throw new ServiceException("解除授权失败,系统忙,请稍后!"); + } + + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthService.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthService.java new file mode 100644 index 00000000..28102e8b --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthService.java @@ -0,0 +1,20 @@ +package com.gitee.sop.openapi.service; + +/*** + * + * @author wangping created on 2020/11/4 15:41 + */ +public interface AuthService { + + int FORBIDDEN = 2; + + /** 0 插件应用未授权 **/ + int UNAUTHORIZED = 0; + /** 1 插件应用已授权 **/ + int AUTHORIZED = 1; + + Byte ENABLE_TOKEN = 1; + Byte DISABLE_TOKEN = 0; + + Long CODE_TIMEOUT = 3600L; +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthTokenService.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthTokenService.java new file mode 100644 index 00000000..f3a2c514 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/service/AuthTokenService.java @@ -0,0 +1,83 @@ +package com.gitee.sop.openapi.service; + +import com.alibaba.fastjson.JSONObject; +import com.gitee.sop.openapi.config.RedisOperator; +import com.gitee.sop.openapi.entity.IsvAuthTokenEntity; +import com.gitee.sop.openapi.po.AuthCodePo; +import com.gitee.sop.openapi.po.AuthTokePo; +import com.gitee.sop.openapi.repository.IsvAuthTokenRepository; +import com.gitee.sop.openapi.util.GrantType; +import com.gitee.sop.openapi.util.MD5GeneratorUtil; +import com.gitee.sop.openapi.util.RedisKeyUtil; +import com.gitee.sop.openapi.vo.AuthTokenVo; +import com.gitee.sop.servercommon.exception.ServiceException; +import org.springframework.stereotype.Service; + +/*** + * + * @author wangping created on 2020/11/4 15:21 + */ +@Service +public class AuthTokenService implements AuthService { + + private final RedisOperator redisOperator; + private final IsvAuthTokenRepository isvAuthTokenRepository; + + public AuthTokenService(final RedisOperator redisOperator, + final IsvAuthTokenRepository isvAuthTokenRepository) { + this.redisOperator = redisOperator; + this.isvAuthTokenRepository = isvAuthTokenRepository; + } + + + public AuthTokenVo getToken(AuthTokePo authTokePo) { + + AuthTokenVo tokenVo = new AuthTokenVo(); + String grantType = authTokePo.getGrantType(); + if (GrantType.AUTHORIZATION_CODE.name().equals(grantType)) { + String codeKey = RedisKeyUtil.getCodeKey(authTokePo.getCode()); + Object jsonObj = redisOperator.get(codeKey); + if (jsonObj == null) { + throw new ServiceException("Invalid request"); + } + + AuthCodePo codePo = JSONObject.parseObject(jsonObj.toString(), AuthCodePo.class); + if (codePo == null) { + throw new ServiceException("Can not found user by code"); + } + IsvAuthTokenEntity authTokenEntity = isvAuthTokenRepository.findFirstByAppAuthCode(authTokePo.getCode()); + if (authTokenEntity == null) { + throw new ServiceException("Can not found user by code"); + } + + + String authToken = MD5GeneratorUtil.generateValue(); + String refreshToken = MD5GeneratorUtil.generateValue(); + + + authTokenEntity.setStatus(true); + authTokenEntity.setAppAuthToken(authToken); + authTokenEntity.setAppRefreshToken(refreshToken); + isvAuthTokenRepository.save(authTokenEntity); + + String authTokenKey = RedisKeyUtil.getAuthTokenKey(authToken); + String refreshTokenKey = RedisKeyUtil.getRefreshTokenKey(refreshToken); + redisOperator.set(authTokenKey, codePo); + redisOperator.set(refreshTokenKey, codePo); + + tokenVo.setAppAuthToken(authToken); + tokenVo.setAppRefreshToken(refreshToken); + tokenVo.setUserId(codePo.getUserId()); + return tokenVo; + } else if (GrantType.REFRESH_TOKEN.name().equals(grantType)) { + + + return tokenVo; + } else { + throw new ServiceException("Invalid grant"); + } + + } + + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/util/AuthErrorEnum.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/AuthErrorEnum.java new file mode 100644 index 00000000..208eed5d --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/AuthErrorEnum.java @@ -0,0 +1,10 @@ +package com.gitee.sop.openapi.util; + +/*** + * + * @author wangping created on 2020/11/2 16:42 + */ +public enum AuthErrorEnum { + + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/util/GrantType.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/GrantType.java new file mode 100644 index 00000000..de8949ee --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/GrantType.java @@ -0,0 +1,23 @@ +package com.gitee.sop.openapi.util; + +/*** + * + * @author wangping created on 2020/11/4 15:25 + */ +public enum GrantType { + + AUTHORIZATION_CODE("authorization_code"), + PASSWORD("password"), + REFRESH_TOKEN("refresh_token"), + CLIENT_CREDENTIALS("client_credentials"); + + private String grantType; + + private GrantType(String grantType) { + this.grantType = grantType; + } + + public String toString() { + return this.grantType; + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/util/LogFormatUtil.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/LogFormatUtil.java new file mode 100644 index 00000000..001d729c --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/LogFormatUtil.java @@ -0,0 +1,27 @@ +package com.gitee.sop.openapi.util; + +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MessageFormatter; + +/*** + * + * @author wangping created on 2020/11/2 18:14 + */ +public class LogFormatUtil { + + /** + * append log with pattern + * + * @param appendLogPattern like "aaa {} bbb {} ccc" + * @param appendLogArguments like "111, true" + */ + public static String log(String appendLogPattern, Object ... appendLogArguments) { + FormattingTuple ft = MessageFormatter.arrayFormat(appendLogPattern, appendLogArguments); + return ft.getMessage(); + } + + + public static void main(String[] args) { + System.out.println(log("My name is {}, from {}", "hanmeimei", "London")); + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/util/MD5GeneratorUtil.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/MD5GeneratorUtil.java new file mode 100644 index 00000000..ff244a5f --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/MD5GeneratorUtil.java @@ -0,0 +1,53 @@ +package com.gitee.sop.openapi.util; + +import com.gitee.sop.servercommon.exception.ServiceException; + +import java.security.MessageDigest; +import java.util.UUID; + +/*** + * + * @author wangping created on 2020/11/2 16:47 + */ +public class MD5GeneratorUtil { + + private static final char[] hexCode = "0123456789abcdef".toCharArray(); + + private MD5GeneratorUtil() { + } + + public static String generateValue() throws ServiceException { + return generateValue(UUID.randomUUID().toString()); + } + + public static String toHexString(byte[] data) { + if (data == null) { + return null; + } else { + StringBuilder r = new StringBuilder(data.length * 2); + byte[] arr$ = data; + int len$ = data.length; + + for (int i$ = 0; i$ < len$; ++i$) { + byte b = arr$[i$]; + r.append(hexCode[b >> 4 & 15]); + r.append(hexCode[b & 15]); + } + + return r.toString(); + } + } + + public static String generateValue(String param) throws ServiceException { + try { + MessageDigest algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(param.getBytes()); + byte[] messageDigest = algorithm.digest(); + return toHexString(messageDigest); + } catch (Exception var4) { + throw new ServiceException("OAuth Token cannot be generated."); + } + } + +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/util/RedisKeyUtil.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/RedisKeyUtil.java new file mode 100644 index 00000000..19bab21c --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/util/RedisKeyUtil.java @@ -0,0 +1,30 @@ +package com.gitee.sop.openapi.util; + +import com.google.common.base.Joiner; + +/*** + * + * @author wangping created on 2020/11/2 17:47 + */ +public class RedisKeyUtil { + + public static String CODE_PREFIX = "sop:app_auth_code"; + public static String AUTH_TOKEN_PREFIX = "sop:app_auth_token"; + public static String REFRESH_TOKEN_PREFIX = "sop:app_refresh_token"; + + + private static final Joiner JOINER = Joiner.on(":"); + + + public static String getCodeKey(String code) { + return JOINER.join(CODE_PREFIX, code); + } + + public static String getAuthTokenKey(String token) { + return JOINER.join(AUTH_TOKEN_PREFIX, token); + } + + public static String getRefreshTokenKey(String refreshToken) { + return JOINER.join(REFRESH_TOKEN_PREFIX, refreshToken); + } +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/vo/AuthCodeVo.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/vo/AuthCodeVo.java new file mode 100644 index 00000000..410e48cc --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/vo/AuthCodeVo.java @@ -0,0 +1,9 @@ +package com.gitee.sop.openapi.vo; + +import lombok.Data; + +@Data +public class AuthCodeVo { + private String code; + private Integer state; +} diff --git a/sop-openapi/src/main/java/com/gitee/sop/openapi/vo/AuthTokenVo.java b/sop-openapi/src/main/java/com/gitee/sop/openapi/vo/AuthTokenVo.java new file mode 100644 index 00000000..c8476430 --- /dev/null +++ b/sop-openapi/src/main/java/com/gitee/sop/openapi/vo/AuthTokenVo.java @@ -0,0 +1,40 @@ +package com.gitee.sop.openapi.vo; + +import lombok.Data; + +/*** + * + * @author wangping created on 2020/11/4 15:22 + */ +@Data +public class AuthTokenVo { + + /** + * 授权令牌 + */ + private String appAuthToken; + + /** + * 用户id + */ + private Long userId; + + + /** + * 令牌有效期. -1:永久有效 + */ + private long expiresIn = -1; + + /** + * 刷新令牌有效期. -1:永久有效 + */ + private long reExpiresIn = -1; + + + /** + * 刷新令牌时使用。 + * 刷新令牌后,我们会保证老的app_auth_token从刷新开始10分钟内可继续使用,请及时替换为最新token + */ + private String appRefreshToken; + +} diff --git a/sop-openapi/src/main/resources/META-INF/app.properties b/sop-openapi/src/main/resources/META-INF/app.properties new file mode 100644 index 00000000..596945c0 --- /dev/null +++ b/sop-openapi/src/main/resources/META-INF/app.properties @@ -0,0 +1,2 @@ +app.id=open-apis +jdkVersion=1.8 diff --git a/sop-openapi/src/main/resources/application-cluster.yml b/sop-openapi/src/main/resources/application-cluster.yml new file mode 100644 index 00000000..8f894f6b --- /dev/null +++ b/sop-openapi/src/main/resources/application-cluster.yml @@ -0,0 +1,16 @@ +spring: + redis: + cluster: + nodes: + - 10.1.11.46:7000 + - 10.1.11.46:7001 + - 10.1.11.46:7002 + jedis: + pool: + max-active: 10 + max-wait: -1 + max-idle: 3 + min-idle: 1 + timeout: 60000 + password: + diff --git a/sop-openapi/src/main/resources/application-sentinel.yml b/sop-openapi/src/main/resources/application-sentinel.yml new file mode 100644 index 00000000..bc6419d9 --- /dev/null +++ b/sop-openapi/src/main/resources/application-sentinel.yml @@ -0,0 +1,16 @@ +spring: + redis: + sentinel: + master: mymaster + nodes: + - 192.168.0.114:26379 + - 192.168.0.114:26380 + - 192.168.0.114:26381 + jedis: + pool: + max-active: 10 + max-wait: -1 + max-idle: 9 + min-idle: 0 + timeout: 60000 + password: \ No newline at end of file diff --git a/sop-openapi/src/main/resources/application-standalone.yml b/sop-openapi/src/main/resources/application-standalone.yml new file mode 100644 index 00000000..4e4735a5 --- /dev/null +++ b/sop-openapi/src/main/resources/application-standalone.yml @@ -0,0 +1,13 @@ +spring: + redis: + host: 52.81.162.154 + port: 8095 + jedis: + pool: + max-active: 10 + max-wait: -1 + max-idle: 9 + min-idle: 1 + database: 0 + timeout: 60000 + password: zixun@2020! diff --git a/sop-openapi/src/main/resources/application.properties b/sop-openapi/src/main/resources/application.properties new file mode 100644 index 00000000..b0b24634 --- /dev/null +++ b/sop-openapi/src/main/resources/application.properties @@ -0,0 +1,37 @@ +spring.profiles.active=standalone + +server.port=8089 +spring.application.name=open-apis +# ע +spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 + + +## ݿ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=jdbc:mysql://localhost:3306/sop?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai +spring.datasource.username=root +spring.datasource.password=root +spring.datasource.hikari.connectionInitSql=set names utf8mb4 + +#ӳ +spring.datasource.hikari.pool-name=HikariCP +spring.datasource.hikari.max-lifetime=500000 +spring.datasource.hikari.minimum-idle=2 +spring.datasource.hikari.idle-timeout=60000 +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.connection-test-query=select 1 + +# Naming strategy +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl +spring.jpa.hibernate.globally_quoted_identifiers=true +spring.jpa.properties.hibernate.globally_quoted_identifiers=true +spring.jpa.show-sql=false + + +# Apollo +app.id=open-apis +apollo.bootstrap.enabled=true +apollo.bootstrap.namespaces=application +apollo.bootstrap.eagerLoad.enable=true + +test.redis=wangping \ No newline at end of file diff --git a/sop-openapi/src/main/resources/isp/auth_error_en.properties b/sop-openapi/src/main/resources/isp/auth_error_en.properties new file mode 100644 index 00000000..53cea47c --- /dev/null +++ b/sop-openapi/src/main/resources/isp/auth_error_en.properties @@ -0,0 +1,4 @@ +isp.auth.user_id_empty_error=the user_id can NOT be null + +isp.auth.app_id_empty_error=the app_id can NOT be null +isp.auth.app_id_length_error=The app_id length must >= {0} and <= {1} \ No newline at end of file diff --git a/sop-openapi/src/main/resources/isp/auth_error_zh_CN.properties b/sop-openapi/src/main/resources/isp/auth_error_zh_CN.properties new file mode 100644 index 00000000..4dae56c3 --- /dev/null +++ b/sop-openapi/src/main/resources/isp/auth_error_zh_CN.properties @@ -0,0 +1,9 @@ +# û¼ƾ֤Ϊ +isp.auth.user_id_empty_error=\u7528\u6237\u0049\u0044\u4e0d\u80fd\u4e3a\u7a7a +# û¼ƾ֤ȱ{0}{1}֮ +#isp.auth.user_token_length_error=\u7528\u6237\u767b\u5f55\u51ed\u8bc1\u957f\u5ea6\u5fc5\u987b\u5728{0}\u548c{1}\u4e4b\u95f4 + +# ӦIDΪ +isp.auth.app_id_empty_error=\u5e94\u7528\u0049\u0044\u4e0d\u80fd\u4e3a\u7a7a +# ӦIDȱ{0}{1}֮ +isp.auth.app_id_length_error=\u5e94\u7528\u0049\u0044\u957f\u5ea6\u5fc5\u987b\u5728{0}\u548c{1}\u4e4b\u95f4 \ No newline at end of file diff --git a/sop-openapi/src/main/resources/isp/bizerror_en.properties b/sop-openapi/src/main/resources/isp/bizerror_en.properties new file mode 100644 index 00000000..8b8a691d --- /dev/null +++ b/sop-openapi/src/main/resources/isp/bizerror_en.properties @@ -0,0 +1,5 @@ +# + +# ϵͳ +isp.error_isv.common-error=The system is busy. +isp.error_isv.invalid-parameter=Invalid parameter, {0} \ No newline at end of file diff --git a/sop-openapi/src/main/resources/isp/bizerror_zh_CN.properties b/sop-openapi/src/main/resources/isp/bizerror_zh_CN.properties new file mode 100644 index 00000000..5df89769 --- /dev/null +++ b/sop-openapi/src/main/resources/isp/bizerror_zh_CN.properties @@ -0,0 +1,9 @@ +# + +# ϵͳæ +isp.error_isv.common-error=\u7cfb\u7edf\u7e41\u5fd9 +# Ч +isp.error_isv.invalid-parameter=\u53c2\u6570\u65e0\u6548, {0} + +# ==== ==== + diff --git a/sop-openapi/src/test/java/com/gitee/sop/AppTest.java b/sop-openapi/src/test/java/com/gitee/sop/AppTest.java new file mode 100644 index 00000000..cbd517a4 --- /dev/null +++ b/sop-openapi/src/test/java/com/gitee/sop/AppTest.java @@ -0,0 +1,20 @@ +package com.gitee.sop; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Unit test for simple App. + */ +public class AppTest +{ + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() + { + assertTrue( true ); + } +}