I am working on a Spring-MVC application in which we have Spring-security for authentication and authorization. We are working on migrating to Spring websockets, but we are having an issue with getting the authenticated user inside a websocket connection. The security context simply doesn't exist in the websocket connection, but works fine with regular HTTP. What are we doing wrong?
WebsocketConfig :
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
}
In the controller below, we are trying to get the currently authenticated user and it's always null
#Controller
public class OnlineStatusController extends MasterController{
#MessageMapping("/onlinestatus")
public void onlineStatus(String status) {
Person user = this.personService.getCurrentlyAuthenticatedUser();
if(user!=null){
this.chatService.setOnlineStatus(status, user.getId());
}
}
}
security-applicationContext.xml :
<security:http pattern="/resources/**" security="none"/>
<security:http pattern="/org/**" security="none"/>
<security:http pattern="/jquery/**" security="none"/>
<security:http create-session="ifRequired" use-expressions="true" auto-config="false" disable-url-rewriting="true">
<security:form-login login-page="/login" username-parameter="j_username" password-parameter="j_password"
login-processing-url="/j_spring_security_check" default-target-url="/canvaslisting"
always-use-default-target="false" authentication-failure-url="/login?error=auth"/>
<security:remember-me key="_spring_security_remember_me" user-service-ref="userDetailsService"
token-validity-seconds="1209600" data-source-ref="dataSource"/>
<security:logout delete-cookies="JSESSIONID" invalidate-session="true" logout-url="/j_spring_security_logout"/>
<security:csrf disabled="true"/>
<security:intercept-url pattern="/cometd/**" access="permitAll" />
<security:intercept-url pattern="/app/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<!-- <security:intercept-url pattern="/**" requires-channel="https"/>-->
<security:port-mappings>
<security:port-mapping http="80" https="443"/>
</security:port-mappings>
<security:logout logout-url="/logout" logout-success-url="/" success-handler-ref="myLogoutHandler"/>
<security:session-management session-fixation-protection="newSession">
<security:concurrency-control session-registry-ref="sessionReg" max-sessions="5" expired-url="/login"/>
</security:session-management>
</security:http>
I remember stumbling across the very same problem in a project I was working on. As I could not figure out the solution using the Spring documentation - and other answers on Stack Overflow were not working for me - I ended up creating a workaround.
The trick is essentially to force the application to authenticate the user on a WebSocket connection request. To do that, you need a class which intercepts such events and then once you have control of that, you can call your authentication logic.
Create a class which implements Spring's ChannelInterceptorAdapter. Inside this class, you can inject any beans you need to perform the actual authentication. My example uses basic auth:
#Component
public class WebSocketAuthInterceptorAdapter extends ChannelInterceptorAdapter {
#Autowired
private DaoAuthenticationProvider userAuthenticationProvider;
#Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
StompCommand cmd = accessor.getCommand();
if (StompCommand.CONNECT == cmd || StompCommand.SEND == cmd) {
Authentication authenticatedUser = null;
String authorization = accessor.getFirstNativeHeader("Authorization:");
String credentialsToDecode = authorization.split("\\s")[1];
String credentialsDecoded = StringUtils.newStringUtf8(Base64.decodeBase64(credentialsToDecode));
String[] credentialsDecodedSplit = credentialsDecoded.split(":");
final String username = credentialsDecodedSplit[0];
final String password = credentialsDecodedSplit[1];
authenticatedUser = userAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
if (authenticatedUser == null) {
throw new AccessDeniedException();
}
SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
accessor.setUser(authenticatedUser);
}
return message;
}
}
Then, in your WebSocketConfig class, you need to register your interceptor. Add the above class as a bean and register it. After these changes, your class would look like this:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Autowired
private WebSocketAuthInterceptorAdapter authInterceptorAdapter;
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(authInterceptorAdapter);
super.configureClientInboundChannel(registration);
}
}
Obviously, the details of the authentication logic are up to you. You can call a JWT service or whatever you are using.
If you are using SockJS + Stomp and configured your security correctly, you should be able to connect via regular username/pw authenticator like #AlgorithmFromHell and do
accessor.setUser(authentication.getPrincipal()) // stomp header accessor
You can also connect via http://{END_POINT}/access_token={ACCESS_TOKEN}. Spring security should be able to pick it and do loadAuthentication(access_token) via ResourceServerTokenServices. When this is done, you can get your principal by adding this to your impl of AbstractSessionWebSocketMessageBrokerConfigurer or WebSocketMessageBrokerConfigurer. When doing this, for some reason, the loaded Pricipal is saved in "simpUser" header instead.
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
if (message.getHeaders().get("simpUser") != null && message.getHeaders().get("simpUser") instanceof OAuth2Authentication) { // or Authentication depending on your impl of security
OAuth2Authentication authentication = (OAuth2Authentication) message.getHeaders().get("simpUser");
accessor.setUser(authentication != null ? (UserDetails) authentication.getPrincipal() : null);
}
}
return message;
}
});
}
Related
I am trying to intercept the login request before spring security start its processing or it reaches to spring security interceptor. I want to do this because I have to do some validations on the user's input. So, I have created a interceptor from HandlerInterceptorAdapter class but it is not working.
So what I want to do is :
When any user try to login with username and password then this request must reaches to LoginRequestInterceptor first. Do some validation on user inputs and on success pass this request to spring security login processing url other wise to some other url.
But in my case the request is reaching directly to the spring security without visiting the interceptor.
Interceptor class
public class LoginRequestInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = LogManager.getLogger(LoginRequestInterceptor.class);
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
logger.debug("Interceptor working pre");
return super.preHandle(request, response, handler);
}
Spring Security xml
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/Candidate/**" access="hasRole('ROLE_USER')" />
<!-- access denied page -->
<access-denied-handler error-page="/403" />
<form-login
login-processing-url="/login"
login-page="/"
default-target-url="/Candidate"
authentication-failure-url="/error=1"
username-parameter="registerationid"
password-parameter="password"
/>
<logout logout-success-url="/?logout" />
<custom-filter ref="loginRequestInterceptors" position="FIRST"/>
<!-- enable csrf protection -->
<csrf />
</http>
<authentication-manager id="authenticationManager">
<authentication-provider user-service-ref="candidateDetailServices" />
</authentication-manager>
Spring dispatcher xml
<bean id="loginRequestInterceptor" class="org.ibps.clerk.inteceptors.login.LoginRequestInterceptor"></bean>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/login"/>
<ref bean="loginRequestInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
</beans>
So at last I wanted to know whether it is possible or not? If Yes then please share the link or solution.
You should create a class (for example CustomFilter) extend UsernamePasswordAuthenticationFilter and over ride attemptAuthentication and do your stuff in it and then call super.attemptAuthentication;
Then you should confgiure in your security config class which extends WebSecurityConfigurerAdapter.
#Configuration
#EnableWebSecurity
public class SecurityConfigs extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(getCustomFilter()
,UsernamePasswordAuthenticationFilter.class)
.formLogin()
.and() // more configs ...
}
public CustomFilter getCustomFilter() throws Exception {
CustomFilter filter= new CustomFilter ("/loginuser","POST");
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationFailureHandler((request, response, exception) -> {
response.sendRedirect("/login?error");
});
return filter;
}
And your CustomFilter class:
public class CustomFilter extends UsernamePasswordAuthenticationFilter {
public CustomFilter (String urlLogin, String method) {
setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(urlLogin, method));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// do your stuff ...
return super.attemptAuthentication(request, response);
}
}
What you're probably looking to do is create a servlet filter that is executed before the Spring Security filterchain. By default the springSecurityFilterChain filter's order value is set to 0, meaning that it is executed before all other filters. One workaround for this is to set the security.filter-order to a higher value than that of the filter you wish to run before its execution in your properties file.
Check Filter order in spring-boot for further discussion on this topic.
Additional resources:
https://github.com/spring-projects/spring-security-oauth/issues/1024
https://mtyurt.net/2015/07/15/spring-how-to-insert-a-filter-before-springsecurityfilterchain/
I'm writing spring application to serve mobile as well as web portal requests.
I have added Controller to handle web portal requests and RestController to handle mobile requests. This all stuff I have done in single project.
I've configured auth.xml for authetication and all.
<security:http pattern="/api/**" entry-point-ref="restAuthenticationEntryPoint" use-expressions="true" auto-config="false" create-session="stateless" >
<security:intercept-url pattern="/api/auth" access="permitAll" />
<security:intercept-url pattern="/api/token" access="permitAll" />
<security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
<security:intercept-url pattern="/api/**" access="isAuthenticated()" />
<security:logout />
</security:http>
<bean class="com.auth.TokenAuthenticationFilter"
id="authenticationTokenProcessingFilter">
<constructor-arg type="java.lang.String"><value>/api/**</value></constructor-arg>
</bean>
<!-- Code for REST API Authentication -->
<!-- create-session="stateless" -->
<security:http auto-config="false" use-expressions="true" entry-point-ref="ajaxAwareAuthenticationEntryPoint" disable-url-rewriting="true">
<security:intercept-url pattern="/login" access="permitAll()" />
<security:intercept-url pattern="/**" access="isAuthenticated()" />
<security:custom-filter position="FORM_LOGIN_FILTER" ref="authenticationFilter" />
<security:custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<security:logout logout-url="/logout" logout-success-url="/login.do" invalidate-session="true" />
<security:remember-me services-ref="rememberMeService" />
<security:session-management session-authentication-strategy-ref="sas" />
<security:csrf disabled="true"/>
</security:http>
But I want to integrate Spring OAuth 2.0 in it.
Can anyone has idea about the same ?
You can configure 2 different security filters for 2 different paths. That way, you can have differents paths of you application secured differently. Typically, you would want to have "/public/" accessible to anyone while "/api/" being secured by authentication.
I would strongly recommend to configure Spring Security in Java by extending WebSecurityConfigurerAdapter.
Here is an example Java configuration which protects only some endpoints while leaving others accessible publicly.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/**", OPTIONS.toString()),
new AntPathRequestMatcher("/public/**"),
new AntPathRequestMatcher("/health"),
// Spring Social
new AntPathRequestMatcher("/signin/**"),
new AntPathRequestMatcher("/auth/**"),
// Swagger Documentation
new AntPathRequestMatcher("/swagger-ui.html"),
new AntPathRequestMatcher("/v2/api-docs"),
new AntPathRequestMatcher("/swagger-resources/**"),
new AntPathRequestMatcher("/webjars/**")
);
private static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
#Autowired
private RESTAuthenticationProvider authenticationProvider;
#Autowired
private TokenService credentials;
#Autowired
private UserSecurityService users;
#Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(final WebSecurity web) throws Exception {
web.ignoring().requestMatchers(PUBLIC_URLS);
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet
// authenticated
.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable()
.sessionManagement().disable();
}
#Bean
RESTAuthenticationFilter restAuthenticationFilter() throws Exception {
final RESTAuthenticationFilter filter =
new RESTAuthenticationFilter(PROTECTED_URLS, credentials);
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationSuccessHandler(getSuccessHandler());
return filter;
}
// Upon successful authentication, Spring will attempt to try and move you to another URL
// We have to prevent this because the request for the resource and the authentication all get done in the same request!
#Bean
SimpleUrlAuthenticationSuccessHandler getSuccessHandler() {
final SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setRedirectStrategy(new NoRedirectStrategy());
return successHandler;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new Http401AuthenticationEntryPoint("Bearer");
}
}
Try out spring security. It has built in functionalities also you can always override existing behavior for your purposes.
Here is my spring security config:
<http pattern="/auth/login" security="none" />
<http pattern="/auth/loginFailed" security="none" />
<http pattern="/resources/**" security="none" />
<http auto-config="true" access-decision-manager-ref="accessDecisionManager">
<intercept-url pattern="/auth/logout" access="permitAll"/>
<intercept-url pattern="/admin/**" access="ADMINISTRATIVE_ACCESS"/>
<intercept-url pattern="/**" access="XYZ_ACCESS"/>
<form-login
login-page="/auth/login"
authentication-failure-url="/auth/loginFailed"
authentication-success-handler-ref="authenticationSuccessHandler" />
<logout logout-url="/auth/logout" logout-success-url="/auth/login" />
</http>
The authenticationSuccessHandler extends the SavedRequestAwareAuthenticationSuccessHandler ensuring that the user is redirected to the page he originally requested.
However, since /auth/login is marked as security="none", I am unable to successfully redirect the user to the homepage if he accesses the login page after being logged in. I believe this is the right user experience too.
I tried the below too but the Principal object is always null, presumably because of the security="none" attribute again.
#RequestMapping(value = "/auth/login", method = GET)
public String showLoginForm(HttpServletRequest request, Principal principal) {
if(principal != null) {
return "redirect:/";
}
return "login";
}
I've checked the topic more deeply than last time and found that you have to determine if user is authenticated by yourself in controller. Row Winch (Spring Security dev) says here:
Spring Security is not aware of the internals of your application
(i.e. if you want to make your login page flex based upon if the user
is logged in or not). To show your home page when the login page is
requested and the user is logged in use the SecurityContextHolder in
the login page (or its controller) and redirect or forward the user to
the home page.
So solution would be determining if user requesting /auth/login is anonymous or not, something like below.
applicationContext-security.xml:
<http auto-config="true" use-expressions="true"
access-decision-manager-ref="accessDecisionManager">
<intercept-url pattern="/auth/login" access="permitAll" />
<intercept-url pattern="/auth/logout" access="permitAll" />
<intercept-url pattern="/admin/**" access="ADMINISTRATIVE_ACCESS" />
<intercept-url pattern="/**" access="XYZ_ACCESS" />
<form-login login-page="/auth/login"
authentication-failure-url="/auth/loginFailed"
authentication-success-handler-ref="authenticationSuccessHandler" />
<logout logout-url="/auth/logout" logout-success-url="/auth/login" />
</http>
<beans:bean id="defaultTargetUrl" class="java.lang.String">
<beans:constructor-arg value="/content" />
</beans:bean>
<beans:bean id="authenticationTrustResolver"
class="org.springframework.security.authentication.AuthenticationTrustResolverImpl" />
<beans:bean id="authenticationSuccessHandler"
class="com.example.spring.security.MyAuthenticationSuccessHandler">
<beans:property name="defaultTargetUrl" ref="defaultTargetUrl" />
</beans:bean>
Add to applicationContext.xml bean definition:
<bean id="securityContextAccessor"
class="com.example.spring.security.SecurityContextAccessorImpl" />
which is class
public final class SecurityContextAccessorImpl
implements SecurityContextAccessor {
#Autowired
private AuthenticationTrustResolver authenticationTrustResolver;
#Override
public boolean isCurrentAuthenticationAnonymous() {
final Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
return authenticationTrustResolver.isAnonymous(authentication);
}
}
implementing simple interface
public interface SecurityContextAccessor {
boolean isCurrentAuthenticationAnonymous();
}
(SecurityContextHolder accessing code is decoupled from controller, I followed suggestion from this answer, hence SecurityContextAccessor interface.)
And last but not least redirect logic in controller:
#Controller
#RequestMapping("/auth")
public class AuthController {
#Autowired
SecurityContextAccessor securityContextAccessor;
#Autowired
#Qualifier("defaultTargetUrl")
private String defaultTargetUrl;
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
if (securityContextAccessor.isCurrentAuthenticationAnonymous()) {
return "login";
} else {
return "redirect:" + defaultTargetUrl;
}
}
}
Defining defaultTargetUrl String bean seems like a hack, but I don't have better way not to hardcode url... (Actually in our project we use <util:constant> with class containing static final String fields.) But it works after all.
You could also restrict your login page to ROLE_ANONYMOUS and set an <access-denied-handler />:
<access-denied-handler ref="accessDeniedHandler" />
<intercept-url pattern="/auth/login" access="ROLE_ANONYMOUS" />
And in your handler check if the user is already authenticated:
#Service
public class AccessDeniedHandler extends AccessDeniedHandlerImpl {
private final String HOME_PAGE = "/index.html";
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && !(auth instanceof AnonymousAuthenticationToken)) {
response.sendRedirect(HOME_PAGE);
}
super.handle(request, response, e);
}
}
Implement a Redirect Interceptor for this purpose:
The Interceptor (implementing HandlerInterceptor interface) check if someone try to access the login page, and if this person is already logged in, then the interceptor sends a redirect to the index page.
public class LoginPageRedirectInterceptor extends HandlerInterceptorAdapter {
private String[] loginPagePrefixes = new String[] { "/login" };
private String redirectUrl = "/index.html";
private UrlPathHelper urlPathHelper = new UrlPathHelper();
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (isInLoginPaths(this.urlPathHelper.getLookupPathForRequest(request))
&& isAuthenticated()) {
response.setContentType("text/plain");
sendRedirect(request, response);
return false;
} else {
return true;
}
}
private boolean isAuthenticated() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
if (authentication instanceof AnonymousAuthenticationToken) {
return false;
}
return authentication.isAuthenticated();
}
private void sendRedirect(HttpServletRequest request,
HttpServletResponse response) {
String encodedRedirectURL = response.encodeRedirectURL(
request.getContextPath() + this.redirectUrl);
response.setStatus(HttpStatus.SC_TEMPORARY_REDIRECT);
response.setHeader("Location", encodedRedirectURL);
}
private boolean isInLoginPaths(final String requestUrl) {
for (String login : this.loginPagePrefixes) {
if (requestUrl.startsWith(login)) {
return true;
}
}
return false;
}
}
You can keep it simple flow by access-denied-page attribute in http element or as dtrunk said to write handler for access denied as well as. the config would be like
<http access-denied-page="/403" ... >
<intercept-url pattern="/login" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/user/**" access="ROLE_USER" />
<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<form-login login-page="/login" default-target-url="/home" ... />
...
</http>
in controller for /403
#RequestMapping(value = "/403", method = RequestMethod.GET)
public String accessDenied() { //simple impl
return "redirect:/home";
}
and for /home
#RequestMapping(value = "/home", method = RequestMethod.GET)
public String home(Authentication authentication) {
// map as many home urls with Role
Map<String, String> dashBoardUrls = new HashMap<String, String>();
dashBoardUrls.put("ROLE_USER", "/user/dashboard");
dashBoardUrls.put("ROLE_ADMIN", "/admin/dashboard");
String url = null;
Collection<? extends GrantedAuthority> grants = authentication
.getAuthorities();
// for one role per user
for (GrantedAuthority grantedAuthority : grants) {
url = dashBoardUrls.get(grantedAuthority.getAuthority());
}
if (url == null)
return "/errors/default_access_denied.jsp";
return "redirect:" + url;
}
and when you make request for /admin/dashboard without logged in, it will redirect /login automatically by security
<http pattern="/login" auto-config="true" disable-url-rewriting="true">
<intercept-url pattern="/login" access="ROLE_ANONYMOUS"/>
<access-denied-handler error-page="/index.jsp"/>
</http>
You can try checking
if(SecurityContextHolder.getContext().getAuthentication() == null)
True means the user isn't authenticated, and thus can be sent to the login page. I don't know how robust/reliable this is, but it seems reasonable to try.
I'm using the spring-security-javaconfig library for spring security. If I were using xml config files, I'd use something like this to define a custom Access Denied page:
<http auto-config="true">
<intercept-url pattern="/admin*" access="ROLE_ADMIN" />
<access-denied-handler ref="accessDeniedHandler"/>
</http>
Here is my security configuration class so far:
#Configuration
#EnableWebSecurity
public class SecurityConfigurator extends WebSecurityConfigurerAdapter {
#Override
protected void registerAuthentication(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
auth.inMemoryAuthentication().withUser("admin").password("password").roles("ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeUrls().antMatchers( "/admin").hasRole("ADMIN");
}
}
I suppose this should do the trick:
HttpSecurity http = ...
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
I'm using spring/spring-security 3.1 and want to take some action whenever the user logs out (or if the session is timed out). I managed to get the action done for logout but for session timeout, I can't get it working.
In web.xml I only have the ContextLoaderListener specified ( can this be the issue? ) and of course the DelegatingFilterProxy.
I use the auto config like this.
<security:http auto-config="false" use-expressions="false">
<security:intercept-url pattern="/dialog/*"
access="ROLE_USERS" />
<security:intercept-url pattern="/boa/*"
access="ROLE-USERS" />
<security:intercept-url pattern="/*.html"
access="ROLE-USERS" />
<security:form-login login-page="/auth/login.html"
default-target-url="/index.html" />
<security:logout logout-url="/logout"
invalidate-session="true"
delete-cookies="JSESSIONID" success-handler-ref="logoutHandler" />
</security:http>
<bean id="logoutHandler" class="com.bla.bla.bla.LogoutHandler">
<property name="logoutUrl" value="/auth/logout.html"/>
</bean>
The logout handler is called when user clicks logout, which will make some calls to a database.
But how do I handle the session timeout ???
One way to handle it would be to inject the username into the session when user logs in and then use an ordinary httpsessionlistener and do the same thing on session timeout.
Is there a similar way with spring security, so that when spring discovers that the session is to timeout, I can hook in there, access the Authentication and get the UserDetails from there and do the clean up.
I've got a simpler solution. This works for both logout and session timeout.
#Component
public class LogoutListener implements ApplicationListener<SessionDestroyedEvent> {
#Override
public void onApplicationEvent(SessionDestroyedEvent event)
{
List<SecurityContext> lstSecurityContext = event.getSecurityContexts();
UserDetails ud;
for (SecurityContext securityContext : lstSecurityContext)
{
ud = (UserDetails) securityContext.getAuthentication().getPrincipal();
// ...
}
}
}
web.xml:
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
Ok, I got a solution working, it's not as good as I'd like, but it get's me to the result.
I create a bean from which I can get a hold of the ApplicationContext.
public class AppCtxProvider implements ApplicationContextAware {
private static WeakReference<ApplicationContext> APP_CTX;
#Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
APP_CTX = new WeakReference<ApplicationContext>(applicationContext);
}
public static ApplicationContext getAppCtx() {
return APP_CTX.get();
}
}
I implement HttpSessionEventPublisher and on destroy, i get the UserDetails via sessionRegistry.getSessionInfo(sessionId)
Now I have the spring beans which I need to do the cleanup of the session and the user for whom the session timed out for.
public class SessionTimeoutHandler extends HttpSessionEventPublisher {
#Override
public void sessionCreated(HttpSessionEvent event) {
super.sessionCreated(event);
}
#Override
public void sessionDestroyed(HttpSessionEvent event) {
SessionRegistry sessionRegistry = getSessionRegistry();
SessionInformation sessionInfo = (sessionRegistry != null ? sessionRegistry
.getSessionInformation(event.getSession().getId()) : null);
UserDetails ud = null;
if (sessionInfo != null) {
ud = (UserDetails) sessionInfo.getPrincipal();
}
if (ud != null) {
// Do my stuff
}
super.sessionDestroyed(event);
}
private SessionRegistry getSessionRegistry() {
ApplicationContext appCtx = AppCtxProvider.getAppCtx();
return appCtx.getBean("sessionRegistry", SessionRegistry.class);
}
You can use SimpleRedirectInvalidSessionStrategy to redirect to a URL when an invalid requested session is detected by the SessionManagementFilter.
Sample applicationContext would be like this:
<http>
<custom-filter ref="sessionManagementFilter" before="SESSION_MANAGEMENT_FILTER" />
<http>
<beans:bean id="sessionManagementFilter" class="org.springframework.security.web.session.SessionManagementFilter">
<beans:constructor-arg name="securityContextRepository" ref="httpSessionSecurityContextRepository" />
<beans:property name="invalidSessionStrategy" ref="simpleRedirectInvalidSessionStrategy " />
</beans:bean>
<beans:bean id="simpleRedirectInvalidSessionStrategy" class="org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/general/logins/sessionExpired.jsf" />
<beans:property name="createNewSession" value="false" />
</beans:bean>
<beans:bean id="httpSessionSecurityContextRepository" class="org.springframework.security.web.context.HttpSessionSecurityContextRepository"/>
If you are using JSF, also refer to JSF 2, Spring Security 3.x and Richfaces 4 redirect to login page on session time out for ajax requests on how to handle Ajax requests as well.
UPDATE: In such a case, you can extend the HttpSessionEventPublisher and listen for sessionDestroyed events like this:
package com.examples;
import javax.servlet.http.HttpSessionEvent;
import org.springframework.security.web.session.HttpSessionEventPublisher;
public class MyHttpSessionEventPublisher extends HttpSessionEventPublisher {
#Override
public void sessionCreated(HttpSessionEvent event) {
super.sessionCreated(event);
}
#Override
public void sessionDestroyed(HttpSessionEvent event) {
//do something
super.sessionDestroyed(event);
}
}
and then register this listener in your web.xml like this:
<listener>
<listener-class>com.examples.MyHttpSessionEventPublisher</listener-class>
</listener>
While session getting created at the time of one microservice application with setMaxInactive interval to 60 sec suppose . Session gets timedout after 60sec but after using your suggested change it should come to sessionDestroyed event but still it gets under session created and session again doesnot gets invalidated.