[코드로 배우는 스프링 웹 프로젝트 개정판] 7장 정리
스프링 시큐리티를 이용해 로그인, 로그아웃, 자동 로그인, 인증/권한 처리, JDBC 인증/권한 처리, 사용자 정의 UserDetailsService, JSP에서의 사용 등을 처리할 수 있다. 이는 POST 방식, JDBC, BCryptPasswordEncoder, CustomUserDetailsService, Ajax 등을 활용하며, 스프링 시큐리티의 어노테이션과 표현식을 사용한다. 또한, 스프링 시큐리티는 CSRF 토큰 처리와 암호화 처리를 지원하며, 이를 통해 보안을 강화할 수 있다.
Mar 28, 2024
🌼Spring Web Security 소개
💡핵심 키워드
- 스프링 시큐리티의 기본 동작 방식은 서블릿의 여러 종류 필터와 인터셉터를 이용해서 처리된다.
- 필터는 서블릿에서 말하는 단순한 필터를 의미하고, 인터셉터는 스프링에서 필터와 유사한 역할을 한다.
- 필터는 특정한 서블릿이나 컨트롤러의 접근에 관여한다는 점에서는 유사하지만 결정적인 차이를 구분하자면 필터는 스프링과 무관하게 서블릿 자원이고, 인터셉터는 스프링의 빈으로 관리되면서 스프링의 컨텍스트 내에 속한다는 차이다.
- 스프링 시큐리티는 현재 동작하는 스프링 컨텍스트 내에서 동작하기 때문에 이미 컨텍스트에 포함된 여러 번들을 같이 이용해서 다양한 방식의 인증 처리가 가능하도록 설계할 수 있다.
Spring Web Security의 설정
- 스프링 시큐리티 관련된 태그 라이브러리를 사용할 수 있도록
pom.xml
에spring-security-taglib
을 추가한다.
- 스프링 시큐리티는 단독으로 설정할 수 있기 때문에 기존의 xml과는 별도로
security-context.xml
을 따로 작성하는 것이 좋다.
- XML을 이용해서 스프링 시큐리티를 설정할 때에는 5.0 네임스페이스에서 문제가 발생하기 때문에 빈즈의 수정이 필요하다.
- 스프링 시큐리티가 스프링 MVC에서 사용되기 위해서는 필터를 이용해서 스프링 동작에 관여하도록 설정이 필요하다.
- 스프링 시큐리티가 동작하기 위해서는
Authentication Manager
라는 존재와 스프링 시큐리티의 시작 지점이 필요하다.
인증과 권한 부여
- 인증은 자기 스스로가 무언가 자신을 증명할 만한 자료를 제시하는 것이다.
- 권한 부여는 남에 의해서 자격이 부여된다는 것이다.
- 스프링 시큐리티의 내부에도 이와 비슷한 구조를 가지고 있다.
AuthenticationManager
는 다양한 방식의 인증을 처리할 수 있는 구조로 설계되어 있다.ProviderManager
는 인증에 대한 처리를AuthenticationProvider
라는 타입의 객체를 이용해서 처리를 위임한다.AuthenticationProvider
는 실제 인증 작업을 진행한다. 이때 인증된 정보에는 권한에 대한 정보를 같이 전달하게 되는데 이 처리는UserDetailsService
라는 존재와 관련이 있다.UserDetailsService
인터페이스의 구현체는 사용자의 정보와 사용자가 가진 권한의 정보를 처리해서 반환한다.
- 개발자가 스프링 시큐리티를 커스터마이징하는 방식은 크게
AuthenticationProvider
를 직접 구현하는 방식과 실제 처리를 담당하는UserDetailsService
를 구현하는 방식으로 나누어진다.
🌼로그인과 로그아웃 처리
💡핵심 키워드
접근 제한 설정
- 특정한 URI에 접근할 때 인터셉터를 이용해서 접근을 제한하는 설정은
<security:intercept-url>
을 이용한다. <security:intercept-url>
은pattern
이라는 속성과access
라는 속성을 지정해야 한다.pattern
은 URI의 패턴을 의미하고,access
는 권한을 체크한다.
단순 로그인 처리
- 스프링 시큐리티에서 명심해야 하는 사항 중 하나는 스프링 시큐리티가 사용하는
username
이나User
라는 용어의 의미가 일반적인 시스템에서의 의미와 차이가 있다는 점이다. - 보통 시스템에서 사용자 아이디를 의미하는
userid
는 스프링 시큐리티에서는username
에 해당한다. 일반적으로 사용자의 이름은username
이라고 처리하는 것과 혼동하면 안된다. - User라는 용어 역시 혼란의 여지가 있다. 스프링 시쿠리티의
User
는 인증 정보와 권한을 가진 객체이므로 일반적인 경우에 사용하는 사용자 정보와는 다른 의미다.
- 스프링 시큐리티 5버전부터 반드시
PasswordEncoder
의 지정이 반드시 필요하다. 만일 패스워드의 인코딩 처리 없이 사용하고 싶다면 패스워드 앞에{noop}
문자열을 추가한다.
- 스픟링 시큐리티를 학습하다 보면 매번 로그아웃하고 새롭게 로그인을 해야 하는 사오항이 자주 발생한다.
- 개발자 도구에서
Application
탭을 확인해 보면Cookies
항목에JSESSIONID
와 같이 세션을 유지하는데 사용되는 세션 쿠키의 존재를 확인할 수 있다. - 로그아웃은
JSESSIONID
쿠키를 강제로 삭제해서 처리한다.
<security:access-denied-handler>
는org.springframework.security.web.access.AccessDeniedHandler
인터페이스의 구현체를 지정하거나error-page
를 지정할 수 있다.
- Access Denied의 경우는 403 에러 메시지가 발생한다. JSP에서는
HttpServletRequest
안에SPRING_SECURITY_403_EXCEPTION
이라는 이름으로AccessDeniedException
객체가 전달된다.
- 접근 제한이 된 경우에 다양한 처리를 하고 싶다면
AccessDeniedHandler
인터페이스를 구현하는 편이 좋다. - 이 경우
servlet-contextg.xml
에서는error-page
속성 대신에CustomAccessDeniedHandler
를 빈으로 등록해서 사용한다. <security:access-denied-handler>
는error-page
속성과ref
속성 둘 중 하나만을 사용한다.
커스텀 로그인 페이지
- 거의 대부분의 경우 별도로 URI를 이용해서 로그인 페이지를 다시 제작해서 사용한다.
- 이 경우
<form>
태그의action
속성값을/login
으로 지정하고 반드시POST
방식으로 데이터를 전송해야 한다.
CSRF(Cross-site request forgery) 공격과 토큰
- 스프링 시큐리티에서 POST 방식을 이용하는 경우 기본적으로 CSRF 토큰을 이용하게 된다. 별도의 설정이 없다면 스프링 시큐리티가 적용된 사이트의 모든 POST 방식에는 CSRF 토큰이 사용되는데
사이트간 위조 방지
를 목적으로 특정한 값의 토큰을 사용하는 방식이다.
- CSRF 공격은
사이트간 요청 위조
라고 번역될 수 있다. 서버에서 받아들이는 정보가 특별히 사전 조건을 검증하지 않는다는 단점을 이용하는 공격 방식이다.
- CSRF는
<img>
태그 등의 URI 등을 이용할 수 있기 때문에 손쉽게 공격할 수 있는 방법이 된다.
- 이를 막기 위해서 여러 방식이 존재한다.
- 사용자의 요청에 대한 출처를 의미하는
referer
헤더를 체크하거나 일반적인 경우에 잘 사용되지 않고REST
방식에서 사용되는PUT
,DELETE
와 같은 방식을 이용하는 등의 방식을 고려해 볼 수 있다.
- CSRF 토큰은 사용자가 임의로 변하는 특정한 토큰값을 서버에서 체크하는 방식이다.
- 사용자가 POST 방식 등으로 특정한 작업을 할 때는 브라우저에서 전송된 CSRF 토큰의 값과 서버가 보관하고 있는 토큰의 값을 비교한다. 만일 CSRF 토큰의 값이 다르다면 작업을 처리하지 않는 방식이다.
로그인 성공과 AuthenticationSuccessHandler
- 로그인을 처리하다 보면 로그인 성공 이후에 특정한 동작을 하도록 제어하고 싶은 경우가 있다.
- 이런 경우를 위해서 스프링 시큐리티에서는
AuthenticationSuccessHandler
라는 인터페이스를 구현해서 설정할 수 있다.
로그아웃의 처리와 LogoutSuccessHandler
- 로그인과 마찬가지로 특정한 URI를 지정하고, 로그아웃 처리 후 직접 로직을 처리할 수 있는 핸들러를 등록할 수 있다.
- 로그아웃 시 세션을 무효화 시키는 설정이나 특정한 쿠키를 지우는 작업을 지정할 수 있다.
- POST 방식으로 처리되는 로그아웃은 스프링 시큐리티의 내부에서 동작한다. 만일 로그아웃 시 추가적인 작업을 해야 한다면
logoutSuccessHandler
를 정의해서 처리한다.
🌼JDBC를 이용하는 간편 인증/권한 처리
💡핵심 키워드
- 인증과 권한에 대한 처리는 크게 보면
Authentication Manager
를 통해서 이루어지는데 이때 인증이나 권한 정보를 제공하는 존재(Provider
)가 필요하고, 다시 이를 위해서UserDetailsService
라는 인터페이스를 구현한 존재를 활용하게 된다.
jdbc-user-service
는 기본적으로DataSource
가 필요하므로 root-context.xml에 있는 설정을 추가한다.
기존의 테이블을 이용하는 경우
- 스프링 시큐리티가 기본적으로 이용하는 테이블 구조를 그대로 생성해서 사용하는 방식도 있지만, 기존의 회원 관련 데이터베이스가 구축되어 있었다면 이를 사용하는 것은 오히려 복잡해질 수 있다.
<security:jdbc-user-service>
태그에는users-by-username-query
속성과authorities-by-user-name-query
속성에 적당한 쿼리문을 지정해 주면 JDBC를 이용하는 설정을 그대로 사용할 수 있다.
BCryptPasswordEncoder 클래스를 이용한 패스워드 보호
bcrypt
는 태생 자체가 패스워드를 저장하는 용도로 설계된 해시 함수로 특정 문자열을 암호화하고, 체크하는 쪽에서는 암호화된 패스워드가 가능한 패스워드인지만 확인하고 다시 원문으로 되돌리지는 못한다.
BCryptPasswordEncoder
는 이미 스프링 시큐리티의 API 안에 포함되어 있으므로, 이를 활용해서 security-context.xml에 설정한다.PasswordEncoder
는 이미 스프링 시큐리티에서 제공하므로 이를 빈으로 추가하고,org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
로 지정한다.
- 지정된 방식이 아닌 테이블 구조를 이용하는 경우에는 인증을 하는데 필요한 쿼리(
users-by-username-query
속성값)와 권한을 확인하는데 필요한 쿼리(authorities-by-username-query
속성값)를 이용해서 처리한다.
🌼커스텀 UserDetailsService 활용
💡핵심 키워드
- JDBC를 이용하는 방식은 사용자의 여러 정보들 중에서 제한적인 내용만을 이용한다는 단점이 있다.
- 이러한 문제를 해결하기 위해서는 직접 UserDetailsService를 구현하는 방식을 이용하는 것이 좋다. 이를 이용하면 원하는 객체를 인증과 권한 체크에 사용할 수 있다.
- 가장 일반적으로 많이 사용되는 방법은 여러 하위클래스들 중에서
org.springframework.security.core.userdetails.User
클래스를 상속하는 형태다.
회원 도메인, 회원 Mapper 설계
- MyBatis에서는 하나의 결과에 부가적으로 여러 개의 데이터를 처리하는 경우 1:N의 결과를 처리할 수 있는
<resultMap>
태그를 지원한다. <resultMap>
은<result>
와<collection>
을 이용해서 바깥쪽 객체와 안쪽 객체들을 구성할 수 있다.
CustomUserDetailsService 구성
- 스프링 시큐리티의 UserDetailsService를 구현하는 클래스를 직접 작성할 경우, MemberMapper 타입의 인스턴스를 주입받아서 실제 기능을 구현한다.
- 해당 클래스는 security-context.xml을 이용해서 스프링의 빈으로 등록한다.
- 스프링 시큐리티의 UserDetailsService는
loadUserByUsername()
라는 하나의 추상 메서드만을 가지고 있으며 리턴 타입은org.springframework.security.core.userdetails.UserDetails
라는 타입이다.
org.springframework.security.core.userdetails.User
클래스를 상속한다면 부모 클래스의 생성자를 호출해야만 정상적인 객체를 생성할 수 있다.- 이 과정에서 VO 인스턴스는
GrandAuthority
객체로 변환해야 하므로stream()
과map()
을 이용해서 처리한다.
🌼스프링 시큐리티를 JSP에서 사용하기
💡핵심 키워드
- 별도의 인증/퀀한 체크를 하는 가장 큰 이유는 JSP 등에서 단순히 사용자의 아이디(스프링 시큐리티에서는 username) 정도가 아닌 사용자의 이름이나 이메일과 같은 추가적인 정보를 이용하기 위해서다.
JSP에서 로그인한 사용자 정보 보여주기
- 권한이 적당하지 않으면 보여줄 수 없는 페이지의 경우 로그인한 사용자가 접근했을 때에는 해당 사용자의 여러 정보를 보여줄 필요가 있다.
- 스프링 시쿠리티와 관련된 정보를 출력하거나 사용하려면 JSP 상단에 스프링 시큐리티 관련 태그 라이브러리의 사용을 선언하고,
<sec:authentication>
태그와principal
이라는 이름의 속성을 사용한다.
<sec:authentication property="principal"/>
가 의미하는 것은 UserDetailsService에서 반환된 객체다. 즉 CustomUserDetailsService를 이용했다면loadUserByUsername()
에서 반환된 CustomUser 객체가 된다.- 이 사실을 이해하면
principal
이 CustomUser를 의미하므로principal.member
는 CustomUser 객체의getMember()
를 호출한다는 것을 알 수 있다.
표현식을 이용하는 동적 화면 구성
- 경우에 따라서 특정한 페이지에서 로그인한 사용자의 경우에는 특정한 내용을 보여주고 그렇지 않은 경우에는 다른 내용을 보여주는 경우가 있다.
- 이 때 유용한 것이 스프링 시큐리티의 표현식이다.
표현식 | 설명 |
hasRole([role])
hasAuthority([authority]) | 해당 권한이 있으면 true |
hasAnyRole([role, role2])
hasAnyAuthority([authority]) | 여러 권한들 중에서 하나라도 해당하는 권한이 있으면 true |
principal | 현재 사용자 정보를 의미 |
permitAll | 모든 사용자에게 허용 |
denyAll | 모든 사용자에게 거부 |
isAnonymous() | 익명의 사용자의 경우(로그인을 하지 않은 경우도 해당) |
isAuthenticated() | 인증된 사용자라면 true |
isFullyAuthenticated() | Remember-me로 인증된 것이 아닌 인증된 사용자의 경우 true |
🌼자동 로그인(remember-me)
💡핵심 키워드
- 최근의 웹페이지들은
자동 로그인
이나로그인 기옥하기
라는 이름으로 한 번 로그인하면 일정 시간 동안 다시 로그인을 하지 않아도 되는 기능을 가지고 있다.
remember-me
기능은 거의 대부분 쿠키를 이용해서 구현된다.
- 스프링 시큐리티의 경우
remember-me
기능을 메모리상에서 처리하거나, 데이터베이스를 이용하는 형태로 약간의 설정만으로 구현이 가능하다.
- security-context.xml에는
<security:remember-me>
태그를 이용해서 기능을 구현한다.<security:remember-me>
태그는 다음과 같은 속성들을 지정할 수 있다. - key: 쿠키에 사용되는 값을 암호화하기 위한 키(key)값
- data-source-ref: DataSource를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록(옵션)
- remember-me-cookie: 브라우저에 보관되는 쿠키의 이름을 지정한다. 기본값은
remember-me
이다. - remember-me-parameter: 웹 화면에서 로그인할 때
remember-me
는 대부분 체크박스를 이용해서 처리한다. 이 때 체크박스 태그는 name 속성을 의미한다. - token-validity-seconds: 쿠키의 유효시간을 지정한다.
데이터베이스를 이용하는 자동 로그인
- 자동 로그인 기능을 처리하는 방식 중에서 가장 많이 사용되는 방식은 로그인이 되었던 정보를 데이터베이스를 이용해서 기록해 두었다가 사용자의 재방문 시 세션에 정보가 없으면 데이터베이스를 조회해서 사용하는 방식이다.
- 스프링 시큐리티의 공식 문서에 나오는 로그인 정보를 유지하는 테이블은 아래와 같은 스크립트로 구성된다.
create table persistent_logins( username varchar2(64) not null, series varchar2(64) primary key, token varchar2(64) not null, last_used timestamp not null );
- 자동 로그인 기능을 이용하는 경우에 사용자가 로그아웃을 하면 기존과 달리 자동 로그인에 사용하는 쿠키도 삭제해 주도록 쿠키를 삭제하는 항목을 security-context.xml에 지정한다.
🌼어노테이션을 이용하는 스프링 시큐리티 설정
💡핵심 키워드
- 스프링 시큐리티 역시 다른 기능들처럼 어노테이션을 이용해서 필요한 설정을 추가할 수 있다.
@Secured
: 스프링 시큐리티 초기부터 사용되었고, () 안에ROLE_ADMIN
과 같은 문자열 혹은 문자열 배열을 이용한다.@PreAuthorize
,@PostAuthorize
: 3버전부터 지원되며, () 안에 표현식을 사용할 수 있으므로 최근에는 더 많이 사용된다.
- 컨트롤러에 사용하는 스프링 시큐리티의 어노테이션을 활성화하기 위해서는 security-context.xml이 아닌 스프링 MVC의 설정을 담당하는 servlet-context.xml에 관련 설정을 추가해야 한다.
- security 네임스페이스를 이용해서 global-method-security를 지정한다.
- 어노테이션은 기본으로
disabled
되어 있으므로enabled
로 설정한다.
🌼기존 프로젝트에 스프링 시큐리티 접목하기
💡핵심 키워드
로그인 페이지 처리
- custumLogin.jsp를 작성할 때 신경 써야 하는 부분은 다음과 같다.
- JSTL이나 스프링 시큐리티의 태그를 사용할 수 있도록 선언
- CSS 파일이나 JS 파일의 링크는 절대 경로를 쓰도록 수정
<form>
태그 내의<input>
태그의name
속성을 스프링 시큐리티에 맞게 수정- CSRF 토큰 항목 추가
- JavaScript를 통한 로그인 전송
- 스프링 시큐리티는 기본적으로 로그인 후 처리를
SavedRequestAwareAuthenticationSuccessHandler
라는 클래스를 이용한다. SavedRequestAwareAuthenticationSuccessHandler
를 이용하는 설정은 기존에 XML이나 Java 설정에서authentication-success-handler-ref
속성이나successHandler()
메서드를 삭제하고 관련 스프링 빈의 설정도 사용하지 않도록 한다.
게시물 작성 시 스프링 시큐리티 처리
- 일반적인 경우라면 게시물 리스트의 경우 사용자들의 관심을 끌기 위해서 아무 제약 없이 보여주지만, 게시물 작성 시엔 로그인한 사용자에 한해서 처리되는 경우가 많다.
- 이를 고려해 servlet-context.xml에는 스프링 시큐리티 관련 설정을 추가하고, Controller는 어노테이션을 통해서 제어하도록 한다.
@PreAuthroize
를 이용할 때의 표현식은isAuthenticated()
로 어떠한 사용자든 로그인이 성공한 사용자만이 해당 기능을 사용할 수 있도록 처리한다.
- View와 같이 스프링 시큐리티의 영향을 받는 JSP 파일에는 반드시 시큐리티 관련 태그 라이브러리를 설정하도록 주의한다.
- 스프링 시큐리티를 사용할 때
POST
방식의 전송은 반드시CSRF 토큰
을 사용하도록 추가해야만 한다. <form>
태그 내에CSRF 토큰
의 값을<input type=’hidden’>
으로 추가한다.
- 게시물의 등록에서 주의할 점은 스프링 시큐리티의 적용 이후에 한글이 꺠지는 문제가 발생할 수 있다는 점이다.
- 한글 처리는 web.xml을 이용해서 스프링의
CharacterEncodingFilter
를 이용해서 처리하지만, 시큐리티를 필터로 적용할 때에는 필터의 순서가 인코딩 필터보다 뒤로 적용되게 주의해서 설정해야 한다.
게시물 조회와 로그인 처리
<secu:authentication>
태그를 매번 이용하는 것은 불편하기 때문에 로그인과 관련된 정보인 principal은 아예 JSP 내에서 pinfo라는 이름의 변수로 사용하도록 한다.
<sec:authorize>
는 인증받은 사용자만이 영향을 받기 위해서 지정한다.
게시물의 수정/삭제
- 게시물의 수정과 삭제는 브라우저에서는 로그인한 사용자만이 접근할 수 있지만, 사용자가 URL을 조작해서도 접근이 가능하기 떄문에 화면과 POST 방식으로 처리되는 부분에서 CSRF 토큰과 스프링 시큐리티를 적용한다.
- 게시물의 수정과 삭제는 현재 로그인한 사용자와 게시물의 작성자가 동일한 경우에만 할 수 있다.
Ajax와 스프링 시큐리티 처리
<form>
태그를 이용하는 방식 외에 많이 사용되는 Ajax를 이용하는 경우에는 추가적인 설정이 필요하다.
POST, PUT, PATCH, DELETE
와 같은 방식으로 데이터를 전송하는 경우에는 반드시 추가적으로X-CSRF-TOKEN
과 같은 헤더 정보를 추가해서CSRF 토큰값
을 전달하도록 해야 한다.
- Ajax는 JavaScript를 이용하기 때문에 브라우저에서는 CSRF 토큰과 관련된 값을 변수로 선언하고, 전송 시 포함시켜 주는 방식으로 수정한다.
- 기존 코드에서
csrfHeaderName
과csrfTokenValue
변수를 추가한다.CSRF 토큰
의 값은 세션이 달라질 때마다 변한다. - 첨부파일의 등록과 삭제의 경우에도 POST 방식으로 동작하므로 CSRF 토큰의 처리가 필요하다.
- 게시물의 수정 화면에서도 첨부파일은 추가되거나 삭제가 가능하므로 코드를 수정한다.
- 댓글의 경우 모든 동작이 Ajax를 통해서 이루어지기 때문에 화면과 서버쪽에 수정을 한다.
- 서버쪽에서는 Controller가 댓글에 대한 보안 원칙을 다음과 같이 설계할 수 있다.
- 댓글의 등록: 로그인한 사용자만이 댓글을 추가할 수 있도록 한다.
- 댓글의 수정과 삭제: 기존의 댓글 삭제에는 댓글 번호만으로 처리했는데, 서버 쪽에서 사용할 것이므로 댓글 작성자를 같이 전송하도록 수정한다.
- 브라우저 쪽에서 달라지는 부분은 다음과 같다.
- 댓글의 등록:
CSRF 토큰
을 같이 전송하도록 수정한다. - 댓글의 수정/삭제: 기존의 댓글 삭제에는 댓글 번호만으로 처리했는데, 서버 쪽에서 사용할 것이므로 댓글 작성자를 같이 전송하도록 수정한다.
- CSRF 토큰에 대한 처리는
csrfHeaderName, csrfTokenValue
변수를 선언해서 처리한다.
- jQuery를 이용해서 Ajax로 CSRF 토큰을 전송하는 방식은 첨부파일의 경우 beforeSend를 통해서 처리했지만, 기본 설정으로 지정해서 사용하는 것이 더 편하다.
aJaxSend()
를 이용한 코드는 모든 Ajax 전송 시CSRF 토큰
을 같이 전송하도록 세팅되기 때문에 매번 Ajax 사용 시beforeSend
를 호출해야 하는 번거로움을 줄일 수 있다.
🏁결론
해당 내용을 정리하면서 Interceptor를 이용한 사용자의 권한이나 등급에 기반을 두는 로그인 체크를 연습해볼 수 있었다. 또한 스프링 웹 시큐리티를 통해 로그인 처리와 CSRF 토큰 처리, 암호화 처리, 자동로그인, JSP에서의 로그인 처리와 같은 개념을 이해할 수 있었다.
Share article