add Redis cache and some clean up

- update and cleanup in README.md
- update getIdentity to throw response exception with 401 in JwtService
- update http files to use identity from env
- add cacheable to getIdentityFromDevice in DeviceRegistryService
- add RedisCacheConfiguration
- add CacheProperty
- set up cache property
- set up data redis cache
This commit is contained in:
2024-12-24 05:50:56 +01:00
parent 19f46bd01f
commit 6522809dce
14 changed files with 153 additions and 62 deletions

View File

@@ -5,26 +5,31 @@ Classes and endpoints, to shape and to steer, Devices and sensors, their purpose
## Properties for deployment ## Properties for deployment
| name | required | info | | name | required | info |
|----------------------------------------------|----------|----------------------------------------------| |----------------------------------------------|:--------:|----------------------------------------------|
| spring.profiles.active | * | Spring Boot environment | | spring.profiles.active | ✓ | Spring Boot environment |
| server.port | * | HTTP port | | server.port | ✓ | HTTP port |
| server.ssl.enabled | * | HTTP Enable SSL | | server.ssl.enabled | ✓ | HTTP Enable SSL |
| server.ssl.key-store | * | HTTP Keystore | | server.ssl.key-store | ✓ | HTTP Keystore |
| server.ssl.key-store-type | * | HTTP Cert Type | | server.ssl.key-store-type | ✓ | HTTP Cert Type |
| server.ssl.key-store-password | ** | HTTP Cert Pass | | server.ssl.key-store-password | ✗ | HTTP Cert Pass |
| jwt.public-key | * | JWT public key | | spring.cache.type | | Cache type (redis) |
| device-registry.url | * | Device Register URL | | spring.data.redis.host | ✓ | Redis host |
| device-data.url | * | Device Data URL | | spring.data.redis.port | | Redis port |
| device-configuration.url | * | Device Configuration URL | | spring.data.redis.database | ✓ | Redis database |
| cache.time-to-live | | Cache time to live (minutes) |
| jwt.public-key | ✓ | JWT public key |
| device-registry.url | ✓ | Device Register URL |
| device-data.url | ✓ | Device Data URL |
| device-configuration.url | ✓ | Device Configuration URL |
| management.influx.metrics.export.api-version | | InfluxDB API version | | management.influx.metrics.export.api-version | | InfluxDB API version |
| management.influx.metrics.export.enabled | | Enable/Disable exporting metrics to InfluxDB | | management.influx.metrics.export.enabled | | Enable/Disable exporting metrics to InfluxDB |
| management.influx.metrics.export.bucket | * | InfluxDB bucket name | | management.influx.metrics.export.bucket | ✓ | InfluxDB bucket name |
| management.influx.metrics.export.org | * | InfluxDB organization | | management.influx.metrics.export.org | ✓ | InfluxDB organization |
| management.influx.metrics.export.token | ** | InfluxDB token | | management.influx.metrics.export.token | ✗ | InfluxDB token |
| management.influx.metrics.export.uri | * | InfluxDB URL | | management.influx.metrics.export.uri | ✓ | InfluxDB URL |
| management.metrics.tags.application | * | Application instance tag for metrics | | management.metrics.tags.application | ✓ | Application instance tag for metrics |
Required: * can be stored as text, and ** need to be stored as secret. *Required: ✓ can be stored as text, and ✗ need to be stored as secret.*
## Releasing Service ## Releasing Service
@@ -34,43 +39,32 @@ Run `release.sh` script from `master` branch.
### Developer Keystore ### Developer Keystore
1. Open `hosts` file: We use a keystore to enable HTTPS for our API. To set up your developer environment for local development, please refer to [generate keystore](https://github.com/swordsteel/hlaeja-development/blob/master/doc/keystore.md) documentation. When generating and exporting the certificate for local development, please store it in the `./cert/keystore.p12` folder at the project root.
* On Unix-like systems (Linux, macOS), this directory is typically `/etc/hosts`.
* On Windows, this directory is typically `%SystemRoot%\System32\drivers\etc\hosts`.
2. Add the following lines to the `hosts` file:
```text
127.0.0.1 deviceapi # Hlæja Device API
```
3. Generate Keystores
```shell
keytool -genkeypair -alias device-api -keyalg RSA -keysize 2048 -validity 3650 -dname "CN=deviceapi" -keypass password -keystore ./cert/keystore.p12 -storetype PKCS12 -storepass password
```
4. Export the public certificate
```shell
keytool -export -alias device-api -keystore ./cert/keystore.p12 -storepass password -file ./cert/device-api.cer -rfc
```
### Public RSA Key ### Public RSA Key
To validate devices, copy file named `public_key.pem` from `./cert` generated for local development in **Hlæja Device Register** in to `./cert`. This service uses the public key from **[Hlæja Device Register](https://github.com/swordsteel/hlaeja-device-registry)** to identify devices. To set up device identification for local development, copy the `public_key.pem` file from the `./cert` directory in **Hlæja Device Register** into the `./cert` directory of this project.
### Global gradle properties *Note: For more information on generating RSA keys, please refer to our [generate RSA key](https://github.com/swordsteel/hlaeja-development/blob/master/doc/rsa_key.md) documentation.*
To authenticate with Gradle to access repositories that require authentication, you can set your user and token in the `gradle.properties` file. ### Global Settings
Here's how you can do it: This services rely on a set of global settings to configure development environments. These settings, managed through Gradle properties or environment variables.
1. Open or create the `gradle.properties` file in your Gradle user home directory: *Note: For more information on global properties, please refer to our [global settings](https://github.com/swordsteel/hlaeja-development/blob/master/doc/global_settings.md) documentation.*
- On Unix-like systems (Linux, macOS), this directory is typically `~/.gradle/`. #### Gradle Properties
- On Windows, this directory is typically `C:\Users\<YourUsername>\.gradle\`.
2. Add the following lines to the `gradle.properties` file:
```properties ```properties
repository.user=your_user repository.user=your_user
repository.token=your_token_value repository.token=your_token_value
influxdb.token=your_token_value
```
#### Environment Variables
```properties
REPOSITORY_USER=your_user
REPOSITORY_TOKEN=your_token_value
INFLUXDB_TOKEN=your_token_value
``` ```
or use environment variables `REPOSITORY_USER` and `REPOSITORY_TOKEN`

View File

@@ -18,6 +18,8 @@ dependencies {
implementation(hlaeja.micrometer.registry.influx) implementation(hlaeja.micrometer.registry.influx)
implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.library.hlaeja.common.messages)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.cache)
implementation(hlaeja.springboot.starter.redis)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.jjwt.impl) runtimeOnly(hlaeja.jjwt.impl)

View File

@@ -1,3 +1,3 @@
### get measurement ### get configuration
GET {{hostname}}/configuration GET {{hostname}}/configuration
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}

View File

@@ -1,8 +1,10 @@
{ {
"development": { "development": {
"hostname": "https://localhost:8443" "hostname": "https://localhost:8443",
"identity": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
}, },
"docker": { "docker": {
"hostname": "https://localhost:9000" "hostname": "https://localhost:9000",
"identity": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
} }
} }

View File

@@ -1,11 +1,11 @@
### get measurement ### get measurement
GET {{hostname}}/measurement GET {{hostname}}/measurement
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}
### add measurement for all ### add measurement for all
POST {{hostname}}/measurement POST {{hostname}}/measurement
Content-Type: application/json Content-Type: application/json
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}
{ {
"button0": 0, "button0": 0,
@@ -16,8 +16,8 @@ Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVW
### add measurement for one ### add measurement for one
POST {{hostname}}/measurement POST {{hostname}}/measurement
Content-Type: application/json Content-Type: application/json
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}
{ {
"button0": 0 "button0": 1
} }

View File

@@ -1,5 +1,6 @@
package ltd.hlaeja package ltd.hlaeja
import ltd.hlaeja.property.CacheProperty
import ltd.hlaeja.property.DeviceConfigurationProperty import ltd.hlaeja.property.DeviceConfigurationProperty
import ltd.hlaeja.property.DeviceDataProperty import ltd.hlaeja.property.DeviceDataProperty
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
@@ -7,8 +8,11 @@ import ltd.hlaeja.property.JwtProperty
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
@EnableCaching
@EnableConfigurationProperties( @EnableConfigurationProperties(
CacheProperty::class,
DeviceConfigurationProperty::class, DeviceConfigurationProperty::class,
DeviceDataProperty::class, DeviceDataProperty::class,
DeviceRegistryProperty::class, DeviceRegistryProperty::class,

View File

@@ -0,0 +1,26 @@
package ltd.hlaeja.configuration
import java.time.Duration
import ltd.hlaeja.exception.CacheException
import ltd.hlaeja.property.CacheProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.core.RedisTemplate
@Configuration
class RedisCacheConfiguration(
private val cacheProperty: CacheProperty,
) {
@Bean
fun cacheManager(
redisTemplate: RedisTemplate<String, String>,
): RedisCacheManager = redisTemplate.connectionFactory
?.let { RedisCacheManager.builder(it).cacheDefaults(getRedisCacheConfiguration()).build() }
?: throw CacheException("Redis connection factory is not set")
private fun getRedisCacheConfiguration(): RedisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(cacheProperty.timeToLive))
}

View File

@@ -0,0 +1,23 @@
package ltd.hlaeja.exception
@Suppress("unused")
class CacheException : Exception {
constructor() : super()
constructor(message: String) : super(message)
constructor(cause: Throwable) : super(cause)
constructor(
message: String,
cause: Throwable,
) : super(message, cause)
constructor(
message: String,
cause: Throwable,
enableSuppression: Boolean,
writableStackTrace: Boolean,
) : super(message, cause, enableSuppression, writableStackTrace)
}

View File

@@ -0,0 +1,8 @@
package ltd.hlaeja.property
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties(prefix = "cache")
data class CacheProperty(
val timeToLive: Long,
)

View File

@@ -7,6 +7,7 @@ import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
import ltd.hlaeja.util.deviceRegistryIdentityDevice import ltd.hlaeja.util.deviceRegistryIdentityDevice
import org.springframework.cache.annotation.Cacheable
import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.ErrorResponseException import org.springframework.web.ErrorResponseException
@@ -31,6 +32,7 @@ class DeviceRegistryService(
.description("Number of failed device identity calls") .description("Number of failed device identity calls")
.register(meterRegistry) .register(meterRegistry)
@Cacheable(value = ["identity"], key = "#device")
suspend fun getIdentityFromDevice( suspend fun getIdentityFromDevice(
device: UUID, device: UUID,
): Identity.Response = try { ): Identity.Response = try {

View File

@@ -1,13 +1,16 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.JwtParser import io.jsonwebtoken.JwtParser
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.property.JwtProperty import ltd.hlaeja.property.JwtProperty
import ltd.hlaeja.util.PublicKeyProvider import ltd.hlaeja.util.PublicKeyProvider
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@@ -23,8 +26,13 @@ class JwtService(
suspend fun getIdentity( suspend fun getIdentity(
identityToken: String, identityToken: String,
): Identity.Response = readIdentity(identityToken) ): Identity.Response = try {
readIdentity(identityToken)
.let { deviceRegistry.getIdentityFromDevice(it) } .let { deviceRegistry.getIdentityFromDevice(it) }
} catch (e: JwtException) {
log.warn { e.localizedMessage }
throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
}
private suspend fun readIdentity( private suspend fun readIdentity(
identity: String, identity: String,

View File

@@ -39,6 +39,11 @@
"name": "device-configuration.url", "name": "device-configuration.url",
"type": "java.lang.String", "type": "java.lang.String",
"description": "Url for device configuration service." "description": "Url for device configuration service."
},
{
"name": "cache.time-to-live",
"type": "java.lang.Long",
"description": "Cache expiration in minutes."
} }
] ]
} }

View File

@@ -9,6 +9,12 @@ spring:
os: os:
name: "%APP_BUILD_OS_NAME%" name: "%APP_BUILD_OS_NAME%"
version: "%APP_BUILD_OS_VERSION%" version: "%APP_BUILD_OS_VERSION%"
cache:
type: redis
data:
redis:
port: 6379
database: 1
management: management:
endpoints: endpoints:
@@ -29,6 +35,9 @@ management:
bucket: hlaeja bucket: hlaeja
org: hlaeja_ltd org: hlaeja_ltd
cache:
time-to-live: 10
jwt: jwt:
public-key: cert/public_key.pem public-key: cert/public_key.pem
@@ -40,6 +49,9 @@ spring:
config: config:
activate: activate:
on-profile: development on-profile: development
data:
redis:
host: localhost
management: management:
metrics: metrics:
@@ -76,6 +88,9 @@ spring:
config: config:
activate: activate:
on-profile: docker on-profile: docker
data:
redis:
host: Redis
management: management:
metrics: metrics:

View File

@@ -6,3 +6,5 @@ device-data:
url: http://localhost:9020 url: http://localhost:9020
device-configuration: device-configuration:
url: http://localhost:9030 url: http://localhost:9030
cache:
time-to-live: 10