Last Updated on: February 8, 2021 pm
Spring Security - Overview Model
Spring Security defines a framework for Security.
Implmented using Servlet filters in the background.
Two methods of securing a Web app: Declarative and Programmatic.
Big Pic
Flow Chart in Action
Security Concepts
Authentication - Check user id or password with credentials stored in App/db.
Authorization - Check to see if user has an authorized role.
Declarative Security Defines application’s security constraints in configuration.
All Java config
Or Spring XML Config
Programmatic Security
Login Methods
HTTP
Default Login Form
Custom Login Form
Spring MVC - Java Config Add Maven Dependencies Import existing maven project into IntelliJ.
Since we are not using web.xml
, customize Maven build and must add Maven WAR plugin.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <build > <finalName > spring-security-demo</finalName > <pluginManagement > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-war-plugin</artifactId > <version > 3.3.1</version > </plugin > </plugins > </pluginManagement > </build >
Create Spring @Configuration Create a DemoAppConfig.java file in the source directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration @EnableWebMvc @ComponentScan(basePackages = "com.luv2code.springsecurity.demo") public class DemoAppConfig { @Bean public ViewResolver viewResolver () { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/view/" ); viewResolver.setSuffix(".jsp" ); return viewResolver; } }
Create Spring Dispatcher Servlet Initializer Spring MVC provides support for web app initialization.
Your code is used to initialize the servlet container.
AbstractAnnotationConfigDispatcherServletInitializer
Extend the abstract base class
Override some required methods
Specify servlet mapping and location of your app config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class MySpringMvcDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[0 ]; } @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{ DemoAppConfig.class }; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } }
Develop Spring Controller 1 2 3 4 5 6 7 @Controller public class DemoController { @GetMapping("/") public String showHome () { return "home" ; } }
Develop JSP View Page Create a test.jsp file and run the tomcat server to test the demo.
Create Spring Security Initializer AbstractSecurityWebApplicationInitializer A special class to register the Spring Security Filters.
TODO List : Just extends the abstract class! No methods to override!
1 2 3 public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { }
Create Spring Security Configuration 1 2 3 4 5 6 7 8 9 @Configuration @EnableWebSecurity public class DemoSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { } }
Add Users Passwords and Roles Add users in DemoSecurityConfig.java
1 2 3 4 5 6 UserBuilder users = User.withDefaultPasswordEncoder(); auth.inMemoryAuthentication() .withUser(users.username("John" ).password("test123" ).roles("EMPLOYEE" )) .withUser(users.username("Mary" ).password("test123" ).roles("MANAGER" )) .withUser(users.username("Susan" ).password("test123" ).roles("ADMIN" ));
Modify Spring Security Configuration To reference custom login form.
1 2 3 4 5 6 7 8 9 10 @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/showMyLoginPage" ) .loginProcessingUrl("/authenticateTheUser" ) .permitAll(); }
Override the configure method.
Develop a Controller To show the custom login form.
1 2 3 4 5 6 7 @Controller public class LoginController { @GetMapping("/showMyLoginPage") public String showMyLoginPage () { return "plain-login" ; } }
Why use Context Path?
Allows us to dynamically reference context path of application.
Helps to keep links relative to application context path.LL
If you change context path of app, then links will still work.
Much better than hard-coding the context path.
1 2 3 4 5 6 7 8 9 <body> <h3> My Custom Login Page</h3> <form:form action="${pageContext.request.contextPath}/authenticateTheUser" method="post" > <p>User Name: <input type="text" name="username"/></p> <p>Password: <input type="password" name="password"/></p> <input type="submit" value="Login" /> </form:form> </body> </html>
Add Error Message 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <body> <h3> My Custom Login Page</h3> <form:form action="${pageContext.request.contextPath}/authenticateTheUser" method="post" > <c:if test="${param.error != null}" > <i class="failed">Sorry, you entered wrong credentials.</i> </c:if> <p>User Name: <input type="text" name="username"/></p> <p>Password: <input type="password" name="password"/></p> <input type="submit" value="Login" /> </form:form> </body> </html>
Note: Change http://java.sun.com/jstl/core
to http://java.sun.com/jsp/jstl/core
, unless you will get Error: attribute test does not accept any expressions
.
Add Logout Support Add Logout to Configuration 1 2 3 4 5 6 7 8 9 10 11 12 @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/showMyLoginPage" ) .loginProcessingUrl("/authenticateTheUser" ) .permitAll() .and() .logout().permitAll(); }
Note: This will add logout support for default URL /logout
The button needs to send data to the default URL /logout
Logout URL will be handled by Spring Security Fileters - No coding required.
1 2 3 <form:form action="${pageContext.request.contextPath}/logout" method="post" > <input type="submit" value="Logout" > </form:form>
1 2 3 4 5 <c:if test="${param.logout != null}" > <div class ="alert alert-success col-xs-offset-1 col-xs-10" > You have been logged out. </div> </c:if>
Cross Site Request Forgery(CSRF) What is CSRF?
CSRF(Cross Site Request Forgery)即跨站请求伪造。就是利用后台有规律的接口,例如 localhost/deleteAriticle.php?id=3&username=xiaoxiao ,攻击者在被攻击的网站页面嵌入这样的代码,当用户xiaoxiao访问该网站的时候,会发起这条请求。服务器会删除id为3的数据。 客户端防范:对于数据库的修改请求,全部使用POST提交,禁止使用GET请求。 服务器端防范:一般的做法是在表单里面添加一段隐藏的唯一的token(请求令牌)。
Spring Security CSRF Protection Spring Security uses the Synchronizer Token Pattern .
Each request includes a session cookie and randomly generated token.
For request processing, Spring Security verifies token before processing.
All of this is handled by Spring Security Filters automatically.
When to use CSRF Protection? Use CSRF protection for any normal browser web requests.
How to use CSRF Protection? For form submissions use POST instead of GET .
Just use Spring MVC Form Tags.
<form:form>
will automatically adds CSRF token.
Manually Add CSRF Token 1 2 3 4 5 <form action ="" method ="" > <input type ="hidden" name ="${_csrf.parameterName}" value ="${_csrf.token}" /> </form >
If you use plain <form>
tag, then you need to manually add CSRF token authentication in the form submission.
Break It Use plain <form>
tag rather than Spring MVC <form:form>
tag.
1 <form action="${pageContext.request.contextPath}/authenticateTheUser" method="POST" class ="form-horizontal" >
Then, we will get an ugly 403 - Forbidden
error message.
The server understood the request but refused to authorize it.
Fix It Add token manually like mentioned above.
Display Username and Roles Add new Dependency in pom.xml 1 2 3 4 5 <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-taglibs</artifactId > <version > ${springsecurity.version}</version > </dependency >
Update JSP Page 1 2 3 4 5 <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %> <p> Users: <security:authentication property="principal.username" /> Roles: <security:authentication property="principal.authorities" /> </p>
Restrict Access based on Role Create Controller and Views 1 2 3 4 5 6 7 8 9 @GetMapping("/leader") public String showLeaders () { return "leader" ; }@GetMapping("/system") public String showSystem () { return "system" ; }
Update Roles 1 2 3 4 auth.inMemoryAuthentication() .withUser(users.username("John" ).password("test123" ).roles("EMPLOYEE" )) .withUser(users.username("Mary" ).password("test123" ).roles("EMPLOYEE" ,"MANAGER" )) .withUser(users.username("Susan" ).password("test123" ).roles("EMPLOYEE" ,"ADMIN" ));
Authorize Access to Roles 1 2 3 4 5 6 http.authorizeRequests() .antMatchers("/" ).hasRole("EMPLOYEE" ) .antMatchers("/leader/**" ).hasRole("MANAGER" ) .antMatchers("/system/**" ).hasRole("ADMIN" ) .and() .exceptionHandling().accessDeniedPage("/access-denied" );
Create the Access-Denied Page 1 2 3 4 5 6 7 8 9 <body> <h1> Access Denied! </h1> <p> <a href="${pageContext.request.contextPath}/">Back To Home Page</a> </p> </body> </html>
Display Content Based on Roles 1 2 3 4 5 6 7 8 9 10 11 <security:authorize access="hasRole('MANAGER')" > <p> <a href="${pageContext.request.contextPath}/leader">Leadership Meeting</a> (Only for Managers) </p> </security:authorize> <security:authorize access="hasRole('ADMIN')" > <p> <a href="${pageContext.request.contextPath}/system">Admin </a> (Only for Admins) </p> </security:authorize>
Add JDBC Database Authentication Run SQL Scripts 1 2 3 4 5 INSERT INTO `users` VALUES ('john' ,'{noop}test123' ,1 ), ('mary' ,'{noop}test123' ,1 ), ('susan' ,'{noop}test123' ,1 );
Note: noop
is the encoding algorithm of the password.
Add Database Support to pom.xml 1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.45</version > </dependency > <dependency > <groupId > com.mchange</groupId > <artifactId > c3p0</artifactId > <version > 0.9.5.2</version > </dependency >
Create JDBC Properties File src/main/resources/persistence-mysql.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 jdbc.driver =com.mysql.jdbc.Driver jdbc.url =jdbc:mysql://localhost:3306/spring_security_demo_plaintext?useSSL=false&serverTimezone=UTC jdbc.user =springstudent jdbc.password =springstudent connection.pool.initialPoolSize =5 connection.pool.minPoolSize =5 connection.pool.maxPoolSize =20 connection.pool.maxIdleTime =3000
Define DataSource in Spring Config Introduce the Environment
variable, this will hold data from the properties file.
Add the @Autowired
annotation.
1 2 3 4 5 6 @Autowired private Environment env; private Logger logger = Logger.getLogger(getClass().getName());
Define a Bean for the security data source.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Bean public DataSource securityDataSource () { ComboPooledDataSource securityDataSource; try { securityDataSource = new ComboPooledDataSource(); } catch (Exception e){ throw new RuntimeException(e); } try { securityDataSource.setDriverClass(env.getProperty("jdbc.driver" )); } catch (PropertyVetoException e) { throw new RuntimeException(e); } logger.info(">>> jdbc.url=" +env.getProperty("jdbc.url" )); logger.info(">>> jdbc.user=" +env.getProperty("jdbc.user" )); securityDataSource.setJdbcUrl(env.getProperty("jdbc.url" )); securityDataSource.setUser(env.getProperty("jdbc.user" )); securityDataSource.setPassword(env.getProperty("jdbc.password" )); securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.initialPoolSize" )); securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.minPoolSize" )); securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.maxPoolSize" )); securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.maxIdleTime" )); return securityDataSource; }
Define a helper method to read the env properties and convert them to integer.
1 2 3 4 5 6 7 private int getIntProperties (String propsName) { String propVal = env.getProperty(propsName); int intPropVal = Integer.parseInt(propVal); return intPropVal; }
Add JDBC Configuration Retrieve a securityDataSource
Bean from the factory and inject into DemoSecurityConfig.java
1 2 3 4 5 6 7 @Autowired private DataSource securityDataSource;@Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication().dataSource(securityDataSource); }
Diagram
Password Encryption Run SQL Scripts Change the DDL to Bcrypt
hashing.
1 2 3 4 5 6 7 DROP TABLE IF EXISTS `users`;CREATE TABLE `users` ( `username` varchar (50 ) NOT NULL , `password` char (68 ) NOT NULL , `enabled` tinyint(1 ) NOT NULL , PRIMARY KEY (`username`) ) ENGINE= InnoDB DEFAULT CHARSET= latin1;
Password must be 68 chars.
{bcrypt}
- 8 chars.
{encodedPassword}
- 60 chars.
Modify db Properties 1 jdbc.url =jdbc:mysql://localhost:3306/spring_security_demo_bcrypt?useSSL=false&serverTimezone=UTC
Behind the Scenes
Retrieve the password from db for the user
Read the encoding algorithm id
For case of bcrypt, encrypt the plaintext password from the login form
Compare the encrypted password from login from WITH encrypted password from db.
If there is a matc, then login successful.
Note: The password from db is NEVER decrypted. Because Bcrypt is a one way encryption algorithm.