Adding a customized pre-authentication in spring boot takes the following 4 steps:
AbstractAuthenticationToken
AbstractAuthenticationProcessingFilter
AuthenticationProvider
WebSecurityConfigurerAdapter
Assume our site is http://127.0.0.1:8080. The login path is http://127.0.0.1:8080/login. To make is simple, we take a token from login path like http://127.0.0.1:8080/login?token=THE_TOKEN. If THE_TOKEN is worktimefun, we allow the user login with ROLE_USER, otherwise we redirect the user to a log in failed page.
Our token will store user credential and principal, and a collection of its GrantedAuthority
after it is being authenticated.
public class MyToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private String token; private String username; public MyToken(String sToken) { super(null); this.token = sToken; setAuthenticated(false); } public MyToken(String sToken, String sUsername, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.token = sToken; this.username = sUsername; super.setAuthenticated(true); } public String getToken() { return token != null ? token : ""; } public void setToken(String token) { this.token = token; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public Object getCredentials() { return getToken(); } @Override public Object getPrincipal() { return getUsername(); } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } }
This filter takes two parameters to be constructed. The first one is the login path pattern, and the second one is the authentication manager. We will provide both in step 4.
public class MyFilter extends AbstractAuthenticationProcessingFilter { public MyFilter(String pattern, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(pattern, "GET")); super.setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String sToken = request.getParameter("token"); if (sToken == null) { sToken = ""; } MyToken token = new MyToken(sToken); return this.getAuthenticationManager().authenticate(token); } }
Here we do three things bad to make this example simpler. The first one is the set a hard-coded ROLE_USER on fly. The second one is set a hard-coded username on fly. The third one is to just compare a token with a hard-coded string. All of them are bad.
public class MyProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!this.supports(authentication.getClass())) { return null; } else { MyToken myAuthenticationToken = (MyToken) authentication; //get the token String sToken = myAuthenticationToken.getToken(); boolean isLogin = false; if (sToken.equals("worktimefun")) { isLogin = true; } if (isLogin) { GrantedAuthority grantedAuthority = new GrantedAuthority() { @Override public String getAuthority() { return "ROLE_USER"; } }; Set<GrantedAuthority> grantedAuthoritySet = new HashSet<>(); grantedAuthoritySet.add(grantedAuthority); MyToken authenticatedToken = new MyToken(sToken, "WorkTimeFunUsername", grantedAuthoritySet); return authenticatedToken; } else { throw new BadCredentialsException("Bad credentials"); } } } @Override public boolean supports(Class<?> authentication) { return MyToken.class.isAssignableFrom(authentication); } }
In this configuration, we provide a ProviderManager
that has our own authentication provider MyProvider
. We also need to add our own authentication filter MyFilter
so that when user access the path /login, our filter's attemptAuthentication will be called by the doFilter called by the filter chain. Finally we need to set the authorize requests to tell which URL patterns need to be authenticated, and authorized to whom.
@Configuration public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { private AuthenticationManager authenticationManager; @Override protected AuthenticationManager authenticationManager() throws Exception { authenticationManager = new ProviderManager(Collections.singletonList(new MyProvider())); return authenticationManager; } @Override protected void configure(HttpSecurity http) throws Exception { final String loginPath = "/login"; final String loginFailedPath = "/loginFailed"; final String userHome = "/user"; LogoutSuccessHandler logoutSuccessHandler = (request, response, authentication) -> { response.setHeader("Location", "/"); response.setStatus(302); }; AuthenticationFailureHandler authenticationFailureHandler = (request, response, exception) -> { response.setHeader("Location", loginFailedPath); response.setStatus(302); }; AuthenticationSuccessHandler successHandler = (request, response, authentication) -> { response.setHeader("Location", userHome); response.setStatus(302); }; MyFilter myFilter = new MyFilter(loginPath, authenticationManager); myFilter.setAuthenticationFailureHandler(authenticationFailureHandler); myFilter.setAuthenticationSuccessHandler(successHandler); http.addFilterBefore(myFilter, LogoutFilter.class); http.authorizeRequests() .antMatchers("/").permitAll() .antMatchers(loginPath).permitAll() .antMatchers(loginFailedPath).permitAll() .antMatchers(userHome + "/**").access("hasRole('ROLE_USER')") .antMatchers("/admin").access("hasRole('ROLE_ADMIN')") .anyRequest().authenticated() .and() .logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler).permitAll(); } }
Note that we have redirect the user to /user or /loginFailed after they attempted to log in. You need to write your own controller to handle these. It could be easily done by this:
@Controller public class WelcomeController { @ResponseBody @RequestMapping("/") public String welcome(Map<String, Object> model) { return "welcome"; } @ResponseBody @RequestMapping(value = "/loginFailed", method = RequestMethod.GET) public String loginFailed() { return "Login failed!!!"; } @ResponseBody @RequestMapping(value = "/user", method = RequestMethod.GET) public String userHome(HttpServletRequest request, Model model) { return "Login succeed."; } }
After logged in, the authentication token can be obtains by calling:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Depending on your authenticate chain, or your will get different type of authentication token that implements Authentication
. In our case, it would be MyToken
.