Spring Security를 이해한다.

Goal

  • Spring Security란
  • Spring Security 사용법
    • 1) 필요한 Library
    • 2) Spring Security 활성화
    • 3) Spring Security 설정
  • (참고) Expression-Based Access Control
  • (참고) Custom Login/Logout Form
  • Spring Security Architecture (전체 과정)

Spring Security란

  1. Authorization(권한): 권한이 없는 User가 접근할 경우, 자동으로 Login Page를 띄어준다.
  2. Authentication(인증): 사용자가 입력한 id/pw가 일치하는지 Authentication Providers(DB)를 통해 확인한다.
  3. id/pw가 일치하지 않으면 1, 2번을 반복한다.
  4. 인증과 권한이 통과가 되면 Secured Area에 접근을 허용한다.

Spring Security 사용법

1. 필요한 Library

<properties>
        <org.springframework-version>5.1.3.RELEASE</org.springframework-version>
        <org.springsecurity-version>4.2.3.RELEASE</org.springsecurity-version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>${org.springsecurity-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>${org.springsecurity-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>${org.springsecurity-version}</version>
    </dependency>
</dependencies>

2. Spring Security 활성화

<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>

3. Spring Security 설정

web.xml 설정

<!-- web.xml -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        /WEB-INF/applicationContext.xml
        /WEB-INF/securityContext.xml 
    </param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

securityContext.xml 설정

# 1. beans에 추가할 설정

<!-- securityContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:security="http://www.springframework.org/schema/security"
       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">
       
       ...
</beans>

# 2. Authentication(인증) 설정

<!-- Authentication(인증) 설정 -->

<!--방법 1. User Authentication with In-Memory definition-->
<security:authentication-manager>
    <security:authentication-provider>
        <security:user-service>
            <security:user name="admin" password="1234" authorities="ROLE_ADMIN"/>
            <security:user name="testUser" password="test" authorities="ROLE_USER"/>
        </security:user-service>
    </security:authentication-provider>
</security:authentication-manager>

<!--방법 2. Other Authentication Provider (Using Database)-->
<security:authentication-manager>
    <security:authentication-provider>
    <security:jdbc-user-service
        data-source-ref="dataSource"
        users-by-username-query="select username, password, enabled from users where username=?"
        authorities-by-username-query="select username, authority from authorities where username=?"/>
    </security:authentication-provider>
</security:authentication-manager>
  1. <security:user-service>
    • test를 위해 메모리 상에 일시적으로 하드 코드된 사용자를 정의한다.
      • 이름은 “admin”, 비밀번호는 “1234”인 사용자에게 “ROLE_ADMIN” 권한 부여
      • 이름은 “admtestUserin”, 비밀번호는 “test”인 사용자에게 “ROLE_USER” 권한 부여
  2. <security:jdbc-user-service>
    • DB에 사용자 계정 정보를 저장한다.
      • table은 개발자가 설계한다.
      • 여기서는 계정 정보 테이블과 권한 정보 테이블을 만들었다.
      • 이 정보들은 사용자가 회원가입할 때 저장해야하는 정보들이다.
    • data-source-ref="dataSource"
      • DB 접근을 위한 명시
    • 데이터베이스 정보를 추출하기 위해 수행할 구체적인 쿼리를 정의한다.
      • 아래의 쿼리는 Default로 설정되어 있다. (명시하지 않아도 됨)
        • Spring Security가 인증, 권한을 확인하기 위한 사용자의 정보들을 가져오기 위해서 Spring에게 해당 쿼리문을 수행하라고 명시하는 것이다.
      • 1) users-by-username-query="select username, password, enabled from users where username=?"
        • 사용자가 입력한 username 정보를 바탕으로 인증을 위한 username, password, enabled 정보를 가져온다.
      • 2) authorities-by-username-query="select username, authority from authorities where username=?"
        • 사용자가 입력한 username 정보를 바탕으로 접근 제어를 위한 authority(권한 정보)을 가져온다.
    • 이후에는 사용자가 입력한 정보와 DB에 저장되어 있는 정보를 비교하여 Spring Security가 알아서 인증, 권한을 확인한다

# 3. Authorization(권한)

<!-- Authorization(권한) 설정 -->
<security:http auto-config="true" use-expressions="true">
    <security:intercept-url pattern="/secured/**" access="hasRole('ROLE_USER')" />
    <security:intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
    <security:intercept-url pattern="/login" access="permitAll()" />
    <security:intercept-url pattern="/" access="permitAll()" />
    <security:intercept-url pattern="/resources/**" access="permitAll()" />
    <security:intercept-url pattern="/**" access="denyAll()" />
    <security:form-login login-page="/login" />
</security:http>

(참고) Expression-Based Access Control

Expression Description
hasRole([role]) 현재 로그인한 주체(principal)가 권한을 가지고 있으면 true 반환
hasAnyRole([role1,role2]) 현재 로그인한 주체가 제공된 권한 중 하나라도 가지고 있으면 true 반환 (쉼표로 여러 개의 권한 목록 지정 가능)
principal 현재 로그인한 사용자를 나타내는 객체에 직접 액세스할 수 있다.
authentication SecurityContext에서 얻은 현재 Authentication 객체에 직접 액세스할 수 있다.
permitAll 모든 사용자 접근 가능 (Always evaluates to true)
denyAll 모든 사용자 접근 불가능 (Always evaluates to false)
isAnonymous() 현재 로그인한 사용자가 익명 사용자(anonymous user)인 경우 true 반환
isRememberMe() 현재 로그인한 사용자가 기억하고 있는 사용자(remember-me user)인 경우 true 반환
isAuthenticated() 현재 로그인한 사용자가 익명이 아닌 경우 true 반환 (인증만 되어 있으면 접근 허용)
isFullyAuthenticated() 현재 로그인한 사용자가 익명 또는 기억하고 있는 사용자가 아닌 경우 true 반환

(참고) Custom Login/Logout Form

  1. default settings을 오버라이딩
    • <security:form-login login-page="/login" />
  2. 내가 정한 URL을 처리할 Login Controller(@GetMapping)을 만든다.
  3. Custom Loing Page(login.jsp)를 만든다.

1. securityContext.xml 설정

<security:form-login login-page="/login" />

security:form-login attribute (Spring Security 4)

Attribute Default value Note
login-page /login implement
username-parameter  username  
password-parameter password  
login-processing-url /login , POST the URL used to process the login request by spring
authentication-failure-url /login?error implement

security:logout attribute (Spring Security 4)

Attribute Default value Note
logout-url  /logout, POST Log out processing module by spring
logout-success-url /login?logout implement

2. Controller

@Controller
public class LoginController {
    // 사용자가 입력한 정보로부터 POST 요청은 Spring Security를 거친 후 해당 메서드로 들어온다.
    @GetMapping("/login") 
    public String login(@RequestParam(value="error", required=false) String error, 
                        @RequestParam(value="logout", required=false) String logout, 
                        Model model) {
        if(error != null) {
            model.addAttribute("errorMsg", "Invalid username and password");
        }
        if(logout != null) {
            model.addAttribute("logoutMsg", "You have been logged out successfully");
        }
        return "login"; // login.jsp(Custom Login Page)
    }    
}

3. Custom Loing Page

<!-- 로그인한 사용자가 있으면 LOGOUT이 보인다. -->
<c:if test="${pageContext.request.userPrincipal.name != null}">
    <a href="javascript:document.getElementById('logout').submit()">LOGOUT</a>
</c:if>

<!-- logout processing module of spring security 4 -->
<form id="logout" action="<c:url value="/logout" />" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>
<h3>Custom Login with Username and Password</h3>
<!-- For logout  -->
<c:if test="${not empty logoutMsg}">
    <div style="color: #0000ff;" > 
        <h3> ${logoutMsg} </h3>
    </div>
</c:if>

<!-- login processing module of spring security 4 -->
<form action="<c:url value="/login"/>" method="post">
<!-- For failed user authentication  -->
  <c:if test="${not empty errorMsg}">
    <div style="color: #ff0000;"> 
        <h3> ${errorMsg} </h3>
    </div>
  </c:if>

  <table>
    <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
    <tr><td>Password:</td><td><input type='password' name='password' /></td></tr>
    <tr><td colspan='2'><input name="submit" type="submit" value="LOGIN" /></td></tr>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  </table>
</form>

Spring Security Architecture (전체 과정)

  1. Authorization
    • securityContext.xml에 권한 설정 명시
    • 권한이 없는 User가 접근할 경우,
    • Spring이 내부적으로 Defalt 페이지인 Spring Login Page로 redirect한다.
    • 참고) 사용자가 설정한 Login Page로 redirect하는 경우, 설정 파일(securityContext.xml)의 권한 설정에 아래 내용 추가
      • <security:form-login login-page="/login" />
  2. Authentication
    • 사용자가 입력한 id/pw가 일치하는지 Authentication Providers(DB)를 통해 확인한다.
  3. id/pw가 일치하지 않으면 1, 2번을 반복한다.
  4. 인증과 권한이 통과가 되면 Secured Area에 접근을 허용한다.
    • Login한 사용자가 인증, 권한에 대해 모두 성공하면 원래의 URL(Secured Page)로 redirect한다.

사용자 동작에 따른 전체 과정 설명

사용자 흐름에 따른 전체 과정 요약

  1. 사용자가 로그인 정보를 입력하고 Submit하면 /login URL로 POST 요청이 날아간다.
  2. 이후에는 Spring Security에 의해 Login/Logout Process를 거친다.
    • Spring Security가 인증 결과에 따라 적절하게 redirect: 를 반환한다.
      • 인증 성공 시, redirect:/
      • 인증 실패 시, redirect:/login?error
      • 로그아웃 성공 시, redirect:/login?logout
  3. Controller에서 해당 redirect GET 요청을 받아 처리한다.
    • 인증 성공 시, HomeController에서 return index.jsp(Main Page)
    • 인증 실패 시, LoginController에서 오류 메시지를 Model에 담아 return login.jsp(Custom Login Page)
    • 로그아웃 성공 시, LoginController에서 로그아웃 확인 메시지를 Model에 담아 return login.jsp

로그인 인증 성공/실패

  1. home.jsp에서 Secured Page(Ex. Admin Page) 이동 클릭 [GET]
  2. 접근 제어에 따라 사용자 인증 절차 필요 (Ex. ROLE_ADMIN 사용자만 가능)
    • login-page="/login"에 의해 Spring Security가 브라우저를 “/login”로 redirect
    • 참고) 이때, login-page의 URL은 개발자가 임의로 정할 수 있다.
    • return redirect:/login; [GET]
  3. 개발자가 작성한 LoginController login method [GetMapping]
    • return login.jsp (Custom Login Page)
  4. login.jsp에서 사용자 정보 입력 후 LOGIN을 Submit [POST]
    • <form action="<c:url value="/login"/>" method="post">
    • 참고) 이때, value 값은 개발자가 임의로 정할 수 없다. (spring security에 지정되어 있는 이름)
  5. Spring Security login method [PostMapping] 실행
    • 인증 절차 성공:
      • redirect:/ [GET] (원래의 URL(Secured Page)로 redirect)
      • -> HomeController [GetMapping]에서 return index.jsp
      • 6번으로 넘어가지 않는다.
    • 인증 절차 실패:
      • request parameter에 error 추가
      • return redirect:/login?error; [GET]
  6. LoginController login method [GetMapping] 실행
    • error request parameter 유무 체크
    • Model에 errorMsg 저장
    • return login.jsp
  7. login.jsp에서 errorMsg 표시

로그아웃 인증 성공

  1. home.jsp에서 LOGOUT을 Submit [POST]
    • <form id="logout" action="<c:url value="/logout" />" method="post">
    • 참고) 이때, value 값은 개발자가 임의로 정할 수 없다. (spring security에 지정되어 있는 이름)
  2. Spring Security logout method [PostMapping] 실행
    • 로그아웃 성공:
      • request parameter에 logout 추가
      • return redirect:/login?lougut; [GET]
  3. LoginController login method [GetMapping] 실행
    • logout request parameter 유무 체크
    • model에 logoutSuccessMsg 저장
    • return login.jsp
  4. login.jsp에서 logoutSuccessMsg 표시

관련된 Post

References