- 인증, ROLE 기반 접근제어,
- 여러 알고리즘을 지원하는 비밀 번호 암호화 및 확인
- 어노테이션을 이용한 함수 수준의 접근 제어
개인적으로 SpringSecurity 을 사용하는 이유는
- 첫번째 OWASP TOP 10 취약점 중 최소한 A2, A4, A6, A7, A8 에 해당하는 보안 이슈 해결에 도움을 준다는 점이다.
- 두번째 레어어화 된 보안 아키텍처를 응용프로그램에 도입하므로써 개발 단계 부터 안전한 응용프로그램 구현이 가능해진다는 점이다. 왜 우리가 Struts2, Spring MVC 와 같은 MVC 프레임워크를 사용하는 가를 생각해보면 보안 프레임워크의 도입의 이유도 설명이 될 것 같다.
환경 설정
개인적으로 Maven 기반의 개발환경을 선호하기 때문에 여기에서는 아래의 Maven Dependency 을 프로젝트에 추가한다.
org.springframework.security spring-security-core ${project.dependency.spring-security.version} compile org.springframework.security spring-security-web ${project.dependency.spring-security.version} compile org.springframework.security spring-security-config ${project.dependency.spring-security.version} org.springframework.security spring-security-test ${project.dependency.spring-security.version} test
SpringSecurity 사용하기
SpringSecurity 기반의 인증 및 접근 제어를 위하여 다음 인터페이스들을 구현한다.
- AuthenticationProvider : 사용자 인증을 수행하는 인터페이스 클래스로 자신만의 비밀번호 기반의 인증을 위해서는 커스텀 클래스 구현이 필요하다.
- AuthenticationSuccessHandler : 인증에 성공한 경우 다음 행위를 제어하기 위한 인터페이스 클래스. API형식의 인증 서비스 구현을 위해서는 커스텀 구현 클래스가 필요하다.
- UserDetailsService : 사용자 정보를 로드하기 위한 인터페이스 클래스로 자신의 사용자 정보를 사용하여 인증 및 권한을 관리하고자 하는 경우는 커스텀 구현 클래스가 필요하다.
SpringSecurity 설정
환경에 따라 다른 설정을 통하여 웹 프로그램을 제어하는 방법을 선호하기 때문에 XML 방식을 선호한다. 어노테이션은 부분적으로만 사용한다.
SpringSecurity 커스터 구현과 설정
AuthenticationProvider & AuthenticationSuccessHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package architecture.community.spring.security.authentication; import javax.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import com.google.common.eventbus.EventBus; import architecture.community.i18n.CommunityLogLocalizer; import architecture.community.spring.security.userdetails.CommuintyUserDetails; import architecture.community.user.UserManager; import architecture.community.user.event.UserActivityEvent; public class CommunityAuthenticationProvider extends DaoAuthenticationProvider { private Logger logger = LoggerFactory.getLogger(getClass()); @Inject @Qualifier("userManager") private UserManager userManager; @Autowired(required = false) @Qualifier("eventBus") private EventBus eventBus; @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) throw new BadCredentialsException(CommunityLogLocalizer.getMessage("010101")); super.additionalAuthenticationChecks(userDetails, authentication); try { CommuintyUserDetails user = (CommuintyUserDetails) userDetails; if(eventBus!=null){ eventBus.post(new UserActivityEvent(this, user.getUser(), UserActivityEvent.ACTIVITY.SIGNIN )); } } catch (Exception e) { logger.error(CommunityLogLocalizer.getMessage("010102"), e); throw new BadCredentialsException( messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | package architecture.community.spring.security.authentication; import java.io.IOException; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.ui.ModelMap; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import architecture.community.web.model.json.Result; import architecture.community.web.util.ServletUtils; import architecture.ee.util.StringUtils; public class CommunityAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { if (ServletUtils.isAcceptJson(request)) { Result result = Result.newResult(); result.getData().put("success", true); result.getData().put("returnUrl", ServletUtils.getReturnUrl(request, response)); String referer = request.getHeader("Referer"); if (StringUtils.isNullOrEmpty(referer)) result.getData().put("referer", referer); Map<String, Object> model = new ModelMap(); model.put("item", result); MappingJackson2JsonView view = new MappingJackson2JsonView(); view.setExtractValueFromSingleKeyModel(true); view.setModelKey("item"); try { createJsonView().render(model, request, response); } catch (Exception e) {} return; } super.onAuthenticationSuccess(request, response, authentication); } protected View createJsonView(){ MappingJackson2JsonView view = new MappingJackson2JsonView(); view.setExtractValueFromSingleKeyModel(true); view.setModelKey("item"); return view; } } | cs |
UserDetailsService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | package architecture.community.spring.security.userdetails; import java.util.Collections; import java.util.List; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import com.fasterxml.jackson.annotation.JsonIgnore; /** * * @author donghyuck * */ public class CommuintyUserDetails extends User { @JsonIgnore private final architecture.community.user.User communityUser; public CommuintyUserDetails(architecture.community.user.User communityUser) { super(communityUser.getUsername(), communityUser.getPassword(), communityUser.isEnabled(), true, true, true, AuthorityUtils.NO_AUTHORITIES); this.communityUser = communityUser; } public CommuintyUserDetails(architecture.community.user.User communityUser, List<GrantedAuthority> authorities) { super(communityUser.getUsername(), communityUser.getPassword(), communityUser.isEnabled(), true, true, true, authorities); this.communityUser = communityUser; } public boolean isAnonymous() { return communityUser.isAnonymous(); } public architecture.community.user.User getUser() { return communityUser; } public long getUserId() { return communityUser.getUserId(); } public long getCreationDate() { return communityUser.getCreationDate() != null ? communityUser.getCreationDate().getTime() : -1L; } } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | package architecture.community.spring.security.userdetails; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import architecture.community.user.Role; import architecture.community.user.RoleManager; import architecture.community.user.User; import architecture.community.user.UserManager; import architecture.community.user.UserNotFoundException; import architecture.community.util.CommunityConstants; import architecture.ee.service.ConfigService; import architecture.ee.util.StringUtils; public class CommunityUserDetailsService implements UserDetailsService { private Logger logger = LoggerFactory.getLogger(getClass()); @Inject @Qualifier("userManager") private UserManager userManager; @Inject @Qualifier("roleManager") private RoleManager roleManager; @Inject @Qualifier("configService") private ConfigService configService; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 사용자 환경에 적절하게 구현.. try { User user = userManager.getUser(username); CommuintyUserDetails details = new CommuintyUserDetails(user, getFinalUserAuthority(user)); return details ; } catch (UserNotFoundException e) { throw new UsernameNotFoundException("User not found.", e); } } protected List<GrantedAuthority> getFinalUserAuthority(User user) { // 사용자 환경에 적절하게 구현.. String authority = configService.getLocalProperty(CommunityConstants.SECURITY_AUTHENTICATION_AUTHORITY_PROP_NAME); List<String> roles = new ArrayList<String>(); if(! StringUtils.isNullOrEmpty( authority )) { authority = authority.trim(); if (!roles.contains(authority)) { roles.add(authority); } } for(Role role : roleManager.getFinalUserRoles(user.getUserId())){ roles.add(role.getName()); } return AuthorityUtils.createAuthorityList(StringUtils.toStringArray(roles)); } } | cs |
Context.xml(스프링 설정 파일)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | <?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:lang="http://www.springframework.org/schema/lang" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <beans:description><![CDATA[ Spring Security 설정 ]]></beans:description> <global-method-security secured-annotations="enabled" pre-post-annotations="enabled" /> <http auto-config="true" use-expressions="true" disable-url-rewriting="true"> <intercept-url pattern="/*" access="permitAll"/> <intercept-url pattern="/data/*" access="permitAll"/> <intercept-url pattern="/secure/data/**" access="hasRole('ROLE_USER')" /> <!-- 로그인 페이지 지정 --> <form-login login-page="/accounts/login" username-parameter="username" password-parameter="password" login-processing-url="/accounts/auth/login_check" authentication-success-handler-ref="authenticationSuccessHandler" authentication-failure-url="/error/401" /> <!-- 로그아웃 설정 --> <logout invalidate-session="true" logout-url="/accounts/logout" logout-success-url="/" delete-cookies="JSESSIONID" /> <!-- CSRF ATTACK --> <csrf disabled="true" /> <anonymous enabled="true" username="ANONYMOUS" /> <!-- 중복 로그인 방지 설정 --> <session-management session-fixation-protection="newSession" > <concurrency-control max-sessions="1" expired-url="/error/login_duplicate"/> </session-management> <!-- 접근 불허시 보여줄 페이지 설정 --> <access-denied-handler error-page="/error/unauthorized" /> </http> <authentication-manager id="authenticationManager"> <authentication-provider ref="authenticationProvider"/> </authentication-manager> <beans:bean id="authenticationProvider" class="architecture.community.spring.security.authentication.CommunityAuthenticationProvider" p:passwordEncoder-ref="passwordEncoder" p:userDetailsService-ref="userDetailsService" /> <beans:bean id="authenticationSuccessHandler" class="architecture.community.spring.security.authentication.CommunityAuthenticationSuccessHandler"/> <beans:bean id="userDetailsService" class="architecture.community.spring.security.userdetails.CommunityUserDetailsService" /> <beans:bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></beans:bean> </beans:beans> | cs |
소스 자료
Community 1.0.0-BETA
참고 자료
OWASP top ten attacks and Spring Security
Spring Security Tutorial
Securing a Web Application
댓글 없음:
댓글 쓰기