I have an application with Spring Session and Spring Websockets. I use websocket handshake interceptor to set properties to websocket session
public class WebsocketHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(true);
if (session != null) {
attributes.put(ApplicationConstants.HTTP_SESSION_ID_KEY, session.getId());
attributes.put(ApplicationConstants.HTTP_SESSION_KEY, session);
}
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {
}
}
Then I catch SessionConnectEvent. Event contais needed information (http session and session id). But the session is null in sessionRepository
#Component
public class WebSocketListener implements ApplicationListener<SessionConnectEvent> {
#Autowired
private MapSessionRepository sessionManager;
#Override
public void onApplicationEvent(SessionConnectEvent event) {
String httpSessionId
= event.getWebSocketSession().getAttributes().get(ApplicationConstants.HTTP_SESSION_ID_KEY).toString();
sessionManager.getSession(httpSessionId); //returns null
}
}
It returns null with the first call, if I try to reconnect, it returns valid session. Can you show me my error? Thanks.
Related
I'm working with RedisHttpSession and my basic goal is to save the staff object in the session object on successful login, retrieve it wherever I need and destroy the session on logout.
On successful login, this is what I'm doing:
Staff staff = staffService.getEmailInstance(body.getEmailId());
request.getSession(true).setAttribute("staff", staff);
And Logout is simply this:
request.getSession().invalidate();
In a different controller, I am calling this utility method that checks if the staff is logged in: util.isStaffLoggedIn(request, response, StaffRole.EDITOR); If the staff is logged in, the API proceeds, else the user is redirected to the login page.
#Service
public class Util {
public boolean isStaffLoggedIn(HttpServletRequest request, HttpServletResponse response, StaffRole staffRole)
throws PaperTrueInvalidCredentialsException, PaperTrueJavaException {
Staff staff = (Staff) request.getSession().getAttribute("staff");
if (!isObjectNull(staff) && staff.getStaffRole().equals(staffRole)) {
return true;
}
invalidateSessionAndRedirect(request, response);
return false;
}
public void invalidateSessionAndRedirect(HttpServletRequest request, HttpServletResponse response)
throws PaperTrueJavaException, PaperTrueInvalidCredentialsException {
request.getSession().invalidate();
try {
response.sendRedirect(ProjectConfigurations.configMap.get("staff_logout_path"));
} catch (IOException e) {
throw new PaperTrueJavaException(e.getMessage());
}
throw new PaperTrueInvalidCredentialsException("Staff not loggedIn");
}
}
Now while the app is running, the get-jobs API is called immidiately after successful login. Most of the times the request.getSession().getAttribute("staff") method works fine and returns the 'staff' object but, once in a while, it returns null. This doesn't happen often, but it does. I printed the session Id to see if they are different after logout, and they were. After each logout I had a new session Id. I even checked if the staff object I retrieved from the database was null, but it wasn't.
The staff object was successfully saved in the sessions but I wasn't able to retrieve it in othe APIs. This is how my session config looks:
#EnableRedisHttpSession(maxInactiveIntervalInSeconds = 10800)
public class SessionConfig {
HashMap<String, String> configMap = ProjectConfigurations.configMap;
#Bean
public LettuceConnectionFactory connectionFactory() {
int redisPort = Integer.parseInt(configMap.get("redis_port"));
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(
configMap.get("redis_host"), redisPort);
redisStandaloneConfiguration.setPassword(configMap.get("redis_password"));
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("PTSESSIONID");
serializer.setSameSite("none");
serializer.setUseSecureCookie(!configMap.get("staff_logout_path").contains("localhost"));
return serializer;
}
}
Please let me know if I missed out anything. Thanks in advance.
Update 1
I'm not invalidating the session anymore and I've replaced request.getSession(true).setAttribute("staff", staff); to request.getSession().setAttribute("staff", staff);
I'm setting the 'staff' in StaffController and getting it in EditorController. Here's how I'm setting it:
#RestController
#RequestMapping(path = { "/staff" }, produces = "application/json")
public class StaffApiController {
private final HttpServletRequest request;
private final HttpSession httpSession;
#Autowired
private StaffService staffService;
#Autowired
StaffApiController(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
this.request = request;
this.httpSession = session;
}
#PostMapping("/login")
public ResponseEntity<StaffLoginResponse> login(#Valid #RequestBody StaffLoginBody body) {
StaffLoginResponse staffLoginResponse = new StaffLoginResponse();
try {
if (!staffService.isValidLogin(body.getEmailId(), body.getPassword())) {
throw new PaperTrueInvalidCredentialsException("Invalid Credentials");
}
Staff staff = staffService.getEmailInstance(body.getEmailId());
httpSession.setAttribute("staff", staff);
staffLoginResponse.setEmail(staff.getEmail()).setRole(staff.getStaffRole().getValue())
.setStaffID(staff.getId()).setStatus(new Status("Staff Login Successful"));
} catch (PaperTrueException e) {
httpSession.removeAttribute("staff");
staffLoginResponse.setStatus(new Status(e.getCode(), e.getMessage()));
}
return ResponseEntity.ok(staffLoginResponse);
}
#PostMapping("/logout")
public ResponseEntity<Status> logout() {
httpSession.removeAttribute("staff");
return ResponseEntity.ok(new Status("Staff Logged Out Successfully"));
}
}
If you are using Spring Security, you can create a custom "/login" endpoint that authenticates the user by setting the SecurityContext.
You can use the default logout behaviour provided by Spring Security.
If you do not need to supply the credentials in the body, you can use the default login behaviour provided by Spring Security and omit this Controller altogether.
This is intended as a starting point.
It does not offer comprehensive security, for example it may be vulnerable session fixation attacks.
#RestController
public class LoginController {
private AuthenticationManager authenticationManager;
public LoginController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#PostMapping("/login")
public void login(#RequestBody StaffLoginBody body, HttpServletRequest request) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(body.getUsername(), body.getPassword());
Authentication auth = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(auth);
HttpSession session = request.getSession();
session.setAttribute("staff", "staff_value");
}
#GetMapping("/jobs")
public String getStaffJobs(HttpServletRequest request) {
return request.getSession().getAttribute("staff").toString();
}
}
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// expose AuthenticationManager bean to be used in Controller
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
// use built in logout
.logout(logout -> logout
.deleteCookies("PTSESSIONID")
);
}
}
You will need to add the Spring Security dependency to use this code org.springframework.boot:spring-boot-starter-security.
I am writing an application with spring messaging and stomp and rabbitmq. My application already sends messages from the browser to rabbitmq and back. But i dont want the predefined rabbitmq queue names based on the session id. I want to change the session id on connect. This is what i tried:
#Component
public class MyListener {
private Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
#EventListener
public void x(SessionConnectEvent event) {
Map<String, Object> headers = event.getMessage().getHeaders();
String id = headers.get("simpSessionId").toString();
logger.info("My current session id is " + id);
headers.put("sessionId", "fred");
}
}
Error is: the map is immutable
You need to update the sessionId before the handshake is done between client <-> server, that is when the headers attributes are defined.
On the other hand, the listener SessionConnectEvent is executed only after the handshake is done.
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
attributes.put("sessionId", "mySessiond");
}
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
}
}
Also don't forget to register the interceptor on the specific endpoint
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/greeting").addInterceptors(new HttpHandshakeInterceptor());
}
Changing the session ID was not the correct way. I used ServletFilter for Cookie and Security Checks and #SendTo for the correct use of rabbitmq queues.
You can change the session id by creating a Principal for each handshake and then you can
target each connected session with the provided username :
class CustomHandshake extends DefaultHandshakeHandler {
#Override
public Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
principal = new AnonymousPrincipal();
String uniqueName = UUID.randomUUID().toString();
((AnonymousPrincipal) principal).setName(uniqueName);
}
return principal;
}
}
Do not forget to register the handler as below :
.setHandshakeHandler(new CustomHandshake())
hope this is helpful
In liferay 7, I am trying to adjust the user session after the login is successful. I used to build a hook for that, and it used to work just fine. But now I am unable to retrieve the session to inspect an attribute.
#Component(immediate = true, property = { "key=login.events.post" }, service = LifecycleAction.class)
public class TestSessionAdjustAction implements LifecycleAction {
#Override
public void processLifecycleEvent(LifecycleEvent lifecycleEvent) throws ActionException {
final HttpSession session = lifecycleEvent.getSession();
final TestAccessToken token = (TestAccessToken) session.getAttribute(TestWebkeys.TEST_ACCESS_TOKEN);
if (token != null) {
TestSessionUtil.put(token.getRefreshToken(), session);
}
}
}
The code is throwing a null pointer ex at:
session.getAttribute(TestWebkeys.TEST_ACCESS_TOKEN);
Poking around I fund that replacing my problematic line with
final HttpSession session = lifecycleEvent.getRequest().getSession();
solves the problem. (I hope this helps some one..cause, it took me a while)
It seems like the lifecycleEvent has in its class several constructors and the one being used here, before my method, sets the session to null...
public class LifecycleEvent {
public LifecycleEvent() {
this(null, null, null, null);
}
public LifecycleEvent(
HttpServletRequest request, HttpServletResponse response) {
this(null, request, response, null);
}
public LifecycleEvent(HttpSession session) {
this(null, null, null, session);
}
public LifecycleEvent(String[] ids) {
this(ids, null, null, null);
}
public LifecycleEvent(
String[] ids, HttpServletRequest request, HttpServletResponse response,
HttpSession session) {
_ids = ids;
_request = request;
_response = response;
_session = session;
}
public String[] getIds() {
return _ids;
}
public HttpServletRequest getRequest() {
return _request;
}
public HttpServletResponse getResponse() {
return _response;
}
public HttpSession getSession() {
return _session;
}
private final String[] _ids;
private final HttpServletRequest _request;
private final HttpServletResponse _response;
private final HttpSession _session;}
I want to add common used attribute in model for every controller.
HandlerInterceptorAdapter can be used to intercept the request. For example, you can override preHandle to validate the session, and add the user to model in postHandle.
public class SessionValidator extends HandlerInterceptorAdapter{
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if (session == null || session.getAttribute("user") == null) {
return false;
}
return super.preHandle(request, response, handler);
}
#Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
HttpSession session = request.getSession();
if (modelAndView != null) {
modelAndView.getModelMap().addAttribute("user", session.getAttribute("user"));
}
}
}
I'm in the process of setting up Spring Security. My CookieAuthenticationFilter should make sure to keep users out unless they have a cookie with an UUID we accept. Although CookieAuthenticationFilter sets an empty context if the UUID is not accepted I still have access to all URLs.
Any idea what's missing?
This is my security configuration:
#Configuration
#EnableWebMvcSecurity
public class LIRSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilter(cookieAuthenticationFilter())
.authorizeRequests()
.antMatchers("/**").hasAnyAuthority("ALL");
}
#Bean
public CookieAuthenticationFilter cookieAuthenticationFilter() {
return new CookieAuthenticationFilter(cookieService());
}
private CookieService cookieService() {
return new CookieService.Impl();
}
#Bean(name = "springSecurityFilterChain")
public FilterChainProxy getFilterChainProxy() {
SecurityFilterChain chain = new SecurityFilterChain() {
#Override
public boolean matches(HttpServletRequest request) {
// All goes through here
return true;
}
#Override
public List<Filter> getFilters() {
List<Filter> filters = new ArrayList<Filter>();
filters.add(cookieAuthenticationFilter());
return filters;
}
};
return new FilterChainProxy(chain);
}
}
This is the CookieAuthenticationFilter implementation:
public class CookieAuthenticationFilter extends GenericFilterBean {
#Resource
protected AuthenticationService authenticationService;
private CookieService cookieService;
public CookieAuthenticationFilter(CookieService cookieService) {
super();
this.cookieService = cookieService;
}
#Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
UUID uuid = cookieService.extractUUID(request.getCookies());
UserInfo userInfo = authenticationService.findBySessionKey(uuid);
SecurityContext securityContext = null;
if (userInfo != null) {
securityContext = new CookieSecurityContext(userInfo);
SecurityContextHolder.setContext(securityContext);
} else {
securityContext = SecurityContextHolder.createEmptyContext();
}
try {
SecurityContextHolder.setContext(securityContext);
chain.doFilter(request, response);
}
finally {
// Free the thread of the context
SecurityContextHolder.clearContext();
}
}
}
The issue here is that you don't want to use GenericFilterBean as it's not actually part of the Spring Security framework, just regular Spring so it's not aware of how to send security-related messages back to the browser or deny access, etc. If you do want to use the GenericFilterBean you'll need to handle the redirect or the 401 response yourself. Alternatively, look into the AbstractPreAuthenticatedProcessingFilter that is part of the Spring Security framework. There is some documentation here: http://docs.spring.io/spring-security/site/docs/3.0.x/reference/preauth.html