- 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, Jooby 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(new Pac4jModule().client(
"/api/secure/*",
conf -> new HeaderClient(
"Authorization",
"Bearer ",
new AdvancedJwtAuthenticator(
require(JedisPooled.class),
new SecretSignatureConfiguration(conf.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
public class AdvancedJwtAuthenticator extends JwtAuthenticator {
private final JedisPooled redis;
public AdvancedJwtAuthenticator(JedisPooled redis, SignatureConfiguration signatureConfiguration) {
super(signatureConfiguration);
this.redis = redis;
}
@Override
protected void createJwtProfile(TokenCredentials credentials, JWT jwt, WebContext context, SessionStore sessionStore) throws ParseException {
var jwtId = jwt.getJWTClaimsSet().getJWTID();
var uid = jwt.getJWTClaimsSet().getClaims().get(Jwt.Attribute.UID).toString();
if (!redis.exists(RedisNameSpace.getUserTokenExpirationKey(uid, jwtId))) {
throw new AuthorizationException();
}
super.createJwtProfile(credentials, jwt, context, sessionStore);
}
}
- For now, when you want
logout
, just delete the relatedjid
inredis
.
@AllArgsConstructor(access = AccessLevel.PUBLIC, onConstructor = @__({@Inject}))
public class AccessVerifierImpl implements AccessVerifier {
private Context context;
@Override
public boolean hasAnyRoles(String... roles) {
var rolesAsList = Arrays.asList(roles);
return Optional.ofNullable(context.getUser())
.map(UserProfile.class::cast)
.map(UserProfile::getRoles)
.stream()
.flatMap(Set::stream)
.anyMatch(rolesAsList::contains);
}
@Override
public void requireAnyRoles(String... roles) {
if (!hasAnyRoles(roles)) {
throw new ForbiddenException();
}
}
}
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
public Optional<UserDto> findCustomActivatedUserByPreferredUsername(Long preferredUsername) {
var query =
entityManager.createNativeQuery("select * from users where preferred_username = :preferredUsername and status = :status");
try {
var result = JpaQueryExecutor
.builder(UserDto.class)
.with(query, Map.of(
"preferredUsername", preferredUsername,
"status", Status.Code.ACTIVATED
)
)
.getSingleResult();
return Optional.of(result);
} catch (Exception e) {
log.warn("Could not find the users with given preferredUsername", e);
return Optional.empty();
}
}
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
)