Assume you have already read https://wiki.chongtin.com/springboot/spring_simple_username-password_authentication_with_h2. If not, do it because we need to use h2 database to store our users.
Unless you want to put the username, and password in your HTTP header every time, otherwise, Ajax authentication is usually use for the very beginning of the JWT authentication when the front-end use their login:password for exchanging the JWT access token, and refresh token.
In this example, we use Ajax Authentication for exchanging JWT token. The JWT part will be in the https://wiki.chongtin.com/springboot/spring_simple_jwt_authentication.
username
and password
for the username, and password.
Here we create our own AuthenticationFilter that extends UsernamePasswordAuthenticationFilter. By default, UsernamePasswordAuthenticationFilter's attemptAuthentication
method takes a HTTP form input with name username
, and password
for path /login
, and try to use the supplied AuthenticationManager
to authenticate it. So UsernamePasswordAuthenticationFilter has already done the heavy lifting for us, all we need to do is to @Override successfulAuthentication(…){…}
method to do our stuff.
There is another possible way to do Ajax authentication with OncePerRequestFilter
, but we are NOT going to talk about it here.
public class AjaxAuthFilter extends UsernamePasswordAuthenticationFilter { public AjaxAuthFilter(AuthenticationManager authenticationManager) { this.setAuthenticationManager(authenticationManager); } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { super.doFilter(req, res, chain); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (authResult.getPrincipal() instanceof UserDetails) { UserDetails user = (UserDetails) authResult.getPrincipal(); String role = user.getAuthorities().iterator().next().getAuthority(); //create the JWT access token, refresh token //push it to the response as token, token-refresh String token = JWT.create() .withSubject(user.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.ACCESS_TOKEN_EXPIRATION_TIME)) .withClaim(SecurityConstants.ROLE, role) .sign(HMAC512(SecurityConstants.SECRET.getBytes())); String refreshToken = JWT.create() .withSubject(user.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.REFRESH_TOKEN_EXPIRATION_TIME)) .withClaim(SecurityConstants.ROLE, role) .sign(HMAC512(SecurityConstants.SECRET.getBytes())); response.addHeader(SecurityConstants.ACCESS_HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token); response.addHeader(SecurityConstants.REFRESH_HEADER_STRING, SecurityConstants.TOKEN_PREFIX + refreshToken); } else { super.successfulAuthentication(request, response, chain, authResult); } } }
Forget about the JWT stuff for now, the key point here are:
The userDetailsService is used by our AjaxAuthFilter, so we need to register it. We have already talked UserDetailsServiceImpl in the other page (https://wiki.chongtin.com/springboot/spring_simple_username-password_authentication_with_h2), so we skip it here.
For Spring to know, and use our filter, we need to register it too. We supply the this.authenticationManager()
since we need to set it for our filter that extends UsernamePasswordAuthenticationFilter
, and we want our filter to be caller before the spring UsernamePasswordAuthenticationFilter (i.e the spring default login page).
@Configuration public class WebMvcConfig extends WebSecurityConfigurerAdapter { private UserDetailsServiceImpl userDetailsService; private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; public WebMvcConfig(UserDetailsServiceImpl userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) { this.userDetailsService = userDetailsService; this.bCryptPasswordEncoder = bCryptPasswordEncoder; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); auth.authenticationProvider(jwtAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { List<String> jwtSkipPaths = new ArrayList<>(); jwtSkipPaths.add("/login"); jwtSkipPaths.add("/error"); http.cors().and().csrf().disable().authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .addFilterBefore(new AjaxAuthFilter(this.authenticationManager()), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtAuthFilter(new SkipPathRequestMatcher(jwtSkipPaths, "/**"), this.authenticationManager()), UsernamePasswordAuthenticationFilter.class) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }
Run the application, and use curl: (Remember to use HTTPS for production server)
curl --noproxy "*" -v http://127.0.0.1:8080/login -d "username=user&password=password"
Here is the result. We put the access token and refresh token in the response header. The front-end should take it and store in browser cookie, or local storage, and it should use a HttpInterceptor (for Angular for example) to put the token in the HTTP header in each of it message to the back-end server.
* Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > POST /login HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.61.0 > Accept: */* > Content-Length: 31 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 31 out of 31 bytes < HTTP/1.1 200 < Vary: Origin < Vary: Access-Control-Request-Method < Vary: Access-Control-Request-Headers < token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTE4NDgzNDB9.wsM6psLzJZm5VbuiY6ynZMBNonWz99X-K8NEaBKQHYeVQv34Y7KUSLlIqWqzD3ZVbZbMlXCAntIuN7alwnCMSQ < token-refresh: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJST0xFIjoiVVNFUiIsInN1YiI6InVzZXIiLCJleHAiOjE1OTE4NTQ2NDB9.epI8No4In3AkzVqsqoyE_GFcuMSNe9BE-OnKiI6FR82qUV5LEUv5jCmCpxgbqwXEXNF4D1rjsi25PgedO-OVGw < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Frame-Options: DENY < Content-Length: 0 < Date: Thu, ** *** **** **:**:** GMT < * Connection #0 to host 127.0.0.1 left intact