How to use WebSession in Spring WebFlux to persist data? - spring

I am trying to develop web application using Spring WebFlux5.0.1 and Spring boot v2.0 M6 version. Requirement is to store objects in session and use it in subsequent pages/controllers.
Controller
#Controller
public class TestController {
#RequestMapping("/")
public Mono<String> testSession(Model model,ServerWebExchange swe){
Mono<WebSession> session = swe.getSession();
System.out.println("In testSession "+session);
model.addAttribute("account", new Account());
return Mono.just("account");
}
}
I was able to get Websession object from ServerWebExchange but i dont see methods to set/get attributes
Need help to understand how to use WebSession object in reactive world

Is it what you want to do ?
swe.getSession().map(
session -> {
session.getAttribute("foo"); // GET
session.getAttributes().put("foo", "bar") // SET
}
);

The accepted solution is incomplete in my opinion since it doesn't show the whole controller method, here it is how it would be done:
#PostMapping("/login")
fun doLogin(#ModelAttribute credentials: Credentials, swe: ServerWebExchange): Mono<String> {
return swe.session.flatMap {
it.attributes["userId"] = credentials.userId
"redirect:/globalPosition".toMono()
}
}

Related

spring Ausing double-slashes in URLs

I'm trying migration spring 4.0.7.RELEASE mvc project to webFlux,but old project definition url /getData/api/test,use /getData//api/test can request success,but in webFlux whether use Controller or FunctionType it`s response 404 notFound。How can I make it compatible old spring?
useController
#PostMapping("/getData/api/test")
public Object getData(#RequestBody Person person) {
log.info("received");
return "<html>hello</html>";
}
use function
#Bean
public RouterFunction<ServerResponse> timeRouter() {
return route().POST("/getData/api/test", timeHandler::getCurrentTime)
.build();
}

Spring 5 Reactive Mono - Pass Mono value to property of object and call another mono

I'm new to the whole Spring reactive webflux. My problem is pretty simple. In my addActions() I am trying to get a Mono by calling getCurrentVal(). This works fine. But I need to get the value of that and update a property (submission.stateVal). Then pass call customService.addActions() which returns Mono. Can this be done without using block()?
#Autowired
private CustomService customService;
public Mono<CustomResponse> addActions(String id, String Jwt, Submission submission) {
Mono<String> updatedStateVal = getCurrentStateVal(tpJwt, id);
// submission.setStateVal(updatedStateVal);
// return customService.addActions(id, jwt, submission);
}
private Mono<String> getCurrentVal(String tpJwt, String id) {
return customService.findById(id, tpJwt)
.map(r -> r.getStateVal());
}
return getCurrentStateVal(tpJwt, id)
.flatMap(s -> {
submission.setStateVal(s);
return customService.addActions(id, tpJwt, submission);
});

Spring Boot Webflux Security - reading Principal in service class when writing tests

I am quite new to the Spring ecosystem in general and Webflux. There are 2 things that I am trying to figure out and cannot find any specifics about.
My Setup:
I am writing a Spring Boot 2 REST API using WebFlux (not using controllers but rather handler functions). The authentication server is a separate service which issues JWT tokens and those get attached to each request as Authentication headers. Here is a simple example of a request method:
public Mono<ServerResponse> all(ServerRequest serverRequest) {
return principal(serverRequest).flatMap(principal ->
ReactiveResponses.listResponse(this.projectService.all(principal)));
}
Which i use to react to a GET request for a list of all "Projects" that a user has access to.
I afterwards have a service which retrieves the list of projects for this user and i render a json response.
The Problems:
Now in order to filter the projects based on the current user id i need to read it from the request principal. One issue here is that i have plenty service methods which need the current user information and passing it through to the service seems like an overkill. One solution is to read the principal inside the service from:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Question 1:
Is this a good practice in general when writing functional code (If i do this instead of propagating the principal)? is this a good approach despite the complexity of reading and sending the principal from the request to the service in each method?
Question 2:
Should i instead use the SecurityContextHolder Thread Local to fetch the principal, and if i do that how do i write tests for my service?
If i use the Security Context how do i test my service implementations which are expecting a principal that is of type JWTAuthenticationToken
and i always get null when trying to do something like described here: Unit testing with Spring Security
In the service tests, In tests what i've managed to do so far is to propagate the principal to the service methods and use mockito to mock the principal. This is quite straightforward.
In the Endpoint Tests i am using #WithMockUser to populate the principal when doing requests and i mock out the service layer. This has the downside of the principal type being different.
Here is how my test class for the service layer looks:
#DataMongoTest
#Import({ProjectServiceImpl.class})
class ProjectServiceImplTest extends BaseServiceTest {
#Autowired
ProjectServiceImpl projectService;
#Autowired
ProjectRepository projectRepository;
#Mock
Principal principal;
#Mock
Principal principal2;
#BeforeEach
void setUp() {
initMocks(this);
when(principal.getName()).thenReturn("uuid");
when(principal2.getName()).thenReturn("uuid2");
}
// Cleaned for brevity
#Test
public void all_returnsOnlyOwnedProjects() {
Flux<Project> saved = projectRepository.saveAll(
Flux.just(
new Project(null, "First", "uuid"),
new Project(null, "Second", "uuid2"),
new Project(null, "Third", "uuid3")
)
);
Flux<Project> all = projectService.all(principal2);
Flux<Project> composite = saved.thenMany(all);
StepVerifier
.create(composite)
.consumeNextWith(project -> {
assertThat(project.getOwnerUserId()).isEqualTo("uuid2");
})
.verifyComplete();
}
}
Based on the other answer, i managed to solve this problem in the following way.
I added the following methods to read the id from claims where it normally resides within the JWT token.
public static Mono<String> currentUserId() {
return jwt().map(jwt -> jwt.getClaimAsString(USER_ID_CLAIM_NAME));
}
public static Mono<Jwt> jwt() {
return ReactiveSecurityContextHolder.getContext()
.map(context -> context.getAuthentication().getPrincipal())
.cast(Jwt.class);
}
Then i use this within my services wherever needed, and i am not forwarding it through the handler to the service.
The tricky part was always testing. I am able to resolve this using the custom SecurityContextFactory. I created an annotation which i can attach the same way as #WithMockUser, but with some of the claim details i need instead.
#Retention(RetentionPolicy.RUNTIME)
#WithSecurityContext(factory = WithMockTokenSecurityContextFactory.class)
public #interface WithMockToken {
String sub() default "uuid";
String email() default "test#test.com";
String name() default "Test User";
}
Then the Factory:
String token = "....ANY_JWT_TOKEN_GOES_HERE";
#Override
public SecurityContext createSecurityContext(WithMockToken tokenAnnotation) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
HashMap<String, Object> headers = new HashMap<>();
headers.put("kid", "SOME_ID");
headers.put("typ", "JWT");
headers.put("alg", "RS256");
HashMap<String, Object> claims = new HashMap<>();
claims.put("sub", tokenAnnotation.sub());
claims.put("aud", new ArrayList<>() {{
add("SOME_ID_HERE");
}});
claims.put("updated_at", "2019-06-24T12:16:17.384Z");
claims.put("nickname", tokenAnnotation.email().substring(0, tokenAnnotation.email().indexOf("#")));
claims.put("name", tokenAnnotation.name());
claims.put("exp", new Date());
claims.put("iat", new Date());
claims.put("email", tokenAnnotation.email());
Jwt jwt = new Jwt(token, Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS), headers,
claims);
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES); // Authorities are needed to pass authentication in the Integration tests
context.setAuthentication(jwtAuthenticationToken);
return context;
}
Then a simple test will look like this:
#Test
#WithMockToken(sub = "uuid2")
public void delete_whenNotOwner() {
Mono<Void> deleted = this.projectService.create(projectDTO)
.flatMap(saved -> this.projectService.delete(saved.getId()));
StepVerifier
.create(deleted)
.verifyError(ProjectDeleteNotAllowedException.class);
}
As you are using Webflux you should be using the ReactiveSecurityContextHolder to retrieve the principal like so : Object principal = ReactiveSecurityContextHolder.getContext().getAuthentication().getPrincipal();
The use of the non-reactive one will return null as you are seeing.
There is more info related to the topic in this answer - https://stackoverflow.com/a/51350355/197342

How to implement Session Tracking in spring MVC?

I am very new to spring mvc, I have to develop a web application based on session tracking and my application is annotation based. In my web app I have route each page based on the username and role existence in session. Initially I have been using HttpSession as parameter to controller method, but it is very difficult to check each and every request. I know there are many application level security ways in spring, but I really couldn't understand how to use them. Please suggest me some solutions, For all help thanks in advance.
After updating with interceptors:
Controller class
// Method to showLogin page to user
#RequestMapping(value = "user")
public ModelAndView showLoginToUser(#ModelAttribute("VMFE") VmFeUser VMFE,HttpSession session) {
System.out.println("#C====>showLoginToUser()===> ");
ModelAndView view = new ModelAndView();
//session.setAttribute("user_name", "no_user");
try {
view.setViewName("login");
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
Interceptor
public class HelloWorldInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle (HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
RequestMapping rm = ((HandlerMethod) handler).getMethodAnnotation(
RequestMapping.class);
boolean alreadyLoggedIn = request.getSession()
.getAttribute("user_name") != null;
boolean loginPageRequested = rm != null && rm.value().length > 0
&& "login".equals(rm.value()[0]);
if (alreadyLoggedIn && loginPageRequested) {
//response.sendRedirect(request.getContextPath() + "/app/main-age");
return false;
} else if (!alreadyLoggedIn && !loginPageRequested) {
System.out.println("REDIRECTING===");
response.sendRedirect(request.getContextPath() + "/user");
return false;
}
return true;
}
}
Using spring security you can implement session tracking and apply filters to validate requests. Spring security is very easy to implement. Kindly follow spring security tutorial click here.
You can also check my git repo for implementation click here. It's a angular spring boot application and i have used spring security and JWT for authentication and authorization.
Hope it helps you thanks.

Authorization in jersey framework

I am using jersey (java) framework. I did authentication based on cookie using Container request filter. Now I have to do Authorization. So, how to I proceed? Quick guidance please.
Jersey has #RolesAllowed("role") annotation to facilitate auth check. Make use of:
#Context
HttpServletRequest httpRequest;`
and in the login method put identity into session like here:
HttpSession session = httpRequest.getSession(true);
session.setAttribute(key, val);
in filter
final String name = session.getAttribute(key);
...
SecurityContext securityContext = new SecurityContext() {
public boolean isUserInRole(String roleName) {
return roleName.equals("role");
}
...
public Principal getUserPrincipal() {
...
return new Principal() {
public String getName() {
return name;
}
};
...
}
...
};
requestContext.setSecurityContext(securityContext);
That's it in short. It is quite common approach. If you want I can share ref impl on GitHub.

Resources