스프링 시큐리티에서 POST방식을 이용하는 경우 기본적으로 CSRF 토큰이라는 것을 이용하게 된다. 별도의 설정이 없다면 스프링 시큐리티가 적용된 사이트의 모든 POST 방식에는 CSRF 토큰이 사용되는데 '사이트간 위조 방지' 목적으로 특정한 값의 토큰을 사용하는 방식이다.
우리가 어떤 사이트에 접속을 해서 admin으로 로그인을 했을때, 서버는 Session으로 클라이언트와 1대1로 매핑을 해서 작동한다. 서버는 SessionId만 같으면 IP가 무엇이던 간에 인증받은 사용자로 인식을한다.
이미지가 아닌url을 넣으면 요청은 간다.(x박스로 나오지만(
)
A사이트의 admin이 B사이트에 접속해서 해커가 올린 글을 봄
그럼 A사이트로 공격이 들어감, A사이트의 admin이 변경될수 있고
A사이트의 회원 내용이 바뀐다.
스프링 시큐리티는 기본적으로 GET방식을 제외하고 모든 요청에 CRSF 토큰 사용
<form>
등의 데이터 전송시에 CSRF토큰을 같이 전송하도록 처리한다.
일반적으로 CSRF 토큰은 세션을 통해서 보관하고, 브라우저에서 전송된 CSRF 토큰값을 검사하는 방식으로 처리한다. 스프링 시큐리티에서는 CSRF 토큰 생성을 비활성화하거나 CSRF 토큰을 쿠키를 이용해서 처리하는 등의 설정을 지원한다.
<security:csrf disabled="true"/>
security-context.xml에 다음 내용을 추가한다.
Spring Security의 CSRF (Cross-Site Request Forgery) 보호를 활성화하려면 <security:http>
태그 내부에 <security:csrf/>
태그를 추가하면 됩니다.
다음과 같이 <security:http>
블록 내에 추가할 수 있습니다:
<security:http>
<security:intercept-url pattern="/sample/all" access="permitAll"/> <!-- 모든사람들에게 허용 -->
<security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
<security:intercept-url pattern="/sample/admin" access="hasRole('ROLE_ADMIN')"/>
<security:form-login login-page="/customLogin"/>
<security:access-denied-handler error-page="/accessError"/>
<security:csrf/> <!-- CSRF 보호 활성화 -->
</security:http>
<security:csrf/>
태그를 추가하면 Spring Security는 자동으로 CSRF 토큰을 생성하고, 해당 토큰을 사용하여 클라이언트의 요청이 신뢰할 수 있는지 검증합니다.
로그인 성공후 특정 URI로 이동하거나 쿠키 처리 등의 추가적인 작업
CSRF토큰을 탈취하더라도 생명주기가 짧기 때문에 실제로 이것으로 해킹을 하기 어렵다.
파라미터 확인작업
authentication.getAuthorities();
: 필요한 ROLE 네임들이 컬렉션의 형태로 리턴된다.
템플릿에서 ? 가 들어가는 경우? : GrantedAuthority의 후손은 다 들어갈 수 있다라는 의미이다.
List<String> rolename = new ArrayList<>();
이거 무슨뜻
authentication.getAuthorities().forEach(
authority -> {
roleNames.add(authority.getAuthority());
}
);
admin의 경우는 getAuthority()할 때 Role이 2개 잡히고
member의 경우는 Role이 1개가 잡힌다. 즉, CustomLoginSuccessHandler는 로그인 한 사용자에 부여된 권한 Authentication객체를 이용해서 사용자가 가진 모든 권한을 문자열로 체크한다. 만일 사용자가 'ROLE_ADMIN' 권한을 가졌다면 로그인 후에 바로 '/sample/admin'으로 이동하게 하는 방식이다.
if (roleNames.contains("ROLE_ADMIN")) {
response.sendRedirect("/sample/admin");
return;
}
if (roleNames.contains("ROLE_MEMBER")) {
response.sendRedirect("/sample/member");
return;
}
response.sendRedirect("/sample/all");
가장 높은 우선순위를 가지는 ROLE을 체크를 한다.
예외에서도 try문이 있으면 가장 특수한 경우를 위에서 체크를 하고
가장 일반적인 것을 마지막에 체크를 한다.
그럼 이제 핸들러가 일을 하기 위해서 어떻게 해야할까?
security-context.xml 파일에 작성된 CustomLoginSuccessHandler를 빈으로 등록하고 로그인 성공 후 처리를 담당하는 핸들러로 지정한다.
<security:form-login login-page="/customLogin" authentication-success-handler-ref=""/>
추가한다.
빈객체로 등록한다.
이 빈의 이름을 hanlder의 ref에 그대로 추가한다(customLoginSuccess)
위에 SuccessHandler가 추가된 것을 확인할 수 있다.
스프링 시큐리티에서 기본적으로 로그인 페이지를 제공하기는 하지만, 현실적으로는 화면 디자인 등의 문제로 사용하기 불편하다. 때문에 거의 대부분 경우 별도의 URI를 이용해서 로그인 페이지를 다시 제작해서 사용한다.
에러와 로그아웃 인자를 설정해서 전달할 수 있게 한다.
loginInput()은 GET방식으로 접근하고, 에러 메시지와 로그아웃 메시지를 파라미터로 사용할 수 있다.
veiws 폴더에는 customLogin.jsp를 추가한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 폼</h1>
<h2>${error}</h2>
<h2>${logout}</h2>
<br>
<form method="post" action="/login">
ID:<input type="text" name="username"></input><br>
PW:<input type="password" name="password"></input><br>
<input type="submit" value="로그인"></input>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}">
</form>
</body>
</html>
⚠ customLogin.jsp를 보면 몇 가지 특이한 점이 있다.
우선 <form>
태그의 action 속성값이 '/login'으로 지정되어 있다는 점이다. 실제로 로그인의 처리 작업은 '/login'을 통해서 이루어 지는데 반드시 POST 방식으로 데이터를 전송해야 한다. <input>
태그의 name 속성은 기본적으로 username과 password 속성을 이용한다.
customLogin의 form에서 /login을 POST로 요청을 하면 SPRING SECURITY가 받아서 LoginSuccessHandler로 보낸다. 그 후, LoginSuccessHandler을 통해 Redirect 작업을 한다.
그런데 핸들러 부분의 log가 찍히지 않는다. log4j.xml에서 warn설정으로 되어 있기 때문에 출력되지 않는다.
warn 레벨로 설정하면(log.info → log.warn) 위와 같이 로그가 출력된다.
<security:logout logout-url="/customLogout" invalidate-session="true"/>
을 추가한다. 해당 URI로 요청하면 세션을 유효하지 않게 만든다는 의미이다.
세션 다시 요청하면 서버의 세션 요청을 리프레시된다.(시간이 지나면 세션의 공간이 사라진다.)