Let us now add important supporting components to the authentication process established by the previous recipe:
- Create and apply two filters needed to establish a security filter chain in this new security model. Implement our first filter interface, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter, which will intercept the authentication and authorization process every time /login is called. Save this AppAuthenticationFilter class in org.packt.secured.mvc.handler package:
public class AppAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); List<String> roles = new ArrayList<String>(); for (GrantedAuthority a : authorities) { roles.add(a.getAuthority()); } String name = obtainPassword(request); String password = obtainUsername(request); UsernamePasswordAuthenticationToken userDetails = new UsernamePasswordAuthenticationToken(name, password, authorities); setDetails(request, userDetails); chain.doFilter(request, response); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.sendRedirect("/ch04/login.html?error=true"); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String name = obtainPassword(request); String password = obtainUsername(request); SecurityContext context = SecurityContextHolder.getContext(); Authentication auth = null; if(context.getAuthentication() == null){ auth = new UsernamePasswordAuthenticationToken( name, password); setDetails(request, (UsernamePasswordAuthenticationToken) auth); }else{ auth = (AnonymousAuthenticationToken) context.getAuthentication(); return auth; } return auth; } }
This class has three overridden methods responsible for filtering incoming authentication requests, namely attemptAuthentication(), which is executed once an anonymous user attempts to access the /login, successfulAuthentication(), which runs after an authentication has been created either from an authenticated or valid anonymous user, and lastly, unsuccessfulAuthentication(), which is responsible for global error page redirection, equivalent to executing http.formLogin.failureUrl(). Authentication is classified into two types, org.springframework.security.authentication.UsernamePasswordAuthenticationToken and org.springframework.security.authentication.AnonymousAuthenticationToken.
- The next custom filter implementation in our filter stack is the org.springframework.security.web.authentication.AnonymousAuthenticationFilter, which is responsible for managing anonymous user access. Save this class AppAnonAuthFilter together with the previous filter:
public class AppAnonAuthFilter extends AnonymousAuthenticationFilter { private String principal; private String key; private List<GrantedAuthority> authorities; public AppAnonAuthFilter(String key) { super(key); this.key = key; } public AppAnonAuthFilter(String key, Object principal, List<GrantedAuthority> authorities) { super(key, principal, authorities); this.key = key; this.principal = principal.toString(); this.authorities = authorities; } @Override protected Authentication createAuthentication(HttpServletRequest request) { if(principal.equalsIgnoreCase( request.getParameter("username")) ){ AnonymousAuthenticationToken authTok = new AnonymousAuthenticationToken(key, principal, authorities); SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(authTok); return authTok; } return null; } }
This class creates an Authentication object once an anonymous account guest has been detected; otherwise, it just throws null to the Spring Security container. This filter must be programmed not to create conflict with the processes of the UsernamePasswordAuthenticationFilter class.
- As helper objects, handlers are triggered by security models every time an Authentication object is thrown. A custom success authentication handler assists filter chains in defining the different default success URLs after a successful user authentication process. This class also overrides the formLogin.defaultSuccessUrl() and gives the application several options of default view pages depending on the roles of the users:
@Component public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String targetUrl = targetUrl(authentication); if (response.isCommitted()) { System.out.println("Can't redirect"); return; } redirectStrategy.sendRedirect(request, response, targetUrl); } protected String targetUrl(Authentication authentication) { String url = ""; Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); List<String> roles = new ArrayList<String>(); for (GrantedAuthority a : authorities) { roles.add(a.getAuthority()); } if (isUserRole(roles)) { // add user-related transactions here url = "/deptform.html"; } else if (isAdminRole(roles)){ // add admin-related transactions here url = "/deptform.html"; } else if (isHrAdminRole(roles)){ // add admin-related transactions here url = "/deptform.html"; } else{ url = "/deptform.html"; } return url; } // refer to sources }
- Save this file in org.secured.mvc.core.handler.
- Another handler called the logout handler must be custom implemented to provide routes once /logout is triggered, depending on the roles of the users. This class overrides formLogin.logoutSuccessUrl():
@Component public class CustomLogoutHandler extends SimpleUrlLogoutSuccessHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String targetUrl = targetUrl(authentication); if (response.isCommitted()) { System.out.println("Can't redirect"); return; } redirectStrategy.sendRedirect(request, response, targetUrl); } protected String targetUrl(Authentication authentication) { String url = ""; Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); List<String> roles = new ArrayList<String>(); for (GrantedAuthority a : authorities) { roles.add(a.getAuthority()); } if (isUser(roles)) { url = "/after_logout.html?message=" + "Thank your, User!"; } else if (isAdmin(roles)){ url = "/after_logout.html?message=" + "Thank you, Admin!"; } else if (isHrAdmin(roles)){ url = "/after_logout.html?message=" + "Thank you, HR!"; } return url; } // refer to sources }
- The last handler essential to this recipe is the handler that will be executed when the user /login fails. Though this class has a limited scope of work, this can be useful in scrutinizing error messages depending on the nature of the validation error or the type of AuthenticationException:
@Component public class CustomFailureHandler extends SimpleUrlAuthenticationFailureHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String targetUrl = ""; if(exception instanceof BadCredentialsException){ targetUrl = "/login.html?error=" + exception.getMessage(); } else { targetUrl = "/login.html?error=true"; } // refer to sources redirectStrategy.sendRedirect(request, response, targetUrl); } }
- Together with the previous custom authentication manager and providers, construct the proper model that will highlight the whole custom security architecture:
@Configuration @EnableWebSecurity public class AppSecurityModelC extends WebSecurityConfigurerAdapter { // refer to sources @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { } @Override protected void configure(HttpSecurity http) throws Exception { http .anonymous().authorities("ROLE_ANONYMOUS") .and() .authorizeRequests() .antMatchers("/login**", "/after**").permitAll() .antMatchers("/deptanon.html").anonymous() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .defaultSuccessUrl("/deptform.html") .failureHandler(customFailureHandler) .successHandler(customSuccessHandler) .and() .addFilterBefore(appAnonAuthFilter(), UsernamePasswordAuthenticationFilter.class) .addFilter(appAuthenticationFilter( authenticationManager())) .logout().logoutUrl("/logout.html") .logoutSuccessHandler(customLogoutHandler) .and() .exceptionHandling().authenticationEntryPoint( setAuthPoint()); http.csrf().disable(); } }
- Implement the authentication filter that will assess all incoming valid users. Include this inside the AppSecurityModelC context definition:
@Bean public UsernamePasswordAuthenticationFilter appAuthenticationFilter(AuthenticationManager authMgr) { AppAuthenticationFilter filter = new AppAuthenticationFilter(); filter.setRequiresAuthenticationRequestMatcher( new AntPathRequestMatcher("/login.html", "POST") ); filter.setAuthenticationManager(authMgr); return filter; }
- Create another filter that will assess users that are not considered valid users and will allow guest or anonymous access to the application:
@Bean public AnonymousAuthenticationFilter appAnonAuthFilter(){ List<GrantedAuthority> anonAuth = new ArrayList<>(); anonAuth.add(new SimpleGrantedAuthority("ROLE_ANONYMOUS")); AppAnonAuthFilter anonFilter = new AppAnonAuthFilter("ANONYMOUS","guest",anonAuth); return anonFilter;
}
- To register into Spring Security container the preceding filters, create an authentication manager by overriding the authenticationManager() of the WebSecurityConfigurerAdapter:
@Override protected AuthenticationManager authenticationManager() throws Exception { // refer to sources }
- Since we have bypassed the default filter configuration of the Spring Security framework, it is mandatory to tell the security platform when to trigger either of the filters implemented by injecting a new AuthenticationTrustResolver:
@Bean public AuthenticationTrustResolver trustResolver() { return new AuthenticationTrustResolver() { @Override public boolean isRememberMe(final Authentication authentication) { return true; } @Override public boolean isAnonymous(final Authentication authentication) { Collection<? extends GrantedAuthority> auths = authentication.getAuthorities(); List<String> roles = new ArrayList<String>(); for (GrantedAuthority a : auths) { roles.add(a.getAuthority()); } if(roles.contains("ROLE_ANONYMOUS") || roles.size() == 0){ return true; }
else{ return false; } } }; }
- Together with AuthenticationTrustResolver, implementation is a new AuthenticationEntryPoint to tell the platforms what URL to trigger with the custom filters created in preceding steps.
@Bean public AuthenticationEntryPoint setAuthPoint(){ return new AppAuthPoint("/login.html"); }
- Create an additional @Controller for the request transactions of our anonymous account guest:
@Controller public class AnonymousController { @RequestMapping(value="/deptanon.html") public String anonPage(){ return "dept_anon"; } }
- Create the additional view page /deptanon.html for the default view page of our anonymous account inside the src/main/webapp/anonymous_sites directory:
<html><head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Anonymous</title> </head> <body> <h1>Anonymous Account</h1> <p>This content is for our beloved guest wants to check our DEPARTMENT database. Enjoy! <em><a href="<c:url value='/login.html'/>">You want some more about us? Login!.</a></em> </body> </html>
- Update the views.properties for the added view details.
- Be sure to update the SpringContextConfig by importing the new AppSecurityModelC and including org.packt.secured.mvc.core.handler in its @ComponentScan.
- Save all files. Just like in the recent recipes, always clear the browser sessions and remove the previously deployed ch04 project in the Tomcat 9 server. clean, build, and deploy the Maven project.