As long as you keep to standards, jhipster is a great tool, but it starts to get difficult when you like to have something different. In my use case it is necessary to call backend service after user creation, and I had to google and tweak a lot to achieve this. I put it in one place, so you do not have to.
Here comes why:
- Gateway app manages user and authority entities and is completely generated by JHipster, and I like to keep it. Or maybe switch from database to LDAP later
- Other entities are managed in own microservice, and there is separate database, and no way to have relationships to user entities.
- There must be an object for every user created / removed as users are created and removed.
- JHipster gateway app is reactive. It makes things interesting.
Reactive service interfaces.
Unfortunately openapii-generator from previous post will now work here - JHIpster uses outdated version, and even new one is unable to generate interfaces for reactive feign clients out of the box. Fortunately it is easy to tweak, and we do not really need jhipster for it.
We start with gradle invocation. I put the code into gradle/swagger.gradle following jhipster project structure. This will generate interfaces when necessary:
apply plugin: "org.openapi.generator"
// generate client interfaces and data models.
// this invocation is very fragile!!!
openApiGenerate {
generatorName = "spring"
inputSpec = "$rootDir/../recorder/src/main/resources/swagger/api.yml".toString()
outputDir = "$buildDir/openapi".toString()
// verbose = true
apiPackage = "de.xxxxxxxx.api"
modelPackage = "de.xxxxxxxxx.model"
invokerPackage = "de.xxxxxxx.invoker"
// we have to use custom template for reactive feign clients
// some explanations:
// https://openapi-generator.tech/docs/templating/
// base for custom templates:
// https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/JavaSpring
templateDir = "$rootDir/src/main/openapigen".toString()
configOptions = [
dateLibrary : "java8",
reactive : "true",
interfaceOnly : "true",
skipDefaultInterface: "true",
]
}
sourceSets {
main {
java {
srcDir file("${project.buildDir.path}/openapi/src/main/java")
}
}
}
compileJava.dependsOn("openApiGenerate")
We can not use standard reactive templates, as they produce method signatures unsuitable for reactive feign client we intend to use - wrapping parameters into monos and fluxes, adding web exchanges etc. So here are custom templates:
This generates nice interfaces (gradle openapiGenerate) :
package de.xxxxxxxxxxx.api;
// imports and generated comments are removed for brevity and readability
.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2021-05-06T17:03:09.822261726+03:00[Europe/Vilnius]")
(value = "userProfile", description = "the userProfile API")
public interface UserProfileApi {
/**
* POST /userProfile : creates new user profile new user specifid by principal
* new user profile object is created for the user with principal set
*
* @param userProfileDto (optional)
* @return user profile was created (status code 200)
* or the same user principal already exists, no new profile was created (status code 406)
* or internal server error occured (status code 500)
*/
(value = "creates new user profile new user specifid by principal", nickname = "createUserProfile", notes = "new user profile object is created for the user with principal set", authorizations = {
(value = "jwt")
}, tags={ })
(value = {
(code = 200, message = "user profile was created"),
(code = 406, message = "the same user principal already exists, no new profile was created"),
(code = 500, message = "internal server error occured") })
(
value = "/userProfile",
consumes = { "application/json" }
)
Mono<Void> createUserProfile(@ApiParam(value = "" ) @Valid @RequestBody(required = false) UserProfileDto userProfileDto
);
/**
* DELETE /userProfile : delete user profile
* user profile and corresponding resources and mappings are removed
*
* @param principal (required)
* @return user profile was removed (status code 200)
* or internal server error occured (status code 500)
*/
(value = "delete user profile", nickname = "removeUserProfile", notes = "user profile and corresponding resources and mappings are removed", authorizations = {
(value = "jwt")
}, tags={ })
(value = {
(code = 200, message = "user profile was removed"),
(code = 500, message = "internal server error occured") })
(
value = "/userProfile"
)
Mono<Void> removeUserProfile(@NotNull @ApiParam(value = "", required = true) @Valid @RequestParam(value = "principal", required = true) String principal
);
}
And now we are just a tiny step away from
Registering the bean for autowiring
Just derive from generated interface and add some annotations:
import reactivefeign.spring.config.ReactiveFeignClient;
import de.vc.admin.client.recorder.api.UserProfileApi;
(name = "${default.name:default}", path="api/", url = "${default.url:http://localhost:8081/api}")
public interface UserProfileReactiveFeign extends UserProfileApi {
}
- name provides name of the eureca registered application. It will be used if it can be found.
- url specifies base fallback URL when none is availab le form eureca
- path defines base path on eureca server - reactive feign client does not support name=”service/path” notation
As we are inside JHipster infrastructure, all the black magic with eureka, histrix and discovery shall just work. And of course, we need to provide basic configuration with @EnableReactiveFeignClients. It is better to put this configuration on dedicated interface as application class itself is generated by jhipster and we are more flexible this way
"de.vc.admin.feign")
public class ReactiveFeignConfiguration {
}
(basePackages =
Proper dependencies
And of course we need feign client library in our build.gradle which provides reactive feign clients (standard one does not do this yet):
implementation("com.playtika.reactivefeign:feign-reactor-spring-cloud-starter:3.0.3")
calling client reactive way
Now we can inject generated spring bean and make calls to backend ( this is basically jhispter generated service, so I shortened irrelevant parts)
/**
* Service class for managing users.
*/
public class UserService {
// injected reactive feign client
private final UserProfileReactiveFeign recorderClient;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthorityRepository authorityRepository, UserProfileReactiveFeign recorderClient) {
//omited for brevity
this.recorderClient = recorderClient;
}
// standard generated user creation method
public Mono<User> createUser(AdminUserDTO userDTO) {
// code omited for brevity
return Flux
.fromIterable(userDTO.getAuthorities() != null ? userDTO.getAuthorities() : new HashSet<>())
.flatMap(authorityRepository::findById)
.doOnNext(authority -> user.getAuthorities().add(authority))
.then(Mono.just(user))
.publishOn(Schedulers.boundedElastic())
.map(
newUser -> {
String encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword());
newUser.setPassword(encryptedPassword);
newUser.setResetKey(RandomUtil.generateResetKey());
newUser.setResetDate(Instant.now());
newUser.setActivated(true);
return newUser;
}
)
.flatMap(this::saveUser)
// here happens async invocation magic
.flatMap(user1 -> recorderClient.createUserProfile(new UserProfileDto().principal(user1.getLogin())).thenReturn(user1))
.doOnNext(user1 -> log.debug("Created Information for User: {}", user1));
}
}
And now when we create new user, backend service will be invoked. And we will certaily see 401 not authorised in our logs
Propagating JWT token
As gateway app is secured with JWT and we are creating user from authenticated web frontend we shall be able to propagate this security token to microservice as bearer token. We need an interceptor. And it has to be done in reactive-way:
package de.xxxxxxxxx.feign;
// those are important, our feign client implementation looks for them
import reactivefeign.client.ReactiveHttpRequest;
import reactivefeign.client.ReactiveHttpRequestInterceptor;
import java.util.List;
public class UserFeignClientInterceptor implements ReactiveHttpRequestInterceptor {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER = "Bearer";
private final TokenProvider tokenProvider;
public UserFeignClientInterceptor(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
public Mono<ReactiveHttpRequest> apply(ReactiveHttpRequest reactiveHttpRequest) {
log.error("applying security filter");
return Mono.just(reactiveHttpRequest)
.log("UserFeignClientInterceptor")
.flatMap(request -> {
// Hardwire Auth token, as I did not figured yet how to make reactive feign clients to
// pick up proper security context
String jwtToken = tokenProvider.createToken(
new UsernamePasswordAuthenticationToken("foobar", "", Arrays.asList(new SimpleGrantedAuthority("ADMIN"))),
true
);
log.error("invoking map {}", jwtToken);
request.headers().put(AUTHORIZATION_HEADER, List.of(String.format("%s %s", BEARER, jwtToken)));
// IMPORTANT: wrape incomming request into mono
return Mono.just(request);
});
}
}
And now our calls are authorised to call microservices.