- Fast, light, easy to learn
- I've been using Spring for almost my wokring time with Java/Kotlin. For large scale enterprise projects, Spring Stacks are undoubtedly the way to go, but for minimal projects, Kooby is a good choice.
- Support Default JWT
- Support Role Access Layer
- Hibernate, Flyway support by default
- Add custom JPAQueryExecutor for the better querying
- Add Jedis support instead of Lettuce
- Using MapStruct for Object Mapper
- Using Guice as Dependency Injection Framework
- Multiple language support
- Default admin user:
admin@localhost/admin
- Create users
curl --location 'http://localhost:8080/api/auth/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "test",
"email": "test@localhost",
"password": "test"
}'
- Generate token
curl --location 'http://localhost:8080/api/auth/generate-token' \
--header 'Content-Type: application/json' \
--data-raw '{
"username": "test@localhost",
"password": "test"
}'
- Get User Info
# With JPA Query
curl --location 'http://localhost:8080/api/secure/user/info' \
--header 'Accept-Language: vi'
--header 'Authorization: ••••••'
# With JPAQueryExecutor
curl --location 'http://localhost:8080/api/secure/user/info-with-executor' \
--header 'Accept-Language: vi'
--header 'Authorization: ••••••'
- Test Role
curl --location 'http://localhost:8080/api/secure/test/admin-role' \
--header 'Authorization: ••••••'
- Logout
curl --location --request DELETE 'http://localhost:8080/api/auth/secure/logout' \
--header 'Authorization: ••••••'
- Using Pac4j Module as Security Layer
install(
Pac4jModule()
.client("/api/secure/*") {
HeaderClient(
"Authorization",
"Bearer ",
AdvancedJwtAuthenticator(
require(JedisPooled::class.java),
SecretSignatureConfiguration(it.getString("jwt.salt"))
)
)
}
)
- Using
HeaderClient
to tell Jooby readBearer
token from header - By default Jooby use the
JwtAuthenticator
from Pac4j, the problems are:- Token is completed stateless
- What if user is lock/inactivated/deleted -> token may still valid by the
exp
-> user still can access to system - There is no truly
logout
So, I solved these problems by store jid
of JWT in Redis, after validate
raw token, before createProfile
I made a simple check to ensure the jid
exists in redis
. If no, token is invalid
See AdvancedJwtAuthenticator.kt
class AdvancedJwtAuthenticator(private val redis: JedisPooled, signatureConfiguration: SignatureConfiguration) :
JwtAuthenticator(signatureConfiguration) {
@Throws(ParseException::class)
override fun createJwtProfile(
credentials: TokenCredentials, jwt: JWT, context: WebContext?,
sessionStore: SessionStore?
) {
val jwtId = jwt.jwtClaimsSet.jwtid.toString()
val uid = jwt.jwtClaimsSet.claims[Jwt.Attribute.UID].toString()
if (!redis.exists(RedisNameSpace.getUserTokenExpirationKey(uid, jwtId))) {
throw AuthorizationException()
}
super.createJwtProfile(credentials, jwt, context, sessionStore)
}
}
- For now, when you want
logout
, just delete the relatedjid
inredis
.
internal class AccessVerifierImpl @Inject constructor(private val context: Context) : AccessVerifier {
override fun hasRole(role: String): Boolean {
val user = context.getUser<UserProfile>()
return user?.roles?.contains(role) ?: false
}
override fun requireRole(role: String) {
if (!hasRole(role)) {
throw ForbiddenAccessException()
}
}
override fun hasAnyRoles(vararg roles: String): Boolean {
val user = context.getUser<UserProfile>()
return user?.roles?.any { roles.contains(it) } ?: false
}
override fun requireAnyRoles(vararg roles: String) {
if (!hasAnyRoles(*roles)) {
throw ForbiddenAccessException()
}
}
}
hasRole
orhasAnyRoles
will check and returntrue
/false
, whilerequireRole
andrequireAnyRoles
will explicitly throw exception if you do not have access.
- Problem: Sometimes we want to retrieve data from database via native query, but we do not want manually map field's value from result to pojo class.
- To solve this problem we have so many ways. With the usage of Hibernate, I create JPA Query Executor to parse sql result to object via Jackson
class UserRepositoryImpl @Inject constructor(
private val em: EntityManager
) : UserRepository {
override fun findCustomActivatedUserByPreferredUsername(preferredUsername: Long): UserInfoDto? {
val query =
em.createNativeQuery("select * from users where preferred_username = :preferredUsername and status = ${StatusCode.ACTIVATED}")
return JpaQueryExecutor.builder<UserInfoDto>()
.with(query, mutableMapOf("preferredUsername" to preferredUsername))
.map(UserInfoDto::class.java)
.getSingleResult()
}
}
So, we directly map database field's value to Pojo object via Jackson, if Pojo class does not have correct field name, please using @JsonAlias (Like database field
full_name
, pojo classfullName
)