Let us now implement another security model that uses a custom authentication process instead of the default:
- First, create a new security context definition, AppSecurityModelB, which presents the overriding of the http.formLogin().loginProcessingUrl() and the customization of the authentication manager and its providers:
@Configuration @EnableWebSecurity public class AppSecurityModelB extends WebSecurityConfigurerAdapter{ @Autowired private AuthenticationProvider appAdminProvider; @Autowired private AuthenticationProvider appHRProvider; @Autowired private AuthenticationManager appAuthenticationMgr; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login_form**","/after**") .permitAll() .anyRequest().fullyAuthenticated() .and() .formLogin() .loginPage("/login_form.html") .loginProcessingUrl("/login_process") .defaultSuccessUrl("/deptform.html") .failureUrl("/login_form.html?error=true") .and() .logout().logoutUrl("/logout.html") .logoutSuccessUrl("/after_logout_url.html"); http.csrf().disable(); } @Override public void configure(WebSecurity web) throws Exception { // refer to sources } @Override protected AuthenticationManager authenticationManager() throws Exception { return new ProviderManager(Arrays.asList( appAdminProvider, appHRProvider ), appAuthenticationMgr); } }
In the authenticationManager() method, observe the order of the injected custom providers inside the org.springframework.security.authentication.ProviderManager's constructor. The order of execution starts from the leftmost provider up to the parent authentication manager appAuthenticationMgr. Notice also that all the user credentials and roles are not configured using AuthenticationManagerBuilder anymore.
Be sure to save this class inside org.packt.secured.mvc.core.
- Implement the preceding two providers indicated, namely AppAdminProvider and AppHRProvider, and save them in org.packt.secured.mvc.core.manager. The first provider, AppAdminProvider, just checks if the user is admin with a password of "admin". If the credential is correct, it sets its ROLE_ADMIN and passes the authentication chain its Authentication details; otherwise, it throws BadCredentialsException:
@Component public class AppAdminProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String name = authentication.getName(); String password = authentication.getCredentials().toString(); if (name.equalsIgnoreCase("admin") && password.equalsIgnoreCase("admin")) { Set<SimpleGrantedAuthority> authorities = new HashSet<>(); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); return new UsernamePasswordAuthenticationToken(name, password, authorities); } else { throw new BadCredentialsException("Invalid Admin User"); } } @Override public boolean supports(Class<?> authentication) { return authentication.equals( UsernamePasswordAuthenticationToken.class); } }
- The second, AppHRProvider, checks if the user is using the hradmin account, builds its user roles, and returns its Authentication details. The same BadCredentialsException is thrown if it is not the valid user:
@Component public class AppHRProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // refer to sources if (name.equalsIgnoreCase("hradmin") && password.equalsIgnoreCase("hradmin")) { Set<SimpleGrantedAuthority> authorities = new HashSet<>(); authorities.add( new SimpleGrantedAuthority("ROLE_HR")); return new UsernamePasswordAuthenticationToken(name, password,authorities); } else { throw new BadCredentialsException("Invalid HR User"); } } @Override public boolean supports(Class<?> authentication) { // refer to sources } }
If a diamond operator (<>) that is used to simplify the instantiation of generic classes does not work, configure Maven Compiler Plugin in pom.xml to contain:
<configuration> <source>1.8</source> <target>1.8</target> </configuration>
Or, add the following POM properties:
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
- The security model will not be complete without the custom implementation of the parent provider, the AppAuthenticationManager. This authentication manager builds the whole user and their corresponding user account just as AuthenticationManagerBuilder did previously, with the in-memory user credentials:
@Component public class AppAuthenticationMgr implements AuthenticationManager { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { System.out.println(AppAuthenticationMgr.class); String name = authentication.getName(); String password = authentication.getCredentials().toString(); if (name.equalsIgnoreCase("sjctrags") && password.equalsIgnoreCase("sjctrags")) { Set<SimpleGrantedAuthority> authorities = new HashSet<>(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); return new UsernamePasswordAuthenticationToken(name, password, authorities); } else if (name.equalsIgnoreCase("admin") && password.equalsIgnoreCase("admin")) { Set<SimpleGrantedAuthority> authorities = new HashSet<>(); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); return new UsernamePasswordAuthenticationToken(name, password, authorities); } else if (name.equalsIgnoreCase("hradmin") && password.equalsIgnoreCase("hradmin")) { Set<SimpleGrantedAuthority> authorities = new HashSet<>(); authorities.add(new SimpleGrantedAuthority("ROLE_HR")); return new UsernamePasswordAuthenticationToken(name, password, authorities); } else if(name.equalsIgnoreCase("guest")){ Set<SimpleGrantedAuthority> authorities = new HashSet<>(); authorities.add(new SimpleGrantedAuthority("ROLE_ANONYMOUS")); return new AnonymousAuthenticationToken(name, "ANONYMOUS", authorities); } else{ throw new BadCredentialsException("Not Valid User"); } } }
The custom AuthenticationManager includes guest as its anonymous user to be used, in the later recipes.
- Update the SpringConfigContext to accommodate this security model. Also add the package of the provider in its @ComponentScan annotation:
@Import(value = { AppSecurityModelB.class }) @Configuration @EnableWebMvc @ComponentScan(basePackages = {"org.packt.secured.mvc", "org.packt.secured.mvc.core.manager" } public class SpringContextConfig { }
Comment on all the class-level annotations of the previous security model classes to avoid conflict or use @Order to establish a series of security model hierarchies.
- Create a separate login view (/login_form.html) that will highlight the overridden /login URL for login processing:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Just Another Login Form</title> </head> <body> <c:if test="${ not empty errorMsg }"> <em><c:out value='${ errorMsg }'/></em><br/> </c:if> <form action="/ch04/login_process" method='POST'> <table> <tr> <td>User:</td> <td><input type='text' name='username' value=''> </td> </tr> <tr> <td>Password:</td> <td><input type='password' name='password' /> </td> </tr> <tr> <td colspan='2'><input name="submit" type="submit" value="submit" /> </td> </tr> <tr> <td colspan='2'><input name="reset" type="reset" /> </td> </tr> </table> </form> </body> </html>
- Save this file inside src/main/webapp/login_process.
- Update the LoginController by implementing a GET request method for the /login_form.html view page:
@RequestMapping(value = {"/login_form.html"}, method = RequestMethod.GET) public String login_form(@RequestParam(name="error", required=false) String error, Model model) { try { if (error.equalsIgnoreCase("true")) { String errorMsg = "Login Error"; model.addAttribute("errorMsg", errorMsg); }else{ model.addAttribute("errorMsg", error); } } catch (NullPointerException e) { return "login_form_url"; } return "login_form_url"; }
- Create a separate after-logout view page (srcmainwebapplogin_processafter_logout_form.html) to redirect the user to /login_form.html. Also, implement a typical @Controller of this new view page. Make the previous controllers as reference.
- Update views.properties and messages_en_US.properties for the new view.
- Save all files. clean, build, and deploy. Run https://localhost:8443/ch04/login_form.html to test the custom AuthenticationProvider and the overridden login-processing-url. Try logging in using all the user credentials indicated in the AuthenticationManagerBuilder and observe what happens.