form.asc 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. = Creating a Custom Login Form
  2. :author: Rob Winch
  3. :starter-appname: hellomvc-jc
  4. :completed-appname: form-jc
  5. :verify-starter-app-include: form-includes/verify-app.asc
  6. This guide builds off of link:hellomvc.html[Hello Spring MVC Security Java Config] to explain how to configure and use a custom login form with Spring Security Java Configuration.
  7. include::hello-includes/setting-up-the-sample.asc[]
  8. = Overriding the default configure(HttpSecurity) method
  9. As we saw in link:hellomvc.html[Hello Spring MVC Security Java Config], Spring Security's `WebSecurityConfigurerAdapter` provides some convenient defaults to get our application
  10. up and running quickly. However, our login form does not match the rest of our application. Let's see how we can update our configuration to use a custom form.
  11. == Default configure(HttpSecurity)
  12. The default configuration for the configure(HttpSecurity) method can be seen below:
  13. [source,java]
  14. ----
  15. protected void configure(HttpSecurity http) throws Exception {
  16. http
  17. .authorizeRequests()
  18. .anyRequest().authenticated() <1>
  19. .and()
  20. .formLogin() <2>
  21. .and()
  22. .httpBasic(); <3>
  23. }
  24. ----
  25. The configuration ensures that:
  26. <1> every request requires the user to be authenticated
  27. <2> form based authentication is supported
  28. <3> HTTP Basic Authentication is supported
  29. == Configuring a custom login page
  30. We will want to ensure we compensate for overriding these defaults in our updates. Open up the `SecurityConfig` and insert the configure method as shown below:
  31. .src/main/java/org/springframework/security/samples/config/SecurityConfig.java
  32. [source,java]
  33. ----
  34. // ...
  35. @Configuration
  36. @EnableWebSecurity
  37. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  38. @Override
  39. protected void configure(HttpSecurity http) throws Exception {
  40. http
  41. .authorizeRequests()
  42. .anyRequest().authenticated()
  43. .and()
  44. .formLogin()
  45. .loginPage("/login");
  46. }
  47. // ...
  48. }
  49. ----
  50. The line `loginPage("/login")` instructs Spring Security
  51. * when authentication is required, redirect the browser to */login*
  52. * we are in charge of rendering the login page when */login* is requested
  53. * when authentication attempt fails, redirect the browser to */login?error* (since we have not specified otherwise)
  54. * we are in charge of rendering a failure page when */login?error* is requested
  55. * when we successfully logout, redirect the browser to */login?logout* (since we have not specified otherwise)
  56. * we are in charge of rendering a logout confirmation page when */login?logout* is requested
  57. Go ahead and start up the server and try visiting http://localhost:8080/sample/ to see the updates to our configuration. In many browsers you will see an error similar to *This webpage has a redirect loop*. What is happening?
  58. == Granting access to unauthenticated users
  59. The issue is that Spring Security is protecting access to our custom login page. In particular the following is happening:
  60. * We make a request to our web application
  61. * Spring Security sees that we are not authenticated
  62. * We are redirected to */login*
  63. * The browser requests */login*
  64. * Spring Security sees that we are not authenticated
  65. * We are redirected to */login* ...
  66. To fix this we need to instruct Spring Security to allow anyone to access the */login* URL. We can easily do this with the following updates:
  67. .src/main/java/org/springframework/security/samples/config/SecurityConfig.java
  68. [source,java]
  69. ----
  70. // ...
  71. @Configuration
  72. @EnableWebSecurity
  73. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  74. @Override
  75. protected void configure(HttpSecurity http) throws Exception {
  76. http
  77. .authorizeRequests()
  78. .anyRequest().authenticated()
  79. .and()
  80. .formLogin()
  81. .loginPage("/login")
  82. .permitAll();
  83. }
  84. // ...
  85. }
  86. ----
  87. The `permitAll()` statement instructs Spring Security to allow any access to any URL (i.e. */login* and */login?error*) associated to `formLogin()`.
  88. NOTE: Granting access to the `formLogin()` URLs is not done by default since Spring Security needs to make certain assumptions about what is allowed and what is not. To be secure, it is best to ensure granting access to resources is explicit.
  89. Start up the server and try visiting http://localhost:8080/sample/ to see the updates to our configuration. You should now get a 404 error stating that */login* cannot be found.
  90. = Creating a login page
  91. Within Spring Web MVC there are two steps to creating our login page:
  92. * Creating a controller
  93. * Creating a view
  94. == Configuring a login view controller
  95. Within Spring Web MVC, the first step is to ensure that we have a controller that can point to our view. Since our project adds the *messages-jc* project as a dependency and it contains a view controller for */login* we do not need to create a controller within our application. For reference, you can see the configuration below:
  96. [source,java]
  97. ----
  98. // ...
  99. @EnableWebMvc
  100. @ComponentScan("org.springframework.security.samples.mvc")
  101. public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
  102. // ...
  103. @Override
  104. public void addViewControllers(ViewControllerRegistry registry) {
  105. registry.addViewController("/login").setViewName("login");
  106. registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
  107. }
  108. @Bean
  109. public InternalResourceViewResolver jspxViewResolver() {
  110. InternalResourceViewResolver result = new InternalResourceViewResolver();
  111. result.setPrefix("/WEB-INF/views/");
  112. result.setSuffix(".jspx");
  113. return result;
  114. }
  115. }
  116. ----
  117. == Creating a login view
  118. Our existing configuration means that all we need to do is create a *login.jspx* file with the following contents:
  119. .src/main/webapp/WEB-INF/views/login.jspx
  120. [source,xml]
  121. ----
  122. <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
  123. xmlns:spring="http://www.springframework.org/tags"
  124. xmlns:c="http://java.sun.com/jsp/jstl/core"
  125. xmlns:form="http://www.springframework.org/tags/form" version="2.0">
  126. <jsp:directive.page language="java" contentType="text/html" />
  127. <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
  128. <head>
  129. <title>Please Login</title>
  130. </head>
  131. <body>
  132. <c:url value="/login" var="loginUrl"/>
  133. <form:form name="f" action="${loginUrl}" method="post"> <1>
  134. <fieldset>
  135. <legend>Please Login</legend>
  136. <c:if test="${param.error != null}"> <2>
  137. <div class="alert alert-error">
  138. Invalid username and password.
  139. </div>
  140. </c:if>
  141. <c:if test="${param.logout != null}"> <3>
  142. <div class="alert alert-success">
  143. You have been logged out.
  144. </div>
  145. </c:if>
  146. <label for="username">Username</label>
  147. <input type="text" id="username" name="username"/> <4>
  148. <label for="password">Password</label>
  149. <input type="password" id="password" name="password"/> <5>
  150. <div class="form-actions">
  151. <button type="submit" class="btn">Log in</button>
  152. </div>
  153. </fieldset>
  154. </form:form>
  155. </body>
  156. </html>
  157. </jsp:root>
  158. ----
  159. <1> The URL we submit our username and password to is the same URL as our login form (i.e. */login*), but a *POST* instead of a *GET*.
  160. <2> When authentication fails, the browser is redirected to */login?error* so we can display an error message by detecting if the parameter *error* is non-null.
  161. IMPORTANT: Do not display details about why authentication failed. For example, we do not want to display that the user does not exist as this will tell an attacker that they should try a different username.
  162. <3> When we are successfully loged out, the browser is redirected to */login?logout* so we can display an logout success message by detecting if the parameter *logout* is non-null.
  163. <4> The username should be present on the HTTP parameter username
  164. <5> The password should be present on the HTTP parameter password
  165. TIP: We use Spring Web MVC's <form:form> tag to automatically add the CSRF token to our form. We could also manually add the CSRF token using `<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>`.
  166. Start up the server and try visiting http://localhost:8080/sample/ to see the updates to our configuration. We now see our login page, but it does not look very pretty. The issue is that we have not granted access to the css files.
  167. == Grant access to remaining resources
  168. We need to update our configuration to allow anyone to access our resources and our logout pages. Update the configuration as shown below:
  169. .src/main/java/org/springframework/security/samples/config/SecurityConfig.java
  170. [source,java]
  171. ----
  172. // ...
  173. @Configuration
  174. @EnableWebSecurity
  175. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  176. @Override
  177. protected void configure(HttpSecurity http) throws Exception {
  178. http
  179. .authorizeRequests()
  180. .antMatchers("/resources/**").permitAll() <1>
  181. .anyRequest().authenticated()
  182. .and()
  183. .formLogin()
  184. .loginPage("/login")
  185. .permitAll()
  186. .and()
  187. .logout() <2>
  188. .permitAll();
  189. }
  190. // ...
  191. }
  192. ----
  193. <1> This allows anyone to access a URL that begins with */resources/*. Since this is where our css, javascript, and images are stored all our static resources are viewable by anyone.
  194. <2> As you might expect, `logout().permitAll()` allows any user to request logout and view logout success URL.
  195. Start up the server and try visiting http://localhost:8080/sample/ to see the updates to our configuration. We now see a custom login page that looks like the rest of our application.
  196. * Try entering an invalid username and password. You will see our error message is displayed.
  197. * Try entering a valid username and password. You will be authenticated successfully.
  198. * Try clicking the Log Out button. You will see our logout success message