인증과 권한

Spring-Security를 이해하기 위해서 보안의 두가지 영역인 인증과 권한을 반드시 알아야 한다.

  • 인증은 작업을 수행할 수 있는 주체가 맞는지 확인하는 과정을 말한다

  • 권한은 인증된 주체가 해당 동작을 수행하도록 허락되어있는것을 말한다.

<br/>

<br/>

Servlet Filter

<img src="https://file.podo-dev.com/blogs/images/2019/07/10/origin/ID0GTR181224235520.PNG" style="width:400px">

<출처 : http://javacan.tistory.com/entry/58>

Servlet Fliter 들이 요청을 감싸 처리한다. Spring-Security도 Servlet Filter를 사용하여 동작한다.

<br/>

DelegatingFilterProxy

모든 어플리케이션의 요청을 감싸게해서, 모든 요청에 보안이 적용되도록하는 Servlet Filter 이다.

스프링프레임워크에서 제공되며 web.xml에 설정해준다.

구현 부분을 참고하자.

<br/>

<br/>

Spring Security 구현

특정 페이지에 권한을 가진 계정만 접근 가능하도록 설정하고자한다.

<br/>

web.xml에 다음을 추가하자

    <!-- Spring Security -->
	<filter>
		<filter-name>springSecurityFilterChain</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	</filter>

	<filter-mapping>
		<filter-name>springSecurityFilterChain</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

<br/>

Pom.xml에 다음 라이브러리를 추가하자

	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-config</artifactId>
		<version>${org.springframework-version}</version>
	</dependency>
		
	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-core</artifactId>
		<version>${org.springframework-version}</version>
	</dependency>
		
	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-web</artifactId>
		<version>${org.springframework-version}</version>
	</dependency>
<br/>

<br/>

다음 소스는 보안 관련 스프링 설정 파일이다. 추후 기억을 위해 주석을 기입하였다.

<?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:security="http://www.springframework.org/schema/security"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd
		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-4.2.xsd">

	<security:http pattern="/resources/**" security="none"></security:http> <!-- 리소스 파일은 Security(X) -->
	<security:http use-expressions="true" auto-config="true">
		
		<!-- auto-config='true' 를 설정한것만으로 기본 로그인페이지/ HTTP 기본인증 / 로그아웃기능등을 제공 -->
		<!-- use-expressions 사용 여부 (hasRole(),hasAnyRole() 등등) -->
		<!-- access ='permitAll, denyAll, hasRole(), hasAnyRole, hasIpAddress(ip)-->
		<!-- ex. access="hasIpAddress('162.79.8.30')" 또는  access="hasIpAddress('162.0.0.0/224')"-->
		
		<security:intercept-url pattern="/admin/*/*" access="hasRole('ROLE_ADMIN')" />  
		<security:intercept-url pattern="/admin/*" access="hasRole('ROLE_ADMIN')" />
		
		<security:form-login
			username-parameter="username"
			password-parameter="password" 
		 	login-page="/login"
			login-processing-url="/j_spring_security_check"
			authentication-failure-url="/login?result=false" />
			
		<!-- 로그아웃 성공시 핸들러 -->
		<!-- <bean id="LoginSuccessHandler" class="com.cglee079.portfolio.security.LoginSuccessHandler" />  -->
		<!-- authentication-success-handler-ref="LoginSuccessHandler"-->
			
		<!-- login-page: 로그인 페이지 지정 -->
		<!-- login-processing-url: 로그인시 진행될 링크 (default) 로그인 페이지의 form action과 일치 시켜야 함 -->
		<!-- username-parameter : 로그인 페이지,  id의 input name값 -->
		<!-- password-parameter : 로그인 페이지,  password의 input name값 -->
		<!-- authentication-failure-url : 로그인 실패 시, 이동할 페이지 -->
		<!-- default-target-url : 로그인 성공시 이동할 페이지 -->
		<!-- always-use-default-target : 항상 로그인 성공 시 default-target-url로 이동하는지 설정 (false: 기존에 이동하려던 페이지로) -->
		<!-- authentication-success-handler-ref : 로그인 성공시 핸들러 (bean) -->
		
		<security:logout invalidate-session="true"
			logout-success-url="/" logout-url="/j_spring_security_logout" />
		<!-- invalidate-session : 로그아웃시 세션 초기화 설정 -->
		<!-- logout-success-url : 로그아웃시 이동할 페이지 -->
		<!-- logout-url : j_spring_security_logout(default) -->
		
		<security:csrf disabled="true" />
		<!--<security:access-denied-handler error-page="/main/denied" />  -->
		<!-- error-page : 접근권한이 없는 페이지 이동시 , 이동할 페이지 -->

	</security:http>
	<security:authentication-manager> <!-- 로그인 관리자 -->
		<security:authentication-provider user-service-ref="adminService">
		<security:password-encoder ref="passwordEncoder"></security:password-encoder>
		</security:authentication-provider>
	</security:authentication-manager>
	
	<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
  		<constructor-arg name="strength" value="256"></constructor-arg>
	</bean>
</beans>
<br/>

<br/>

일단 리소스 파일은 보안에서 제외시킨다. 예를 들어, 로고파일이 보안땜에 접근 할 수 없으면 안되기 때문이다.

<security:http pattern="/resources/**" security="none"></security:http> <!-- 리소스 파일은 Security(X) -->

<br/>

user-expression을 true로 주면 스프링 표현식 언어를 사용하는것이다. Ex. hasRole(), hasAnyRole()

auto-config='true' 를 설정한것만으로 기본 로그인페이지/ HTTP 기본인증 / 로그아웃기능등을 제공한다.

<security:http use-expressions="true" auto-config="true">

<br/>

다음은 특정 페이지에 대하여 접근 권한을 설정하였다.

/admin/* , /admin// 페이지는 ROLE_ADMIN을 권한을 가진 유저만 접근 할 수 있다.

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

<br/>

access에 들어가는 함수는 다음과 같다.

  • hasRole() : 특정 권한 허용

  • hasAnyRole() : 특정 권한들 허용 EX) hasAnyRole('ROLE_USER','ROLE_ADMIN')

  • denyAll : 모두 거부

  • permitAll : 모두 허용

  • hasIpAddress() : 특정 IP만 허용 EX) hasIpAddress('192.168.1.0/24')

<br/>

<br/>

로그인을 하지 않는 유저가 해당 페이지로 이동하면, 로그인 페이지로 이동한다.

로그인 관련 설정은 다음과 같다. *** 주석 참고 ***

	<security:form-login
			username-parameter="username"
			password-parameter="password" 
		 	login-page="/login"
			login-processing-url="/j_spring_security_check"
			authentication-failure-url="/login?result=false" />
			
	<!-- 로그아웃 성공시 핸들러 -->
	<!-- <bean id="LoginSuccessHandler" class="com.cglee079.portfolio.security.LoginSuccessHandler" />  -->
	<!-- authentication-success-handler-ref="LoginSuccessHandler"-->
			
	<!-- login-page: 로그인 페이지 지정 -->
	<!-- login-processing-url: 로그인시 진행될 링크 (default) 로그인 페이지의 form action과 일치 시켜야 함 -->
	<!-- username-parameter : 로그인 페이지,  id의 input name값 -->
	<!-- password-parameter : 로그인 페이지,  password의 input name값 -->
	<!-- authentication-failure-url : 로그인 실패 시, 이동할 페이지 -->
	<!-- default-target-url : 로그인 성공시 이동할 페이지 -->
	<!-- always-use-default-target : 항상 로그인 성공 시 default-target-url로 이동하는지 설정 (false: 기존에 이동하려던 페이지로) -->
	<!-- authentication-success-handler-ref : 로그인 성공시 핸들러 (bean) -->

<br/>

접근 권한이 없는 유저가 해당 페이지로 이동하면, 다음의 설정한 페이지로 이동한다.

<!--<security:access-denied-handler error-page="/main/denied" />  -->

<br/>

<br/>

<em>그렇다면 계정 정보는 어디서 가져올까?</em>

<br/>

XML상에서 계정 하나 하나 하드코딩 하는 방법도 있지만, DB와 연동하는것이 합리적일 것이다.

다음과 같이 테이블을 만들어보자.

<br/>

사전설정1. 계정 테이블 작성

CREATE TABLE `TB_SAMPLE_ADMINS` (
	`ADMN_USERNAME` VARCHAR(200) NOT NULL DEFAULT '',
	`ADMN_PASSWORD` VARCHAR(500) NULL DEFAULT NULL,
	PRIMARY KEY (`ADMN_USERNAME`)
)

<br/>

사전설정2. 계정별 권한 테이블 작성

CREATE TABLE `TB_SAMPLE_ADMNAUTHS` (
	`ADMN_USERNAME` VARCHAR(200) NOT NULL DEFAULT '',
	`ADMN_ROLE` VARCHAR(200) NOT NULL DEFAULT '',
	PRIMARY KEY (`ADMN_USERNAME`, `ADMN_ROLE`),
	CONSTRAINT `fk_ptfolio_admnauths` FOREIGN KEY (`ADMN_USERNAME`) REFERENCES `TB_PTFOLIO_ADMINS` (`ADMN_USERNAME`) ON UPDATE CASCADE ON DELETE CASCADE
)

<br/>

<br/>

다음은 계정 관련 security 설정이다.

user-service-ref를 adminService 빈으로 설정한다.

	<security:authentication-manager> <!-- 로그인 관리자 -->
		<security:authentication-provider user-service-ref="adminService">
		<security:password-encoder ref="passwordEncoder"></security:password-encoder>
		</security:authentication-provider>
	</security:authentication-manager>
	
	<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
  		<constructor-arg name="strength" value="256"></constructor-arg>
	</bean>

<br/>

UserDetailsService 를 상속받는 AdminService 클래스는 다음과 같다.

@Service
public class AdminService implements UserDetailsService {

	@Autowired
	AdminDao adminDao;

	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		AdminVo admin = adminDao.get(username);

		if (admin != null) {
			List<Role> adminAuths = adminDao.getAuths(username);
			admin.setAuthorities(adminAuths);
		} else{
			throw new UsernameNotFoundException(username);
		}
		
		return admin;
	}

}

추상메소드 loadUserByUsername()을 재정의한다.

매개변수로 username을 받는데, 로그인시 입력한 ID값이다. 해당 ID 값을 DB에서 조회하여, 계정을 가져온다.

계정이 없는 경우 UsernameNotFoundException 예외 처리 해주고, 계정이 있는 경우 권한 테이블에서 권한을 가져와, 객체에 주입한다.

리턴값으로 계정을 리턴한다.

<br/>

계정 모델 클래스는 다음과 같다.

public class AdminVo implements UserDetails {
	private String username;
	private String password;
	private List<Role> authorities;
	
	public AdminVo() {
		authorities = new ArrayList<Role>();
	}

	public AdminVo(String username, String password) {
		this();
		this.username = username;
		this.password = password;
	}
	
	public void setUsername(String username) {
		this.username = username;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public void addAuthoritiy(Role authoritiy) {
		authorities.add(authoritiy);
	}
	
	public void setAuthorities(List<Role> authorities) {
		this.authorities = authorities;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public String getUsername() {
		return username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}
}

<br/>

UserDetails를 상속 받는다. Override 메소드를 살펴보자.

Overide된 메소도를 재정의하면, 기본적인 DB와 연동된 계정 인증 설정은 모두 끝난다.

  • getAuthorities() : 권한들을 리턴한다.

  • getPassword() : 비밀번호를 리턴한다.

  • getUsername() : 계정ID를 리턴한다.

  • isAccountNonExpired() : 계정이 만료 되어있지 않은가?

  • isAccountNonLocked() : 계정이 잠겨 있지 않은가?

  • isCredentialsNonExpired() 비밀번호가 만료 되어있지 않은가?

  • isEnabled() : 사용 가능 계정인가?

<br/>

<br/>

<br/>

#추가적인 설정 (패스워드 암호화, 복호화)

Srping-Security 라이브러리에 포함된 암호화 클래스로부터 passwordEncode 빈을 생성해주었다.

	<security:authentication-manager> <!-- 로그인 관리자 -->
		<security:authentication-provider user-service-ref="adminService">
		<security:password-encoder ref="passwordEncoder"></security:password-encoder>
		</security:authentication-provider>
	</security:authentication-manager>
	
	<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
  		<constructor-arg name="strength" value="256"></constructor-arg>
	</bean>

사용자가 회원가입을 요청할시 해당 빈으로 암호화하여 DB에 저장한다.

계정인증과정에서 자동으로 비밀번호를 복호화하여 검증한다.<br/>

<br/>

<em>그럼 정리해보자.</em>

사용자가 접근권한이 없는 페이지로 이동하려고한다.

접근권한이 없으므로 로그인 페이지 또는 별도의 설정된 접근 거부 페이지로 이동한다.

로그인 페이지로 이동한다면, ID와 PASSWORD를 입력하고 로그인한다.

adminService 객체의 loadUserByUsername() 메소드가 호출되고,

DB를 조회하여 계정이 없다면 예외처리되어, XML에 설정된 authentication-failure-url 페이지로 이동한다.

계정이 있는 경우 계정을 리턴한다.

스프링은 리턴된 계정을 "비밀번호", "계정 만료 여부", "계정 사용 가능 여부" 등을 확인한다.

모든 조건을 만족하면, 사용자는 해당 페이지로 접근한다.

조건을 만족하지 못하면, 사용자는 authentication-failure-url 페이지로 이동한다.

<br/>

<br/>

0
이전 댓글 보기
등록
TOP