Spring Authentication allows us to identify who is trying to access the resource while Spring Authorization allows us to determine who is allowed to access the resource.

Configuration
Java Compilation:
Java Runtime:
Web Server:
Maven:
Java Compilation:
Java Runtime:
Web Server:
Gradle:
JDK 17.0.2
JRE OpenJDK 17.0.2
Tomcat 10.1.16
Maven 3.9.6
JDK 17.0.2
JRE OpenJDK 17.0.2
Tomcat 10.1.16
Gradle 8.6
Create the following folder structure:
folder structure of Spring Web Security
pom.xml: (if using Gradle, replace pom.xml with build.gradle)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.maxjava</groupId> <artifactId>SpringWebSecurity</artifactId> <version>1.0</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.1.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build> </project>
plugins { id 'java' id 'war' } java { sourceCompatibility = 17 targetCompatibility = 17 } group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation group: 'org.springframework', name: 'spring-webmvc', version: '6.1.6' implementation group: 'org.springframework.security', name: 'spring-security-web', version: '6.1.6' implementation group: 'org.springframework.security', name: 'spring-security-config', version: '6.1.6' compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '6.1.0' }
Some notes about pom.xml:
  • spring-security-web and spring-security-config are needed for Spring web security
  • jakarta.servlet-api for servlet related classes
  • maven-compiler-plugin is for compiling the code using source version 17 and target version 17
  • A war artifact will be created for deployment to servlet containers
Some notes about build.gradle:
  • spring-security-web and spring-security-config are needed for Spring web security
  • jakarta.servlet-api for servlet related classes
  • maven-compiler-plugin is for compiling the code using source version 17 and target version 17
  • A war artifact will be created for deployment to servlet containers
web.xml:
<web-app> <display-name>Spring Web Security</display-name> <servlet> <servlet-name>SpringWebSecurity</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/myApplicationContext.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet-mapping> <servlet-name>SpringWebSecurity</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
Some notes about web.xml:
  • org.springframework.web.servlet.DispatcherServlet is the servlet class for Spring
  • /WEB-INF/myApplicationContext.xml is the location of the Spring context configuration file
  • define org.springframework.web.filter.DelegatingFilterProxy for Spring filters to be recognized by the servlet container
myApplicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="controller,configuration"/> </beans>
Some notes about myApplicationContext.xml:
  • <context:component-scan base-package="controller,configuration"/> is for Spring to look for classes in controller,configuration packages to create beans
WebSecurityConfiguration.java:
package configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import security.AuthenticationFilter; @Configuration @EnableWebSecurity public class WebSecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.sessionManagement(customizer -> customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http.exceptionHandling( customizer -> customizer .authenticationEntryPoint((request, response, e) -> { response.setStatus(401); response.getWriter().write("You are not authenticated.");}) .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(403); response.getWriter().write("You are not authorized to access this resource.");}) ); http.authorizeHttpRequests(customizer -> customizer .requestMatchers(AntPathRequestMatcher.antMatcher("/adminApp/**")).hasRole("ADMIN") .requestMatchers(AntPathRequestMatcher.antMatcher("/publicApp/**")).permitAll() .anyRequest().authenticated() ); http.addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
Some notes about WebSecurityConfiguration.java:
  • @Configuration to define Spring configuration where @Bean on the filterChain method to create the SecurityFilterChain bean
  • @EnableWebSecurity enables HttpSecurity to be used in the filterChain method
  • http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) to make the security context stateless so that every request is independent
  • http.addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) to add our customized filter - AuthenticatorFilter (see below for explanation) to the servlet filter chain right after UsernamePasswordAuthenticationFilter which is the beginning of authentication
  • http.exceptionHandling().authenticationEntryPoint() to define response for non-authenticated, meaning no AuthenticationToken is set during AuthenticationFilter
  • http.exceptionHandling().accessDeniedHandler() to define response for non-authorized, meaning failing to meeting the authorization rules defined in http.authorizeHttpRequests
  • http.authorizeRequests().antMatchers("/adminApp/**").hasRole("ADMIN") to allow only the security context has the role "ROLE_ADMIN" (see below AuthenticationFilter for how the "ROLE_ADMIN" is assigned) to access the resources which is on the path "/adminApp/**"
  • http.authorizeRequests().antMatchers("/publicApp/**").permitAll() to allow access the resources which is on the path "/publicApp/**" with no need for authentication or authorization
  • http.authorizeRequests().anyRequest().authenticated() to allow all other resources ("/userApp/**") besides "/adminApp/**" and "/publicApp/**" requires only authentication
AuthenticationFilter.java:
package security; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Base64; import java.util.List; public class AuthenticationFilter extends OncePerRequestFilter { private String[][] USER_PASSWORD_ROLE = { {"Sam","sampass","ROLE_ADMIN"}, {"Mary","marypass","ROLE_USER"}, {"John","johnpass","ROLE_USER"}}; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { AuthenticationToken authenticationToken = null; String role = verifyUser(httpServletRequest); if (role != null) { List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); grantedAuthorities.add(new SimpleGrantedAuthority(role)); authenticationToken = new AuthenticationToken(grantedAuthorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } filterChain.doFilter(httpServletRequest, httpServletResponse); } private String verifyUser(HttpServletRequest httpServletRequest) { String role = null; String authorizationHeader = httpServletRequest.getHeader("Authorization"); if (authorizationHeader != null) { String encodedUsernameAndPasswordIncoming = authorizationHeader.substring("Basic".length()).trim(); for (String[] userPasswordRole: USER_PASSWORD_ROLE) { String encodedUsernameAndPasswordCalculated = Base64.getEncoder().encodeToString((userPasswordRole[0] + ":" + userPasswordRole[1]).getBytes()); if (encodedUsernameAndPasswordCalculated.equals(encodedUsernameAndPasswordIncoming)) { role = userPasswordRole[2]; break; } } } return role; } }
Some notes about AuthenticationFilter.java:
  • This is the logic to decide what role this request session should have
  • Overriding the doFilterInternal method, from HttpServletRequest we verify the username and password using Base64 to see if any of the USER_PASSWORD_ROLE element is matching. If so, create a new AuthenticationToken with the role and assign the token to SecurityContextHolder
  • When setting the AuthenticationToken, a prefix "ROLE_" needs to be added to the actual role
  • The example above is using Basic Authentication, the actual authentication logic could be changed
AuthenticationToken.java:
package security; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import javax.security.auth.Subject; import java.util.Collection; public class AuthenticationToken extends AbstractAuthenticationToken { public AuthenticationToken(Collection authorities) { super(authorities); super.setAuthenticated(true); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return null; } @Override public boolean implies(Subject subject) { return false; } }
Some notes about AuthenticationToken.java:
  • The SecurityContextHolder is taking AbstractAuthenticationToken which is an abstract class so we need to define a regular class which extends AbstractAuthenticationToken
  • Create a constructor and add super.setAuthenticated(true) to set this token as authenticated
  • Other methods no need to be changed
Now the configurations are done, let's create some controllers to test it:
MyAdminController.java:
package controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Calendar; @RestController @RequestMapping("/adminApp") public class MyAdminController { @GetMapping("/currentTime") public String getCurrentTime() { return "ADMIN " + Calendar.getInstance().getTime().toString(); } }
MyUserController.java:
package controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Calendar; @RestController @RequestMapping("/userApp") public class MyUserController { @GetMapping("/currentTime") public String getCurrentTime() { return "USER " + Calendar.getInstance().getTime().toString(); } }
MyPublicController.java:
package controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Calendar; @RestController @RequestMapping("/publicApp") public class MyPublicController { @GetMapping("/currentTime") public String getCurrentTime() { return "PUBLIC " + Calendar.getInstance().getTime().toString(); } }
Sample requests and responses:(assume the war file is deployed to <CONTEXT_PATH>)
  • With Sam and sampass (U2FtOnNhbXBhc3M= is the base64 encoded Sam:sampass) which has the ROLE_ADMIN to call /adminApp/currentTime:
    curl --location --request GET '<CONTEXT_PATH>/adminApp/currentTime' --header 'Authorization: Basic U2FtOnNhbXBhc3M='
    Response:
    ADMIN Fri Jun 21 12:41:10 PDT 2024
  • With Mary and marypass (TWFyeTptYXJ5cGFzcw== is the base64 encoded Mary:marypass) which has the ROLE_USER to call /adminApp/currentTime:
    curl --location --request GET '<CONTEXT_PATH>/adminApp/currentTime' --header 'Authorization: Basic TWFyeTptYXJ5cGFzcw=='
    Response:
    You are not authorized to access this resource.
  • With no authentication to call /adminApp/currentTime:
    curl --location --request GET '<CONTEXT_PATH>/adminApp/currentTime'
    Response:
    You are not authenticated.
  • With Mary and marypass to call /userApp/currentTime:
    curl --location --request GET '<CONTEXT_PATH>/userApp/currentTime' --header 'Authorization: Basic TWFyeTptYXJ5cGFzcw=='
    Response:
    USER Fri Jun 21 12:38:34 PDT 2024
  • With no authentication to call /userApp/currentTime:
    curl --location --request GET '<CONTEXT_PATH>/userApp/currentTime'
    Response:
    You are not authenticated.
  • With no authentication to call /publicApp/currentTime:
    curl --location --request GET '<CONTEXT_PATH>/publicApp/currentTime'
    Response:
    PUBLIC Fri Jun 21 12:40:34 PDT 2024