I'm trying to handle 404 error using an #ControllerAdvice in a Spring MVC application totally configured using Javaconfig.
Spring MVC version is 4.1.5
I have read this:
Stackoverflow Example 1
Stackoverflow Example 1 continuation
But unfortunately it does not work for me.
Here you have my conf:
SpringConfigurationInitializer
public class SpringConfigurationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { AppConfiguration.class };
}
#Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
#Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
#Override
public void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
}
}
Note that i'm using
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
And
GlobalExceptionHandler (version 1)
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(NoHandlerFoundException.class)
public ModelAndView handleError404(HttpServletRequest request, Exception e) {
System.out.println("handled!!!");
ModelAndView mav = new ModelAndView("/errors/404");
mav.addObject("exception", e);
return mav;
}
}
GlobalExceptionHandler (version 2)
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
public ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handled¡¡¡");
return null;
}
}
Keep in mind that i'm not using any kind of xml config file and i'm trying to build a web application (not REST)
AppConfiguration
#Configuration
#ComponentScan({ "org.moyanojv.opendata.*" })
#Import({ MvcConfiguration.class, RepositoryConfiguration.class, SecurityConfig.class })
public class AppConfiguration extends WebMvcConfigurerAdapter{
}
MvcConfiguration
#EnableWebMvc
#Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter {
#Bean
public UrlBasedViewResolver viewResolver() {
UrlBasedViewResolver viewResolver = new UrlBasedViewResolver();
viewResolver.setViewClass(TilesView.class);
return viewResolver;
}
#Bean
public TilesConfigurer tilesConfigurer() {
TilesConfigurer tilesConfigurer = new TilesConfigurer();
tilesConfigurer.setDefinitions(new String[] { "/WEB-INF/tiles.xml" });
tilesConfigurer.setCheckRefresh(true);
return tilesConfigurer;
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
#Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
/* Localization section is started */
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor(){
LocaleChangeInterceptor localeChangeInterceptor=new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
return localeChangeInterceptor;
}
#Bean(name = "localeResolver")
public LocaleResolver getLocaleResolver(){
return new CookieLocaleResolver();
}
#Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasename("i18n/messages");
source.setUseCodeAsDefaultMessage(true);
return source;
}
}
RepositoryConfiguration
#Configuration
public class RepositoryConfiguration {
}
SecurityConfig
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
auth.inMemoryAuthentication().withUser("user").password("user").roles("USER");
}
#Override
public void configure( WebSecurity web ) throws Exception
{
// This is here to ensure that the static content (JavaScript, CSS, etc)
// is accessible from the login page without authentication
web
.ignoring()
.antMatchers( "/resources/**" );
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// access-denied-page: this is the page users will be
// redirected to when they try to access protected areas.
.exceptionHandling()
.accessDeniedPage( "/403" )
.and()
// The intercept-url configuration is where we specify what roles are allowed access to what areas.
// We specifically force the connection to https for all the pages, although it could be sufficient
// just on the login page. The access parameter is where the expressions are used to control which
// roles can access specific areas. One of the most important things is the order of the intercept-urls,
// the most catch-all type patterns should at the bottom of the list as the matches are executed
// in the order they are configured below. So /** (anyRequest()) should always be at the bottom of the list.
.authorizeRequests()
.antMatchers( "/admin" ).hasRole("ADMIN")
.antMatchers("/login**").permitAll()
.antMatchers("/home").permitAll()
.antMatchers("/404").permitAll()
.anyRequest().authenticated()
.and()
// This is where we configure our login form.
// login-page: the page that contains the login screen
// login-processing-url: this is the URL to which the login form should be submitted
// default-target-url: the URL to which the user will be redirected if they login successfully
// authentication-failure-url: the URL to which the user will be redirected if they fail login
// username-parameter: the name of the request parameter which contains the username
// password-parameter: the name of the request parameter which contains the password
.formLogin()
.loginPage( "/login" )
.failureUrl( "/login?err=1" )
.defaultSuccessUrl("/private")
.usernameParameter( "username" )
.passwordParameter( "password" )
.permitAll()
.and()
// This is where the logout page and process is configured. The logout-url is the URL to send
// the user to in order to logout, the logout-success-url is where they are taken if the logout
// is successful, and the delete-cookies and invalidate-session make sure that we clean up after logout
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=1")
.invalidateHttpSession(true)
//.deleteCookies("JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE")
.and()
.csrf()
.disable()
// The session management is used to ensure the user only has one session. This isn't
// compulsory but can add some extra security to your application.
.sessionManagement()
.maximumSessions(1);
}
#Override
#Bean
public UserDetailsService userDetailsServiceBean() throws Exception
{
return super.userDetailsServiceBean();
}
}
SpringSecurityInitializer
public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer{
//do nothing
}
With this config i'm not able to handle 404 error code.
Thanks in advance.
Updated to add more information about config files
Conclusion seems to be that setting throwExceptionIfNoHandlerFound to true does not throw an exception when no handler is found.
The solution is quite simple. From the javadoc # DispatcherServlet.setThrowExceptionIfNoHandlerFound. It states here that a NoHandlerFoundException will never be thrown if DefaultServletHttpRequestHandler is used.
Solution hence is to remove the line
configurer.enable();
from your MvcConfiguration. The exception should fire now and your GlobalExceptionHandler should do the rest!
Workaround: Add #RequestMapping("/**")
#Controller
#ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
#RequestMapping("/**")
public String handlerNotMappingRequest(HttpServletRequest request, HttpServletResponse response, HttpHeaders httpHeaders)
throws NoHandlerFoundException {
throw new NoHandlerFoundException("No handler mapping found.", request.getRequestURL().toString(), httpHeaders);
}
#ExceptionHandler(Throwable.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ModelAndView handleControllerException(Throwable ex) {
logger.error("ErrorLog: ", ex);
return new ModelAndView("error/exception", "exceptionMsg", "ExceptionHandler msg: " + ex.toString());
}
#ExceptionHandler(NoHandlerFoundException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
public ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex) {
logger.error("ErrorLog: ", ex);
return new ModelAndView("error/exception", "exceptionMsg", "NoHandlerFoundException msg: " + ex.toString());
}
}
The solution is to extend AbstractAnnotationConfigDispatcherServletInitializer and override this method:
#Override
protected DispatcherServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
final DispatcherServlet dispatcherServlet = (DispatcherServlet) super.createDispatcherServlet(servletAppContext);
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
return dispatcherServlet;
}
OR this one:
#Override
public void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
}
And finally in your ControlerAdvice use this:
#ExceptionHandler(NoHandlerFoundException.class)
public String error404(Exception ex) {
return new ModelAndView("404");
}
Related
I have a Spring boot application, and I use Keycloak for security. Logging in and out works, but I'd like to have the confirmation dialog from Keycloak like this when the logout button is clicked. How can I get that? Right now, it logs out directly.
SecurityConfig class:
#Configuration
#EnableWebSecurity
public class SecurityConfigKeycloak {
private final KeycloakLogoutHandler keycloakLogoutHandler;
public SecurityConfigKeycloak(KeycloakLogoutHandler keycloakLogoutHandler) {
this.keycloakLogoutHandler = keycloakLogoutHandler;
}
#Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/someurl/**", "/anotherurl/**").permitAll()
.anyRequest()
.fullyAuthenticated();
http.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/");
return http.build();
}
}
Logout Handler:
#Slf4j
#Component
public class KeycloakLogoutHandler implements LogoutHandler {
private final RestTemplate restTemplate;
public KeycloakLogoutHandler(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {
logoutFromKeycloak((OidcUser) auth.getPrincipal());
}
private void logoutFromKeycloak(OidcUser user) {
log.debug("Entering logout...");
String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
ResponseEntity<String> logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
if (logoutResponse.getStatusCode().is2xxSuccessful()) {
log.info("Successfulley logged out from Keycloak");
} else {
log.error("Could not propagate logout to Keycloak");
}
}
}
Controller class:
#Slf4j
#RestController
#RequestMapping("/logout")
public class LogoutController {
#GetMapping(value = "/*")
public ResponseEntity logout(HttpServletRequest request) {
try {
request.logout();
return new ResponseEntity<>(HttpStatus.OK);
} catch (ServletException e) {
log.error(e.getMessage(), e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
I have a Spring project with Keycloak adapter configured and noticed that it loads openid-configuration for every request. Is there any mechanism to cache this configuration, or, why is this happening?
Could not understand this behavior and Keycloak Docs say nothing about this. As I saw the source code, It resolves this configuration when KeycloakDeployment object is created, so every time a request comes a new KeycloakDeployment object is created (see: Keycloak adapter source)
This is the log:
2020-06-25 08:31:36.103 INFO 1 --- [io-8080-exec-10] o.keycloak.adapters.KeycloakDeployment : Loaded URLs from https://mykeyloak.com/auth/realms/myrealm/.well-known/openid-configuration
Here is my Keycloak adapter configuration:
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private KeycloakProperties keycloakProperties;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public AdapterConfig adapterConfig() {
AdapterConfig adapterConfig = new AdapterConfig();
adapterConfig.setRealm(keycloakProperties.getRealm());
adapterConfig.setResource(keycloakProperties.getResource());
adapterConfig.setAuthServerUrl(keycloakProperties.getAuthServerUrl());
adapterConfig.setSslRequired(keycloakProperties.getSslRequired());
adapterConfig.setBearerOnly(keycloakProperties.getBearerOnly());
adapterConfig.setCredentials(keycloakProperties.getCredentials());
adapterConfig.setCors(keycloakProperties.getEnableCors());
adapterConfig.setUseResourceRoleMappings(keycloakProperties.getUseResourceRoleMappings());
return adapterConfig;
}
#Bean
public KeycloakConfigResolver keycloakConfigResolver(AdapterConfig adapterConfig) {
return new KeycloakConfigResolver() {
#Override
public KeycloakDeployment resolve(HttpFacade.Request request) {
return KeycloakDeploymentBuilder.build(adapterConfig);
}
};
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll();
}
#Autowired
public void setKeycloakProperties(KeycloakProperties keycloakProperties) {
this.keycloakProperties = keycloakProperties;
}
}
Keycloak properties:
keycloak.realm=myrealm
keycloak.resource=myclient
keycloak.auth-server-url=https://mykeycloak.com/auth
keycloak.ssl-required=external
keycloak.bearer-only=true
keycloak.credentials={}
keycloak.enable-cors=true
keycloak.use-resource-role-mappings=false
I had to register KeycloakDeployment bean and return that from KeycloakConfigResolver resolve method.
#Bean
public KeycloakDeployment keycloakDeployment(AdapterConfig adapterConfig) {
return KeycloakDeploymentBuilder.build(adapterConfig);
}
#Bean
public KeycloakConfigResolver keycloakConfigResolver(KeycloakDeployment keycloakDeployment) {
return request -> keycloakDeployment;
}
I am trying to implement a very simple example of customized authentication process in Spring to get a better understanding of the concept.
I thought i had everything in place now but sending a request to test what I implemented results in a NullPointerException which can be tracked down to
this.getAuthenticationManager() returning null in my custom filter. But I don't understand why. The very similar existing question didn't really help me unfortunately. So I would be thankful for help; here are the most relevant classes I think, feel free to ask if any more is needed.
MyAuthenticationFilter (Based on the source-code of UsernamePasswordAuthenticationFilter), error happens in last line of this:
public class MyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public MyAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = request.getParameter("username");
String password = request.getParameter("password");
String secondSecret = request.getParameter("secondSecret");
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
MyAuthenticationToken authRequest = new MyAuthenticationToken(username, new MyCredentials(password, secondSecret));
return this.getAuthenticationManager().authenticate(authRequest);
}
}
My config-class:
#Configuration
#EnableWebSecurity
#EnableWebMvc
#ComponentScan
public class AppConfig extends WebSecurityConfigurerAdapter {
#Autowired
MyAuthenticationProvider myAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(myAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new MyAuthenticationFilter(), BasicAuthenticationFilter.class)
.authorizeRequests().antMatchers("/**")
.hasAnyRole()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
#Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
MyAuthenticationProvider:
#Component
public class MyAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MyAuthenticationToken myAuthenticationToken = (MyAuthenticationToken) authentication;
MyCredentials credentials = (MyCredentials) myAuthenticationToken.getCredentials();
if (credentials.getPassword().equals("sesamOeffneDich") && credentials.getSecondSecret().equals(MyAuthenticationToken.SECOND_SECRET)) {
myAuthenticationToken.setAuthenticated(true);
return myAuthenticationToken;
} else {
throw new BadCredentialsException("Bad credentials supplied!");
}
}
#Override
public boolean supports(Class<?> authentication) {
return MyAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Why you're seeing a NullPointerException
You're seeing a NullPointerException because you do not have an AuthenticationManager wired into your filter. According to the javadocs for AbstractAuthenticationProcessingFilter
The filter requires that you set the authenticationManager property. An AuthenticationManager is required to process the authentication request tokens created by implementing classes
This being the case, I do scratch my head as to why the authenticationManager did not make the cut for a constructor argument for this abstract filter. I would recommend enforcing this in your constructor for your custom filter.
public MyAuthenticationFilter(AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher("/login", "POST"));
this.setAuthenticationManager(authenticationManager);
}
AuthenticationManager or the AuthenticationProvider
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(myAuthenticationProvider);
}
The AuthenticationManagerBuilder will create a ProviderManager (an AuthenticationManager).
The ProviderManager is a collection of AuthenticationProvider and will attempt to authenticate() with every AuthenticationProvider that it manages. (This is where public boolean supports(Class<?> authentication) is extremely important in theAuthenticationProvider contract)
In your configuration, you have created an ProviderManager containing just your custom Authentication Provider
Cool. Now where is my AuthenticationManager?
It is possible to grab the AuthenticationManager built by the configure() method by this.authenticationManager() in the WebSecurityConfigurerAdapter
#Bean
public AuthenticationManager authenticationManager throws Exception() {
this.authenticationManager();
}
Recommendations
Creating your own ProviderManager does have it's benefits as it would be explicit and within your control.
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(myAuthenticationProvider));
}
This will let you be flexible with the placement of your AuthenticationManager bean and avoid:
Potential cyclic dependency issues in your bean configurations
Bubbling up the checked Exception as a result of calling this.authenticationManager()
Putting it all Together
...
public class AppConfig extends WebSecurityConfigurerAdapter {
#Autowired
MyAuthenticationProvider myAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new MyAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class)
...
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(myAuthenticationProvider));
}
}
You should set the authentication manager to your filter.
You can do that adding this method to your configuration class
#Bean
public MyAuthenticationFilter myAuthenticationFilter() {
MyAuthenticationFilter res = new MyAuthenticationFilter();
try {
res.setAuthenticationManager(authenticationManagerBean());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return res;
}
and modifying the configure method with
http.addFilterBefore(myAuthenticationFilter(), BasicAuthenticationFilter.class)....
in my case nothing helped except this:
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(tokenAuthenticationProvider,
apiKeyAuthenticationProvider));
}
I'm using Spring Security 4.0.3 with a custom login form and Spring MVC 4.1.1, running in Glassfish 4.1.
My custom login page is presented correctly when I ask for a secured URL (/app/**). However, when the login form is submitted (POST /j_security_check), this request results in a basic authentication dialog being displayed. It seems that something feels that /j_security_check is a resource which is protected by basic authentication at the Glassfish level, since I get a 401 Unauthorized page from Glassfish when I hit Cancel on the dialog.
Here is my MVC initializer:
public class SpringMvcInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { MvcConfiguration.class, SecurityConfiguration.class, PersistenceConfiguration.class };
}
#Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
#Override
protected String[] getServletMappings() {
return new String[] { "*.html", "/j_security_check" };
}
}
My MVC Config:
#Configuration
#EnableWebMvc
#ComponentScan(basePackages = {"com.elemenopy.wishlist"})
public class MvcConfiguration extends WebMvcConfigurerAdapter {
#Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/*
* Configure ResourceHandlers to serve static resources like CSS/ Javascript etc...
*/
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/");
}
#Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasenames("messages", "validationMessages");
return source;
}
#Bean
#Override
public Validator getValidator() {
LocalValidatorFactoryBean factory = new LocalValidatorFactoryBean();
factory.setValidationMessageSource(messageSource());
return factory;
}
#Bean
public MessageSourceAccessor messageSourceAccessor() {
return new MessageSourceAccessor(messageSource());
}
}
My security config:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("bill#example.com").password("abc123").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/static/**", "/*.jsp").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/home.html").permitAll()
.antMatchers("/start.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html").permitAll()
.and()
.logout().logoutUrl("/logout.html").permitAll();
}
#Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authManager() throws Exception {
return authenticationManagerBean();
}
}
Any help would be greatly appreciated. Thanks!
Don't know why I didn't find this while I was searching before, but it turns out with Spring Security 4, your login form post URI has to be consistent with the URI of the login page itself. Not a hardcoded URI like /j_security_check.
spring security 4 custom login page
I am currently working on a project in which I use Angular JS and Spring REST services. These last few days I've been trying to get some security into the system (see my previous post). I'm implementing token based security.
I got the basic stuff working, but the XAuthTokenFilter doesn't get called when requests are being done. I have no idea why, I think it's something very simple that I'm overlooking. The relevant classes:
XAuthTokenFilter (doFilter does not get called each request)
public class XAuthTokenFilter extends GenericFilterBean {
private final static String XAUTH_TOKEN_HEADER_NAME = "x-auth-token";
private UserDetailsService detailsService;
private TokenProvider tokenProvider;
public XAuthTokenFilter(UserDetailsService detailsService, TokenProvider tokenProvider) {
this.detailsService = detailsService;
this.tokenProvider = tokenProvider;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String authToken = httpServletRequest.getHeader(XAUTH_TOKEN_HEADER_NAME);
if (StringUtils.hasText(authToken)) {
String username = this.tokenProvider.getUserNameFromToken(authToken);
UserDetails details = this.detailsService.loadUserByUsername(username);
if (this.tokenProvider.validateToken(authToken, details)) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(details, details.getPassword(), details.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(token);
}
}
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
XAuthTokenConfigurer
public class XAuthTokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private TokenProvider tokenProvider;
private UserDetailsService detailsService;
public XAuthTokenConfigurer(UserDetailsService detailsService, TokenProvider tokenProvider) {
this.detailsService = detailsService;
this.tokenProvider = tokenProvider;
}
#Override
public void configure(HttpSecurity http) throws Exception {
XAuthTokenFilter customFilter = new XAuthTokenFilter(detailsService, tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
SecurityConfiguration
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
#Inject
private UserDetailsService userDetailsService;
#Inject
private TokenProvider tokenProvider;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/scripts/**/*.{js,html}");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/protected/**").authenticated()
.antMatchers("/api/open/**").permitAll()
.and()
.apply(securityConfigurerAdapter());
}
private XAuthTokenConfigurer securityConfigurerAdapter() {
return new XAuthTokenConfigurer(userDetailsService, tokenProvider);
}
/**
* This allows SpEL support in Spring Data JPA #Query definitions.
*
* See https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions
*/
#Bean
EvaluationContextExtension securityExtension() {
return new EvaluationContextExtensionSupport() {
#Override
public String getExtensionId() {
return "security";
}
#Override
public SecurityExpressionRoot getRootObject() {
return new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};
}
};
}
}
I really have no clue why it doesn't get called, is there something wrong with my url antMatcher() statements?
My Context which might be good to include:
#Configuration
#EnableWebMvc
#Import(AppContext.class) // The context from my backend which is included as a dependency
#ComponentScan("com.example.springsecuritytest")
public class RestContext extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
}
WebAppInitializer
public class WebAppInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
//RootContext
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(RestContext.class);
// Add RootContext using ContextLoaderListener
servletContext.addListener(new ContextLoaderListener(rootContext));
// Registering and mapping dispatcher servlet
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
Please check out this git repo. I have prepared a very basic setup that test security with and without a tokenAuthenticationFilter. The filter implementation is just a mock, setting a valid Authentication whenever the header is present, regardless of its value. Please also note the two application WebApplicationInitializers, which are Servlet 3.0 conformant and are configuring the Servlet container programmatically (instead of web.xml).