Spring Security 2015 Web Service Computing
Spring Security 앞서 우리는 /delete라는 URL을 통해 유저를 삭제하는 기능을 구현하였다. 만약 이 주소를 알고 있는 누군가가 이것을 악용하여 모든 유저를 삭제한다면? 이러한 특정 기능들은 보안을 강화하여 특정 권한에 의해 관리가 되어야 한다.
Spring Security 스프링 프레임워크를 통해 각종 로그인처리, 권한 처리등을 수행 기본적으로 자원에 대한 접근을 가로채(intercept) 권한을 체크 간단한 설정으로 보안 수준을 유지 가능 이번 장에서는 로그인 처리, 권한에 따른 URL접근 처리 방법을 알아본다.
Spring Security Spring Security의 인증 Spring Security의 권한 부여 Credential based Authentication : 권한을 부여받는데 사용자명과 비밀번호를 입력받아 입력한 비밀번호가 저장된 비밀번호와 일치하는지 확인. 일반적으로 스프링 시큐리티에서는 아이디를 Principal, 비밀번호를 Credential이라고 부른다. Spring Security의 권한 부여 Granted Authority: 로그인 등을 통해 적절한 인증이 성공했다면 미리 설정한 권한이 부여된다. Intercept: DispatcherServlet보다 먼저 요청을 가로채서 권한에 따른 Resource 접근 필터링을 한다.
Spring Security 실습 내용 1) 사이트에 일반적인 유저(ROLE_USER)와 관리자(ROLE_ADMIN) 권한이 있다고 가정하자. 2) 회원가입시 PasswordEncoder를 통해 비밀번호를 암호화하여 데이터베이스에 저장 한다. 또한 1)에서 설명한 권한을 부여한다. 3) 가입 후 관리자만 접근 가능한 페이지, 일반 유저만 접근 가능한 페이지, 로그인이 없이도 접근 가능한 페이지를 구성해본다.
실습을 위한 구성 Lecture-07_src.zip을 압축해제하고 다음의 파일 위주로 살펴보자. /src/main/java/koreatech.cse.domain.User.java /src/main/java/koreatech.cse.domain.Authority.java /src/main/java/koreatech.cse.repository.AuthorityMapper.java /src/main/java/koreatech.cse.service.UserService.java /src/main/java/koreatech.cse.controller.UserController.java /src/main/resources/common/security.xml /src/main/webapp/WEB-INF/web.xml
실습을 위한 구성 web.xml <!--SpringSecurityFilterChain--> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <url-pattern>/*</url-pattern> </filter-mapping> <!--dispatcher-servlet.xml--> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <url-pattern>/</url-pattern> </servlet-mapping>
실습을 위한 구성 security.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:sec="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-3.2.xsd"> <sec:global-method-security secured-annotations="enabled" pre-post-annotations="enabled" /> <sec:http use-expressions="true"> <sec:form-login login-page="/user/signin" default-target-url="/user/signinSuccess" authentication-failure-url="/user/signinFailed"/> <sec:logout logout-url="/user/signout" logout-success-url="/user/signin" /> <sec:intercept-url pattern="/user/onlyUserByXml" access="hasRole('ROLE_USER')" /> <sec:intercept-url pattern="/user/onlyAdminByXml" access="hasRole('ROLE_ADMIN')" /> <sec:intercept-url pattern="/**" access="permitAll" /> </sec:http> <bean id="passwordEncoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder" /> <sec:authentication-manager> <sec:authentication-provider user-service-ref="userService"> <sec:password-encoder ref="passwordEncoder" /> </sec:authentication-provider> </sec:authentication-manager> </beans>
security.xml 주요설정 분석 로그인 주소, 로그인 성공했을 때 이동할 주소, 실패했을 때 이동할 주소에 대한 설정 로그아웃 주소, 로그아웃 성공했을 때 이동할 주소에 대한 설정 암호화된 패스워드를 읽을 수 있는 인코더(md5, sha256 등)설정 <sec:form-login login-page="/user/signin" default-target-url="/user/signinSuccess" authentication-failure-url="/user/signinFailed"/> <sec:logout logout-url="/user/signout" logout-success-url="/user/signin" /> <bean id="passwordEncoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder" /> <sec:authentication-manager> <sec:authentication-provider user-service-ref="userService"> <sec:password-encoder ref="passwordEncoder" /> </sec:authentication-provider> </sec:authentication-manager>
1) 회원 가입시 권한 부여, 비밀번호 암호화 koreatech.cse.service.UserService.java public Boolean signup(User user) { if(user.getEmail() == null || user.getPassword() == null) return false; user.setPassword(passwordEncoder.encode(user.getPassword())); // 사용자의 실제 비밀번호를 암호화함 userMapper.insert(user); Authority authority = new Authority(); authority.setUserId(user.getId()); authority.setRole("ROLE_USER"); authorityMapper.insert(authority); if(user.getEmail().contains ("admin")) { Authority adminAuthority = new Authority(); adminAuthority.setUserId(user.getId()); adminAuthority.setRole("ROLE_ADMIN"); authorityMapper.insert(adminAuthority); } System.out.println("user signup :" + new Date()); return true;
2) 로그인 폼 구성 /WEB-INF/views/signin.jsp <form action="j_spring_security_check" method="post"> <input type="text" placeholder="email" name="j_username"/> <input type="password" placeholder="password" name="j_password"/> <input type="submit" value="Signin"/> </form> 어떠한 form이든 유저네임은 j_username, 비밀번호는 j_password, 폼 액션대상은 j_spring_security로 지정하면 스프링 시큐리티를 바탕으로 로그인을 시도한다. 로그인은 앞서 설정에 지정해뒀던 userService에서 데이터베이스에서 유저 정보를 불러와 시도
3) 데이터베이스를 바탕으로 로그인 koreatech.cse.service.UserService.java public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.findByEmail(username); if (user == null) { throw new UsernameNotFoundException("Invalid username/password."); } List<Authority> authorities = authorityMapper.findByUserId(user.getId()); user.setAuthorities(authorities); System.out.println("user = " + user); return user; UserDetailsService interface를 implement한 후 loadByUsername 함수를 구현해야 한다. 이 함수를 통해 데이터베이스에서 유저 정보 및 권한 정보를 획득한 후 돌려준다.
4) UserDetails 한편, User 도메인 또한 UserDetails interface를 implement해야 한다. 이 때, 위와 같은 메소드의 구현이 필요하다. (User.java 참고)
5) 로그인 성공/실패 koreatech.cse.controller.UserController.java @RequestMapping(value="/signinSuccess") @ResponseBody public String signinSuccess() { System.out.println("signin Success"); return "signinSuccess"; } @RequestMapping(value="/signinFailed") public String signinFailed() { System.out.println("signin Failed"); return "signinFailed"; 앞서 security.xml에서 설정한대로 Controller에서 로그인 성공/실패를 했을 때 이동할 주소에 대한 매핑을 해준다.
로그인의 기본 http request에 저장한 Model변수는 하나의 요청흐름에 대해서만 유효하다. 변수가 지속적으로 존재하지 않으므로 로그인처리 등에 활용하기 힘들다. / /a /b id: hong pw: 1234 id: hong pw: 1234 id: ? pw: ? GET/POST
로그인의 기본 http session이란 서버가 자신에게 접속한 클라이언트를 식별하는 방법이다. 접근한 클라이언트 각각에게 session-id라는 식별자를 부여한다. 이 식별자는 서버와 클라이언트의 메모리에 저장 한 명의 브라우저 사용자에 대해 지속적으로 관리해야 하는 데이터 저장 장소로서 세션을 활용할 수 있다. session 생성 시기 session 소멸 시기 임의의 웹 브라우저부터의 첫 번째 요청을 처리할 때 session이 생성되고 관련 타이머가 동작한다 1) 브라우저 종료 2) 세션 타이머가 만료 3) 코드상에서 명시적으로 세션 소멸(예, 로그아웃코드)
로그인의 기본 session 의 활용 session에 id:hong, pw:1234 변수를 저장할 경우 브라우저를 닫기 전까지 모든 url에서 사용 가능 / /a /b id: hong pw: 1234 id: hong pw: 1234 id: hong pw: 1234
Spring Security에서의 로그인 유저 기존에는 session을 통한 로그인처리, 유저 관리를 직접 해줘야했다. Spring Security에서 session에 저장된 로그인 유저 객체를 활용하는 법은 다음과 같다. SecurityContextHolder로부터 User Principal을 가져온다. jsp에서의 이용한 활용 <c:set var="user" value="${SPRING_SECURITY_CONTEXT.authentication.principal}"/> java에서의 활용 public static User current() { try { return (User) SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); } catch (Exception e) { return null; }
표현식 기반 권한 설정 <sec:http use-expressions="true"> 설정으로 인해 Expression Language based 설정이 가능하다. 이 때, 이러한 설정을 xml 설정파일과 java 파일 둘다 가능하다.
Java에서의 권한 설정 @PreAuthorize("hasRole('ROLE_USER')") @RequestMapping(value="/onlyUserByJava") @ResponseBody public String onlyUserByJava() { System.out.println("User.current() = " + User.current()); return "user"; } @PreAuthorize("hasRole('ROLE_ADMIN')") @RequestMapping(value="/onlyAdminByJava") public String onlyAdminByJava() { return "admin"; @PreAuthorize 어노테이션을 이용해 앞서의 EL based 표현식으로 URL에 대해 권한 설정이 가능하다. 메소드별로, 혹은 하나의 Controller 전체에 대한 권한 설정이 가능하다.
Xml에서의 권한 설정 intercept-url 설정을 통해 주소에 대한 접근을 제어할 수 있다. <sec:http use-expressions="true"> <sec:form-login login-page="/user/signin" default-target-url="/user/signinSuccess" authentication-failure-url="/user/signinFailed"/> <sec:logout logout-url="/user/signout" logout-success-url="/user/signin" /> <sec:intercept-url pattern="/user/onlyUserByXml" access="hasRole('ROLE_USER')" /> <sec:intercept-url pattern="/user/onlyAdminByXml" access="hasRole('ROLE_ADMIN')" /> <sec:intercept-url pattern="/**" access="permitAll" /> </sec:http> intercept-url 설정을 통해 주소에 대한 접근을 제어할 수 있다. 위에서부터 순서대로 접근을 제어할 주소에 대한 권한 설정을 부여하고, 그 외에는 모두 허용하는 방식이다. 위 예제는 /user/onlyUserByXml과 /user/onlyAdminByXml 주소 각각에 대해 접근 가능한 권한을 부여하고, 그 외의 모든 주소에 대해서는 인증 없이 접근 가능하도록 설정한 예이다.
Jsp에서의 권한 설정 Jsp에서도 동일한 표현식을 이용해서 권한에 따른 view 처리를 할 수 있다. <sec:authorize access="hasRole('ROLE_USER')"> 이 문장은 ROLE_USER 권한을 가진 사람에게만 보입니다.<br/> </sec:authorize> <sec:authorize access="hasRole('ROLE_ADMIN')"> 이 문장은 ROLE_ADMIN 권한을 가진 사람에게만 보입니다.<br/> <sec:authorize access="hasAnyRole('ROLE_USER', 'ROLE_ADMIN')"> 이 문장은 ROLE_USER 혹은 ROLE_ADMIN 권한을 가진 사람에게만 보입니다.<br/> Jsp에서도 동일한 표현식을 이용해서 권한에 따른 view 처리를 할 수 있다. 예) 관리자만 볼 수 있는 특별 문구 혹은 메뉴 지정 가능