Development/Spring

Spring Security

南山 2017. 7. 20. 22:49

Spring Security

 

스프링 2.x 버전 사용할때 security 살펴보고 10여년 만에 다시 볼려니 세월이 많이 지나서 다 잊어버렸고 실제 프로젝트에서는 spring security 보다는, authentication 은 자체 로직으로 처리하고 authorization 은 admin 에서 설정하는 방법을 많이 사용하다보니 spring security 는 점점 사용하지 않게 되었다.

이번 프로젝트에 spring security 를 적용해보고자 오랜만에 삽질을 하면서 article 을 정리해본다.

 

내가 원하는 기능은

 

1. 비밀번호 암호화해서 비교.

    - Sha256 을 이용한 암호화.

2. admin, user role 별로 페이지 접근 제어.

3. remember me 기능 적용.

    - 쿠키 또는 database 사용.

    - database 를 사용하는 경우는 security database schema 에 맞춰준다.

4. 인가된 페이지 외에 불법적인 url 접근 차단.

 

이정도의 기본적인 기능이며 추가적인 기능은 차차 붙여가기로 했다.

일단 기본적인 개념이 확실히 잡히면 부가적인 기능을 구현하는 것은 어렵지 않을 것으로 판단했다.

 

spring security 에서는 보안을 위해서 몇가지 interface 를 지원해주는데 그중에서 UserDetailService, AuthenticationProvider, AuthenticationManager 등을 이용해서 구현해봤다.

구글링 하면서 올라온 글들을 살펴봤는데 일단은 제대로 설명한 내용이 많지 않기도 하거니와 잘못된 설명과 예제들로 초반에 오히려 더욱 많이 헷갈렸다.

interface 별로 지원해주는 기능들이 조금 다른데 초반에 개념이 제대로 잡히지 않은 상태에서는 기본적인 UserDetailService interface 에서 지원해주는 기능이 별로 없을것으로 판단하고 AuthenticationProvider, AuthenticationManager interface 를 구현해서 테스트를 했는데 이것들도 조금씩의 문제는 있었고 결론적으로는 내가 원하는 기능들은 UserDetailService interface 에서도 충분히 지원해주고 있었다는 사실이다.

만약 추가 기능들 구현중에 부족한 부분이 있으면 다른 interface 구현을 고려해볼 생각이다.

 

개념이 잡히지 않은 상태에서 일단 샘플부터 살펴봤는데 가장 기본적인 UserDetailService interface 를 이용하는 샘플은 너무 간단해서 실제 프로젝트에는 도저히 적용하지 못할 정도였다. 

그러다가 AuthenticationProvider, AuthenticationManager interface 를 구현해봤고 시간이 조금 지나면서 어느정도 원하는 기능들을 구현할 수 있었다.

 

spring security 의 구조를 조금씩 이해하면서 다시 생각해보니 UserDetailService interface 로도 원하는 기능은 구현이 가능할것 같았고, 가장 기본적인 interface 를 마음대로 수정할 수 있다면 좀 더 다기능의 interface 에 접근하기도 쉬울것 같았다.

 

막연하게 spring security 를 생각해보면 막막한 심정인데 사실 그렇게 어렵지는 않다.

몇가지 interface 를 구현해보면서 느낀점은 spring 에서는 여러가지 방법으로 security 를 지원해주며 개발자의 수고를 많이 덜어주고 있다는 것이다.

 
결론부터 정리하자면, spring security 에거 가장 중요한 부분은 결국 context-secutiry.xml 의 필터설정과 인증관련된 interface 의 구현이다.
이부분만 확실하게 이해하고 개념을 잡는다면 spring security 는 원하는 기능들을 어렵지 않게 구현할 수 있고 customizing 이 가능하다.
 
 
 
 
 

개발환경.

JDK 1.8
Spring Framework 4.3.7.RELEASE
Spring Security 3.2.3.RELEASE
Maven3
Windows 10 / MacOS
 
 
 

pom.xml

.... 

<dependency>

            <groupId>org.springframework.security</groupId>

            <artifactId>spring-security-core</artifactId>

            <version>${spring.maven.artifact.security.version}</version>

        </dependency>

 

        <dependency>

            <groupId>org.springframework.security</groupId>

            <artifactId>spring-security-web</artifactId>

            <version>${spring.maven.artifact.security.version}</version>

        </dependency>

 

        <dependency>

            <groupId>org.springframework.security</groupId>

            <artifactId>spring-security-config</artifactId>

            <version>${spring.maven.artifact.security.version}</version>

        </dependency>

 

        <dependency>

            <groupId>org.springframework.security</groupId>

            <artifactId>spring-security-taglibs</artifactId>

            <version>${spring.maven.artifact.security.version}</version>

        </dependency>

....

 

security 관련 dependency 들을 정의한다.

JSP 페이지에서 태그를 사용하기 위해서 맨 밑의 tag 관련 dependency 도 추가한다.

 

 

 

context-security.xml

....

<http auto-config='true' use-expressions="true" access-denied-page="/sb/denied">

 

        <!-- 공통영역은 모두에게 허용한다. -->

        <intercept-url pattern="/index.jsp" access="permitAll" />

        <intercept-url pattern="/favicon.ico" access="permitAll" />

        <intercept-url pattern="/sb/login" access="permitAll" />

        <!-- 나머지는 모두 ROLE_USER 권한을 가진 사용자에게만 허용한다. -->

        <intercept-url pattern="/sb/main" access="hasRole('ROLE_USER')" />

        <intercept-url pattern="/sb/admin" access="hasRole('ROLE_ADMIN')" />

        <!-- 인가된 content 외에는 모두 차단한다. -->

        <intercept-url pattern="/**" access="denyAll()"/>

 

        <!-- id, pw 를 가지고 있는 폼기반의 인증방법을 사용한다. -->

        <form-login

                login-page="/sb/login"

                default-target-url="/sb/main"

                username-parameter="username"

                password-parameter="password"

                authentication-failure-url="/sb/login?error"

                always-use-default-target='true'

        />

 

        <!--

            invalidate-session : 로그아웃하면 세션을 초기화 한다.

            logout-url : 로그아웃을 위한 url 설정.(JSP 페이지의 로그아웃 파라메터와 맞춰준다. controller 를 호출하는것이 아니다.)

            logout-success-url : 로그아웃하면 이동하는 페이지.

        -->

        <logout

                invalidate-session="true"

                logout-url="/sb/logout"

                logout-success-url="/sb/login?logout"

                delete-cookies="JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE"

        />

 

        <!-- enable csrf protection -->

        <csrf/>

 

        <!--

        user-service-ref 을 설정하지 않으면 오류 발생.

        86400 : 1일.

        -->

        <remember-me

                key="myAppKey"

                token-validity-seconds="86400"

                remember-me-parameter="remember-me"

                user-service-ref="userDetailService"

        />

 

    </http>

 

    <!-- 인증처리를 위한 최상위 태그. -->

    <authentication-manager>

        <authentication-provider user-service-ref="userDetailService">

            <!-- 비밀전호를 암호화해서 비교한다. -->

            <password-encoder ref="passwordEncoder" />

        </authentication-provider>

    </authentication-manager>

 

    <beans:bean id="userDetailService" class="kr.co.xxxx.securitybasic.common.UserDetailsServiceImpl"/>

 

    <beans:bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">

        <beans:constructor-arg name="strength" value="256"></beans:constructor-arg>

    </beans:bean>

....

 

intercept-url 은 허용하는 경로부터 먼저 정의를 하고 맨나중에는 그 외 모든 경로를 막아준다.

명시적으로 허용하는 경로만 접근 가능하다는 의미이다.

그외는 모두 deny 페이지로 강제 이동시킨다.

 

정상적으로 로그아웃하면 세션을 삭제하고 로그인 화면으로 이동시킨다.

 

remember-me 기능은 cookie 를 1일동안 저장한다.

 

여기서는 UserDetailService interface 를 구현했다.

구현체에서 비밀번호를 암호화해서 비교하기 위해서 ShaPasswordEncoder 를 주입한다.

 

 

 

UserDetailServiceImp.java

....

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

 

        logger.debug("> loadUserByUsername", TAG);

        logger.debug("> username : {}", username, TAG);

 

        UserDto param = new UserDto();

        param.setUsername(username);

 

        UserDto result = securityBasicService.selectUserById(param);

 

        if (result == null) {

            logger.error("> No user found with username {}", result.getUsername(), TAG);

            throw new UsernameNotFoundException("No user found with username " + result.getUsername());

        } else {

            logger.debug("> user exist : {}", result.getUsername(), TAG);

        }

 

        UserDetails user = new User(username, result.getPassword(), getAuthorities(result.getRole()));

 

        logger.debug("> user username : {}", user.getUsername(), TAG);

        logger.debug("> user password : {}", user.getPassword(), TAG);

 

        return user;

    }

 

    /**

     * authorization

     * 사용자의 권한을 확인한다.

     *

     * @param role

     * @return

     */

    public Collection<SimpleGrantedAuthority> getAuthorities(String role) {

 

        logger.debug("> getAuthorities", TAG);

        logger.debug("> role : {}", role, TAG);

 

        // Create a list of grants for this user

        List<SimpleGrantedAuthority> authList = new ArrayList<SimpleGrantedAuthority>(2);

 

        logger.debug("Grant ROLE_USER to this user", TAG);

        authList.add(new SimpleGrantedAuthority("ROLE_USER"));

 

        if ( role.equals("ROLE_ADMIN")) {

            // User has admin access

            logger.debug("Grant ROLE_ADMIN to this user", TAG);

            authList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

        }

 

        return authList;

    }

....

 

UserDetailService interface 구현체이다.

사용자의 권한은 여러개를 추가할 수 있다.

ROLE_USER 를 기본적으로 가지고 특정 조건에서 ROLE_ADMIN 을 추가한다.

 

loadUserByUsername 는 implement 에서 구현해야 하는 메소드이며 이곳에서 사용자 정보를 넘겨주면 spring security 에서는 폼으로 입력된 id, password 와 비교해서 authenticatoin 처리를 한다.

개발자가 직접 비교하는 로직을 구현할 필요가 없으며 당연히 처리되는 내부 과정을 logger 로 찍어볼수도 없다.

 

정리하자면, 

사용자가 폼에서 입력한 id, password 는 loadUserByUsername 에서 저장된 id 가 있는지 database 에서 가져오는데 해당 id 의 모든 정보도 같이 가져온다.

 

1. 동일한 id 가 존재하는지 확인한다.

2. 없으면 null 처리하고, 있으면 해당 id 의 모든 정보도 같이 가져온다.

 

일단 가져온 모든 정보중에서 password 가 폼으로 입력된 password 와 동일한지 비교하는 것은 spring security 에서 자동으로 처리된다.

 

 

context-security.xml 에서 설정한 비밀번호 암호화 비교는 spring security 에서 자동으로 처리된다.

1. 비밀번호 암호화 설정을 하지 않은 경우 : 평문으로 비교.

2. 비밀번호 암호화 설정을 하는 경우 : 폼으로 입력된 비밀번호를 해당 로직(여기서는 SHA256)을 사용해서 암호화 한 뒤 비교.

database 에 비밀번호는 당연히 같은 방법(SHA256)으로 암호화한 string 포멧으로 저장되어 있어야 한다.

 

 

spring security 에서 자동으로 처리해 주는 부분이 많다.

과한 자동화는 customizing 에 문제가 될 수 있지만 여기서는 적절한 수준의 core 부분만 자동화가 되어있다.

만약 비밀번호 암호화 및 비교 로직까지 직접 구현해서 적용하고 싶으면 AuthenticationManager interface 를 구현하면 된다.

각 방법은 모두 장단점이 있다.