I am using a scheduled service in spring boot app , i need to get the current connected user inside that service , my problem is that
SecurityContextHolder.getContext().getAuthentication()
returns the current connected user only once ( just after i am logged in ) , but in the next running tasks
SecurityContextHolder.getContext().getAuthentication()
returns NPE , i have searched and found that SecurityContextHolder is not shared outside the main thread.
My Service :
@Service
@EnableScheduling
public class SsePushNotificationService {
public void addEmitter(final SseEmitter emitter) {
emitters.add(emitter);
}
public void removeEmitter(final SseEmitter emitter) {
emitters.remove(emitter);
}
@Async("taskExecutor")
@Scheduled(fixedDelay = 5000)
public void doNotify() throws IOException {
System.out.println("------@@@@@ inside doNotify");
System.out.println("##### ---- curent thread /notification : " + Thread.currentThread().getName());
if (SecurityContextHolder.getContext().getAuthentication() != null) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
System.out.println("------@@@@@ principal instanceof UserDetails : " + username);
} else {
String username = principal.toString();
System.out.println("------@@@@@ principal : " + username);
}
}
}
}
the controller :
@Controller
@CrossOrigin(origins = "*")
public class SsePushNotificationRestController {
@Autowired
SsePushNotificationService service;
@Autowired
UserDetailsServiceImpl userService;
@Autowired
UserNotificationService userNotifService;
final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
String username;
int nbrEvent;
@GetMapping(value = "/notification", produces = { MediaType.TEXT_EVENT_STREAM_VALUE })
public ResponseEntity<SseEmitter> doNotify() throws InterruptedException, IOException {
System.out.println("##### ---- curent thread /notification : " + Thread.currentThread().getName());
final SseEmitter emitter = new SseEmitter();
service.addEmitter(emitter);
service.doNotify();
emitter.onCompletion(() -> service.removeEmitter(emitter));
emitter.onTimeout(() -> service.removeEmitter(emitter));
return new ResponseEntity<>(emitter, HttpStatus.OK);
}
}
Javascript :
const eventSource = new EventSource('http://localhost:8080/notification');
eventSource.onmessage = e => {
const msg = e.data;
$("#notifCounter").text(msg);
$("#usrNotifCounter").text(msg);
};
eventSource.onopen = e => console.log('open');
eventSource.onerror = e => {
if (e.readyState == EventSource.CLOSED) {
console.log('close');
}
else {
console.log(e);
}
};
eventSource.addEventListener('second', function(e) {
console.log('second', e.data);
}, false);
WebSecurityConfig :
@Configuration
@EnableWebSecurity
public class WebSecurityConfig<S extends Session> extends WebSecurityConfigurerAdapter {
@Autowired
private FindByIndexNameSessionRepository<S> sessionRepository;
@Autowired
private MySessionExpiredStrategy sessionExpiredStrategy;
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Component
public class MySessionExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(
"Your account has been logged in elsewhere, and the current login has expired. If the password is leaked, please change it immediately!");
}
}
@Bean
public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
@Bean
public ConcurrentSessionControlAuthenticationStrategy sessionControlAuthenticationStrategy() {
ConcurrentSessionControlAuthenticationStrategy csas = new ConcurrentSessionControlAuthenticationStrategy(
sessionRegistry());
csas.setExceptionIfMaximumExceeded(true);
return csas;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/img/**", "/error");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
.anyRequest().access("@rbacService.hasPermission(request,authentication)")
.and().formLogin().loginPage("/login").defaultSuccessUrl("/", true).permitAll().and().logout()
.deleteCookies("JSESSIONID").invalidateHttpSession(true).clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/login?logout")
.permitAll().and().exceptionHandling().accessDeniedPage("/static/403")
.and().sessionManagement().sessionFixation().migrateSession()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/static/invalidSession.html").maximumSessions(2).maxSessionsPreventsLogin(false)
.expiredSessionStrategy(sessionExpiredStrategy).sessionRegistry(sessionRegistry())
.expiredUrl("/login?invalid-session=true");
}
}
what is the best approch to share SecurityContextHolder between threads in that case.