Filter Security

This section describes how to apply Spring Security to the Spring MVC bulletin board. (Spring Security is an authentication framework based on Spring framework)

Modify pom.xml as shown below.

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <maven.compiler.source>17</maven.compiler.source>
  <maven.compiler.target>17</maven.compiler.target>
  <spring.version>5.3.33</spring.version>
  <spring.security.version>5.8.10</spring.security.version>
</properties>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>${spring.security.version}</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-taglibs</artifactId>
  <version>${spring.security.version}</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>${spring.security.version}</version>
</dependency>

Create authorities table to java account

You need the users and the authorities table to use Spring Security. You can use our member as it is instead of the table users.

Create an authorities table and insert test data.

CREATE TABLE authorities (
  email VARCHAR2(60) NOT NULL,
  authority VARCHAR2(20) NOT NULL,
  CONSTRAINT fk_authorities FOREIGN KEY(email) REFERENCES member(email)
);

CREATE UNIQUE INDEX ix_authorities ON authorities(email, authority); 

INSERT INTO member VALUES ('john@gmail.org','1111','John','010-1111-1111');
INSERT INTO member VALUES ('jane@gmail.org','1111','Jane','010-1111-2222');

INSERT INTO authorities VALUES ('john@gmail.org','ROLE_USER');
INSERT INTO authorities VALUES ('john@gmail.org','ROLE_ADMIN');
INSERT INTO authorities VALUES ('jane@gmail.org','ROLE_USER');

commit;

ROLE_USER is the regular user privileges, and ROLE_ADMIN is the administrator privileges. John has both ROLE_USER and ROLE_ADMIN, and Jane has only ROLE_USER.

Filter Security

Create a spring configuration file only for Spring Security in the /WEB-INF/spring folder named security.xml (no name restriction), as shown below.

security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
  xmlns:beans="http://www.springframework.org/schema/beans"
  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/security
    http://www.springframework.org/schema/security/spring-security.xsd">

  <http>
    <access-denied-handler error-page="/403" />  
    <intercept-url pattern="/users/bye_confirm" access="permitAll" />
    <intercept-url pattern="/users/welcome" access="permitAll" />
    <intercept-url pattern="/users/signUp" access="permitAll" />
    <intercept-url pattern="/users/login" access="permitAll" />
    <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/>
    <intercept-url pattern="/users/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')"/>
    <intercept-url pattern="/bbs/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
    <intercept-url pattern="/**" access="permitAll" />
    
    <form-login login-page="/users/login" 
      authentication-failure-url="/users/login?error=1" 
      default-target-url="/bbs/list?boardCd=chat&amp;page=1" />
    
    <logout logout-success-url="/users/login" invalidate-session="true" />
    
    <session-management>
      <concurrency-control max-sessions="1"
        error-if-maximum-exceeded="true" />
    </session-management>
  </http>

  <global-method-security pre-post-annotations="enabled" />
  
  <beans:bean id="webexpressionHandler" 
    class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" /> 

  <authentication-manager>
    <authentication-provider>
      <jdbc-user-service 
        data-source-ref="dataSource"
        users-by-username-query="SELECT email as username,passwd as password,1 as enabled 
          FROM member WHERE email = ?"
        authorities-by-username-query="SELECT email as username,authority 
          FROM authorities WHERE email = ?" />
    </authentication-provider>
  </authentication-manager>

</beans:beans>

Since Spring Security 4, the default values of the following attributes have changed:

  • use-expressions: true
  • login-page: /login
  • login-processing-url: /login (POST method)
  • username-parameter: username
  • password-parameter: password
  • authentication-failure-url: /login?error=1

If you want to use a non-default login page such as '/users/ login' and go back to the login page in case of a login failure, you must specify the login-page and authentication-failure-url attributes and add <intercept-url pattern="/users/login" access="permitAll" /> into http element.

Modify web.xml as follows.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0">
      
  <display-name>Spring BBS</display-name>
  
  <listener>
    <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
  </listener>

  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>  
  </filter-mapping> 
  
  <servlet>
    <servlet-name>spring-bbs</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-value>
      <param-value>/WEB-INF/spring/*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring-bbs</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <error-page>
    <error-code>403</error-code>
    <location>/WEB-INF/views/noAuthority.jsp</location>
  </error-page>
</web-app>

Remove the login and logout methods from UsersController.java.

UsersController.java
/*
@RequestMapping(value="/login", method=RequestMethod.POST)
public String login(String email, String passwd, HttpSession session) {
  User user = userService.login(email, passwd);
  if (user != null) {
    session.setAttribute(WebContants.USER_KEY, user);
    return "redirect:/users/changePasswd";
  } else {
    return "redirect:/users/login";
  }
}

@RequestMapping(value="/logout", method=RequestMethod.GET)
public String logout(HttpSession session) {
  session.removeAttribute(WebContants.USER_KEY);
  return "redirect:/users/login";
}
*/

Modify login.jsp as follows.

/WEB-INF/views/users/login.jsp
<c:if test="${not empty param.error }">
  <h2>${SPRING_SECURITY_LAST_EXCEPTION.message }</h2>
</c:if>
<c:url var="loginUrl" value="/login" />
<form id="loginForm" action="${loginUrl }" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
<table>
<tr>
  <td style="width: 200px;">Email</td>
  <td style="width: 390px"><input type="text" name="username" style="width: 99%;" /></td>
</tr>
<tr>
  <td>Password</td>
  <td><input type="password" name="password" style="width: 99%;" /></td>
</tr>
</table>

If you try to log in without <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />, you will see a blank screen. Because Spring Security 's CSRF prevention works.

Spring Security enables CSRF prevention by default. Therefore, you must include the CSRF token in the PATCH, POST, PUT, and DELETE requests. If you use springform tags, you do not need to add the CSRF token parameter because springform tags add the token parameter.

Modify header.jsp as follows.

/WEB-INF/views/inc/header.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<h1 style="float: left;width: 150px;"><a href="../"><img src="../images/ci.gif" alt="java-school logo" /></a></h1>
<div id="memberMenu" style="float: right;position: relative;top: 7px;">
<security:authorize access="hasAnyRole('ROLE_USER','ROLE_ADMIN')">
  <security:authentication property="principal.username" var="check" />
</security:authorize>
<c:choose>
  <c:when test="${empty check}">
    <input type="button" value="Login" onclick="location.href='/users/login'" />
    <input type="button" value="SignUp" onclick="location.href='/users/signUp'" />
  </c:when>
  <c:otherwise>
    <input type="button" value="Logout" id="logout" />
    <input type="button" value="Modify Account" onclick="location.href='/users/editAccount'" />
  </c:otherwise>
</c:choose>
</div>
<form id="logoutForm" action="/logout" method="post" style="display:none">
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>
<script type="text/javascript" src="/resources/js/jquery-3.0.0.min.js"></script>

<script>
$(document).ready(function() {
  $('#logout').click(function() {
    $('#logoutForm').submit();
    return false;
    });
});
</script>

Create a noAuthority.jsp as shown belows.

/WEB-INF/views/noAuthority.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>403</title>
</head>
<body>
Insufficient privileges.
</body>
</html>

Modify sources

UserMapper.xml
<insert id="insertAuthority">
  INSERT INTO authorities VALUES (#{email}, #{authority})
</insert>

<delete id="deleteAuthority">
  DELETE FROM authorities WHERE email = #{email}  
</delete>
UserMapper.java
public void insertAuthority(@Param("email") String email, @Param("authority") String authority);
  
public void deleteAuthority(@Param("email") String email);
UserService.java
public void addAuthority(String email, String authority);
UserServiceImpl.java
@Override
public void addAuthority(String email, String authority) {
  userMapper.insertAuthority(email, authority);
}

@Override
public void bye(User user) {
  userMapper.deleteAuthority(user.getEmail());
  userMapper.delete(user);
}
UsersController
//omit..


import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import java.security.Principal;
import org.springframework.ui.Model;


//omit..

@RequestMapping(value="/signUp", method=RequestMethod.POST)
public String signUp(User user) {

  String authority = "ROLE_USER";

  userService.addUser(user);
  userService.addAuthority(user.getEmail(), authority);

  return "redirect:/users/welcome";
}

@RequestMapping(value="/editAccount", method=RequestMethod.GET)
public String editAccount(Principal principal, Model model) {
  User user = userService.getUser(principal.getName());
  model.addAttribute(WebContants.USER_KEY, user);

  return "users/editAccount";
}

@RequestMapping(value="/editAccount", method=RequestMethod.POST)
public String editAccount(User user, Principal principal) {
  
  user.setEmail(principal.getName());

  int check = userService.editAccount(user);
  if (check < 1) {
    throw new RuntimeException(WebContants.EDIT_ACCOUNT_FAIL);
  } 

  return "redirect:/users/changePasswd";
  
}

@RequestMapping(value="/changePasswd", method=RequestMethod.GET)
public String changePasswd(Principal principal, Model model) {
  User user = userService.getUser(principal.getName());

  model.addAttribute(WebContants.USER_KEY, user);

  return "users/changePasswd";
}

@RequestMapping(value="/changePasswd", method=RequestMethod.POST)
public String changePasswd(String currentPasswd, String newPasswd, Principal principal) {
  
  int check = userService.changePasswd(currentPasswd,newPasswd, principal.getName());

  if (check < 1) {
    throw new RuntimeException(WebContants.CHANGE_PASSWORD_FAIL);
  }  

  return "redirect:/users/changePasswd_confirm";

}

@RequestMapping(value="/bye", method=RequestMethod.POST)
public String bye(String email, String passwd, HttpServletRequest req) 
    throws ServletException {

  User user = userService.login(email, passwd);
  userService.bye(user);
  req.logout();

  return "redirect:/users/bye_confirm";
}
BbsController.java
//omit..

import java.security.Principal;

//omit..

@RequestMapping(value="/write", method=RequestMethod.POST)
public String write(MultipartHttpServletRequest mpRequest, Principal principal) 
    throws Exception {

  //omit..
  Article article = new Article();
  article.setBoardCd(boardCd);
  article.setTitle(title);
  article.setContent(content);
  article.setEmail(principal.getName());
    
  boardService.addArticle(article);  
    
  //omit..
    
  int size = fileList.size();
  for (int i = 0; i < size; i++) {
    MultipartFile mpFile = fileList.get(i);
    AttachFile attachFile = new AttachFile();
    String filename = mpFile.getOriginalFilename();
    attachFile.setFilename(filename);
    attachFile.setFiletype(mpFile.getContentType());
    attachFile.setFilesize(mpFile.getSize());
    attachFile.setArticleNo(article.getArticleNo());
    attachFile.setEmail(principal.getName());
    boardService.addAttachFile(attachFile);
  }

  //omit..
}

@RequestMapping(value="/addComment", method=RequestMethod.POST)
public String addComment(Integer articleNo, 
    String boardCd, 
    Integer page, 
    String searchWord, 
    String memo,
    Principal principal) throws Exception {
    
  Comment comment = new Comment();
  comment.setArticleNo(articleNo);
  comment.setEmail(principal.getName());
  comment.setMemo(memo);

  //omit..
}

You must add a CSRF token to the request as a query string for requests to upload files. (<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> does not work. this is true even if you are using Spring form tags)

Open the write.jsp and modify.jsp and remove >input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> and modify the form's action like below.

write.jsp
<sf:form action="write?${_csrf.parameterName}=${_csrf.token}" method="post" ...
modify.jsp
<sf:form action="modify?${_csrf.parameterName}=${_csrf.token}" method="post" ...

Test

After Build, restart Tomcat and visit the following address:
http://localhost:8080/list?boardCd=chat&page=1.

You will see the login page.
Try login as username: jane@gmail.org, password: 1111.
If login is successful, you will see the bulletin board screen.
Try to visit http://localhost:8080/admin.
Spring Security denied your access because the account has only normal user privilege, so that you will see noAuthority.jsp.

References