2017년 3월 16일

Spring 기반 웹 프로그램 개발 Part 2 - SpringSecurity 사용하기

SpringSecurity 기술을 사용하면
  •  인증, 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(), truetruetrue, AuthorityUtils.NO_AUTHORITIES);
        this.communityUser = communityUser;
    }
 
    public CommuintyUserDetails(architecture.community.user.User communityUser, List<GrantedAuthority> authorities) {
        super(communityUser.getUsername(), communityUser.getPassword(), communityUser.isEnabled(), truetruetrue, 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

댓글 없음:

댓글 쓰기