출처: 코드로 배우는 스프링 웹 프로젝트
이전에 우리는 세개의 URI를 설계했다.
현재 애플리케이션을 실행하면 모든 uri에 모든 사용자들이
접근할 수 있다. 이제 권한에 따른 접근 제한을 설정해보자.
설정은 security-context.xml 에서 한다.
<security:http>
<!--인터셉터를 통해 접근 제한-->
<!-- /sample/all 에는 모든 접근을 허용(permitAll)-->
<security:intercept-url pattern="/sample/all" access="permitAll"/>
<!-- /sample/member 에는 ROLE_MEMBER 권한을 가진 사용자만 허용-->
<security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
</security:http>
<security:authentication-manager>
</security:authentication-manager>
접근에 제한을 걸었으니 애플리케이션을 실행해 /sample/member 에 접근해보자. 당신은 /login으로 리다이렉트 될 것이다.
엥?
우리는 /login을 만든적이 없는데 생겼다.
이 페이지는 스프링 시큐리티가 기본으로 제공하는 페이지다. 하지만 우리만의 로그인 페이지를 만들어야 하기 때문에 테스트용으로만 사용하자.
로그인 화면이 보이긴 하지만 우리의 서버에는 기존 유저에 대한 정보가 아무것도 없다. 임시로 in-memory 유저를 만들어보자.
인증과 권한에 대한 실제 처리는 UserDetailsService라는 인터페이스를 구현해 처리한다. xml로는 다음과 같이 지정한다.
<security:http>
<security:intercept-url pattern="/sample/all" access="permitAll"/>
<security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
<security:form-login />
</security:http>
<security:authentication-manager>
<security-authentication-provider>
<security:user-service>
<!--"member"라는 이름의 "MEMBER"권한을 갖는 in-memory유저 생성-->
<security:user name="member" password="member" authorities="ROLE_MEMBER"/>
</security:user-service>
<security-authentication-provider>
</security:authentication-manager>
이제 애플리케이션을 실행해 /sample/member로 다시 접근한다음 로그인 해보자. PasswordEncoder가 없다는 에러가 뜬다. PasswordEncoder는 간단히 말해 패스워드를 랜덤한 긴 문자열로 암호화 시켜주는 도구다. 스프링 시큐리티 5부터는 PasswordEncoder를 필수로 이용하도록 돼있기 때문에 에러가 발생하는 것이다. 패스워드 앞에 {noop}을 붙혀 임시로 인코딩 없이 사용하자
<security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/>
다시 서버를 시작하면 성공적으로 로그인 될 것이다.
스프링 시큐리티를 배우다 보면 자주 로그인과 로그아웃을 해야 한다. 조금 뒤에 웹 화면을 이용해 로그아웃 처리를 해 볼 것이니 일단은 임시로 JSESSIONID 라는 쿠키를 강제로 삭제해 로그아웃 처리를 하자. 크롬에서 F12를 누르면 개발자 도구가 나온다. Application 탭을 선택해 좌측 Storage 의 Cookies 를 클릭하면 JSESSIONID가 있을 것이다. 마우스 우클릭 -> delete를 눌러 강제로 삭제하자. JSESSIONID 는 Tomcat에서 발행하는 쿠키의 이름이다. 쿠키를 삭제하고 다시 /sample/member 를 호출하면 다시 /login 으로 이동될 것이다.
전에 MEMBER 권한을 갖는 member 사용자를 만들엇다.
이번에는 MEMBER 과 ADMIN 권한을 모두 갖는 admin이라는 이름의 사용자를 만들어보자. xml을 다음과 같이 작성한다.
<security:http>
<security:intercept-url pattern="/sample/all" access="permitAll"/>
<security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
<!--전에 생성했던 /sample/admin URI에 ADMIN 유저만 접근 할 수 있도록 설정한다.-->
<security:intercept-url pattern="/sample/admin" access="hasRole('ROLE_ADMIN')"/>
<security:form-login />
</security:http>
<security:authentication-manager>
<security-authentication-provider>
<security:user-service>
<security:user name="member" password="member" authorities="ROLE_MEMBER"/>
<!--MEMBER와 ADMIN 권한을 갖는 admin 유저 생성-->
<security:user name="admin" password="admin" authorities="ROLE_MEMBER, ROLE_ADMIN"
</security:user-service>
<security-authentication-provider>
</security:authentication-manager>
애플리케이션을 실행해 admin 사용자로 로그인하면
/sample/admin, /sample/member 에 모두 접근할 수 있을 것이다.
우리가 위에서 만든 member 사용자로 로그인 하여
/sample/admin 을 요청하면 403 에러가 뜰 것이다.
이때 에러페이지를 그대로 보여주기 보다는 사용자가 쉽게 이해할 수 있는 특정한 화면을 보여주는 것이 더 좋다.
접근 제한에 대한 처리는 AccessDeniedHandler를 직접 구현하거나 특정한 URI를 지정할 수 있다.
<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 />
<!--접근이 거부됐을 때 보일 화면을 지정-->
<security:access-denied-handler error-page="/accessError"/>
</security:http>
URI를 지정했으니 매핑할 컨트롤러가 필요하다.
CommonController 클래스를 생성해 매핑해주자.
@Controller
public class CommonController {
@GetMapping("/accessError")
//Authentication 객체는 스프링 시큐리티에서 제공하는 사용자의 정보를 담고있는 객체다 (아이디, 권한 등)
//필요한 경우 사용자의 정보를 화면에 보여주기 위해 파라미터로 받았다.
public void accessDenied(Authentication auth, Model model){
//model에 접근이 제한됐다는 메시지를 담아 보낸다.
model.addAttribute("msg", "Access Denied")
}
}
accessError.jsp 를 생성해 model에 담은 메시지를 보여주면 된다.
위와 같이 에러페이지만을 제공하는 경우 사용자의 URI에는 변화가 없다. /sample/admin 으로 접근을 시도해 거부당했다면 그대로 /sample/admin 에서 accessError.jsp 의 화면이 보여진다
조금 더 복잡한 처리가 필요하다면 AccessDeniedHandler를 구현해야한다.
클래스를 생성하자.
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Overried
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessException){
//단순히 만들었던 에러 페이지로 리다이렉트 한다.
//이제 접근 권한이 없는 URI에 요청을 하면 사용자의 URI도 /accessError로 바뀔 것이다.
response.sendRedirect("/accessError");
}
}
이 클래스의 메서드가 동작하려면 이 클래스가 스프링 빈에 등록돼야한다. security-context.xml 에 빈으로 추가해주자.
<bean id="customAccessDenied" class ="org.zerock.security.CustomAccessDeniedHandler"></bean>
<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 />
<!--<security:access-denied-handler error-page="/accessError"/>-->
<!--위에 등록한 빈을 추가-->
<security:access-denied-handler ref="customAccessDenied"/>
</security:http>
이제 애플리케이션을 실행해 member계정으로 로그인하여 /sample/admin 에 접근해보자. 이전과 달리 화면만 바뀌는 것이 아닌 /accessError 로 리다이렉트된다.
다음에는 시큐리티의 기본 로그인 페이지가 아닌 우리가 직접 로그인 페이지를 만들어보자!