Spring 21 Security[업데이트 중]

Kang.__.Mingu·2024년 9월 26일

Spring

목록 보기
20/21

2024-09-19

Spring Security

Spring Security는 웹 애플리케이션의 인증(Authentication)인가(Authorization)를 위한 강력하고 유연한 보안 프레임워크이다. 주로 웹 애플리케이션과 API에서 사용자 인증, 권한 부여, 세션 관리, CSRF 보호, 패스워드 암호화 등 다양한 보안 기능을 제공한다.

핵심 개념

  • 인증
    • 사용자의 신원을 확인하는 과정
    • 사용자가 누구인지 확인하는 단계로, 로그인을 통해 이루어진다.
    • Spring Security는 다양한 인증 방식을 지원한다.(폼 기반 로그인, OAuth2, JWT)
  • 인가
    • 사용자가 어떤 작업을 수행할 권한이 있는지 결정하는 과정
    • 즉, 사용자가 허가된 파일에 접근할 수 있는지 검사하는 단계
  • 필터 기반 아키텍처
    • Spring Security는 필터 체인을 기반으로 동작한다. 모든 요청은 Spring Security 필터 체인을 통과하며, 인증과 인가 절차를 거친다.
    • 즉, 하나의 필터가 종료되고 다음 필터로 넘어간다는 것
  • 보안 설정
    • Spring Security는 기본 보안 설정을 제공하며, 필요에 따라 커스텀할 수 있다.
    • URL 접근 제어, 로그인 및 로그아웃 설정, 비밀번호 암호화 등의 설정을 간단하게 할 수 있다.
  • 비밀번호 암호화
    • Spring Security는 BCrypt 등 다양한 암호화 알고리즘을 지원하여 사용자 비밀번호를 안전하게 관리할 수 있다.
  • CSRF 보호
    • Cross-Site Request Forgery(CSRF) 공격을 방어하는 기능을 제공한다.
    • Spring Security는 CSRF 공격을 방지하기 위한 토큰 기반 보호를 기본적으로 제공한다.

=> Spring Security는 인증과 인가를 위해 Principal 객체를 아이디로 사용하고 Credential 객체를 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용

Spring Security를 SpringMVC 프로그램에 적용하는 방법

  1. spring-security-web, spring-security-core, spring-security-config, spring-security-tablibs 라이브러리를 프로트젝에 빌드 처리 - 메이븐 : pom.xml
<!-- 공용버전으로 세팅해야됨 -->
<properties>
  <spring.security-version>5.8.14</spring.security-version>
</properties>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>${spring.security-version}</version>
</dependency>
		
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-core</artifactId>
	<version>${spring.security-version}</version>
</dependency>
		
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>${spring.security-version}</version>
</dependency>
		
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-taglibs -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-taglibs</artifactId>
	<version>${spring.security-version}</version>
</dependency>
  1. [web.xml] 파일에 Spring Security 기능을 제공하는 필터 클래스를 필터로 등록하고 필터가 실행되기 위한 URL 패턴을 매핑 처리

    • DelegatingFilterProxy 클래스를 필터로 등록되도록 설정
      => 반드시 필터의 이름을 [springSecurityFilterChain]으로 등록되도록 작성해야 한다.

    DelegatingFilterProxy: 메인 Filter Chain에 위치되도록 설정하는 클래스
    => Spring Security 필터를 사용하는 시작점으로 설정되며 서블릿 컨테이너(WAS)의 필터와 ApplicationContext(Spring Container)에서 Spring Bean으로 등록된 필터를 연결하는 다리 역할 수행(밑에 자세하게 설명)

    • 중요한점: 필터에는 등록 순서가 있다.
  <!-- Spring Security 사용시 [multipart/form-data] 형식으로 전달되는 값을 제공받기 위한 
필터 클래스를 필터로 등록 -->
<!-- => [multipart/form-data]를 처리하는 CommonsMultipartResolver 클래스를 root-context.xml
에서 Spring Bean으로 등록 -->
<filter>
	<filter-name>multipartFilter</filter-name>
	<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>

<filter-mapping>
	<filter-name>multipartFilter</filter-name>
	<url-pattern>/*</url-pattern>	
</filter-mapping>
	
<filter>
	<filter-name>encodingFilter</filter-name>
	<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
	<init-param>
		<param-name>encoding</param-name>
		<param-value>utf-8</param-value>
	</init-param>
</filter>
	
<filter-mapping>
	<filter-name>encodingFilter</filter-name>
	<url-pattern>/*</url-pattern>	
</filter-mapping>

<!-- DelegatingFilterProxy 클래스를 필터로 등록되도록 설정 -->
<!-- => 반드시 필터의 이름을 [springSecurityFilterChain]으로 등록되도록 작성 -->
<!-- DelegatingFilterProxy : 메인 Filter Chain에 위치되도록 설정하는 클래스 -->
<!-- => Spring Security 필터를 사용하는 시작점으로 설정되며 서블릿 컨테이너(WAS)의 필터와
ApplicationContext(Spring Container)에서 Spring Bean으로 등록된 필터를 연결하는 다리 역활 수행 -->
<!-- => 사용자가 웹프로그램을 요청하면 DelegatingFilterProxy 필터가 요청을 받아 FilterChainProxy
필터에게 요청을 위임하여 필요한 필터들이 순서대로 실행되도록 동작 -->
<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>
  1. [web.xml] 파일에 Spring Security 기능의 필터가 사용하기 위한 정보를 제공하는 Spring Bean Configuration File 설정 -> ContextLoaderListener 클래스가 읽을 수 있도록 파일 경로 지정
<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>
		/WEB-INF/spring/root-context.xml
		<!-- Spring Security 기능을 제공하기 위한 클래스를 Spring Bean으로 등록하기 위한
		Spring Bean Configuration File을 생성하여 등록 -->
		/WEB-INF/spring/security-context.xml
	</param-value>
</context-param>
  1. Spring Security 기능을 구현하기 위한 Spring Bean Configuration File 작성
    => Spring Security 관련 필터가 동작되기 위한 정보를 security 네임스페이스를 추가하여 spring-security.xsd 파일에 설정된 엘리먼트를 사용하여 제공
    security-context.xml 설명
  • http: Spring Security 기능을 SpringMVC 프로그램에 적용하기 위한 정보를 제공하는 엘리먼트
    => Spring Security 관련 환경 설정이 시작되는위치를 제공

  • auto-config: false 또는 true(기본값) 중 하나를 속성값으로 설정
    => auto-config 속성값을 [true]로 설정한 경우 기본값으로 환경 설정

  • use-expression: false 또는 true(기본값) 중 하나를 속성값으로 설정
    => use-expressions 속성을 [true]로 설정하면 SpEL 표현식을 사용하여 페이지 접근 여부 설정 가능

  • SpEL 표현식: 권한을 표현하는 표현식

    • hasRole('role'): 권한을 가지고 있는 경우를 나타내는 표현식
    • hasAny0Role('role1', 'role2', ...): 나열된 권한 중 하나를 가지고 있는 경우를 나타내는 표현식
    • permitAll: 모든 사용자의 접근 가능을 나타내는 표현식
    • denyAll: 모든 사용자의 접근 불가능을 나타내는 표현식
    • isAnonymous(): Anonymous 사용자(인증 받지 않은 사용자)인 경우에만 접근 가능을 나타내는 표현식
    • isRememberMe(): Remember-me 기능으로 인증받은 사용자인 경우에만 접근 가능을 나타내는 표현식
    • isAuthentucated(): 인증 처리된 사용자(Remember-me 기능으로 인증받은 사용자 제외)인 경우에만 접근 가능을 나타내는 표현식
<http auto-config="true" use-expressions="true"></http>

use-expressions 속성을 [false]로 설정한 경우 access 속성값으로 SpEL 사용 불가능하다.
intercept-url 엘리먼트를 사용해 페이지에 접근 가능한 권한을 하나만 설정 가능하다.(대부분 SpEL 사용)

<intercept-url pattern="/user/**" access="ROLE_USER"/>
<intercept-url pattern="/manager/**" access="ROLE_MANAGER"/>
<intercept-url pattern="/admin/**" access="ROLE_ADMIN"/>
  • intercept-url: 요청 페이지에 접근 가능 권한을 설정하는 엘리먼트
    => 특정 페이지에 대한 권한 설정을 먼저하고 나머지는 마지막에 설정하는 것을 권장

  • pattern 속성: 요청 페이지의 경로를 속성ㄱ밧으로 설정
    => * 또는 ** 등의 패턴문자를 사용하여 속성값 설정 가능

  • access 속성: 페이지에 접근 가능한 권한(Role)을 속성값으로 설정
    => use-expressions 속성을 [true]로 설정한 경우 SpEL을 사용해 권한 설정 가능
    => 권한이 없는 사용자가 페이지를 요청할 경우 AccessDeniedException 발생

<http auto-config="true" use-expressions="true">
 	<intercept-url pattern="/guest/**" access="hasAnyRole('ROLE_USER', 'ROLE_MANAGER', 'ROLE_ADMIN')"/>
 	<intercept-url pattern="/user/**" access="hasRole('ROLE_USER')"/>
 	<intercept-url pattern="/manager/**" access="hasRole('ROLE_MANAGER')"/>
 	<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/>
 	<intercept-url pattern="/**" access="permitAll"/>
</http>
  • authentication-manager: 인증 관리자를 등록하기 위한 엘리먼트
    => 다양한 형태의 인증 방식 제공

  • authentication-provider: 인증 제공자를 등록하기 위한 엘리먼트
    => 실질적인 인증 작업을 진행하는 기능 제공

  • user-service: 인증 정보를 등록하기 위한 엘리먼트
    => 인증 처리를 이용해 사용자의 권한 관련 정보를 반환하는 기능

  • user: 인증을 위한 정보 및 사용자의 권한 정보를 설정하는 엘리먼트

  • name 속성: 사용자를 구분하기 위한 식별자(아이디)를 속성값으로 설정

  • password 속성: 사용자의 비밀번호를 속성값으로 설정
    => Spring Security 5.0 이상에서는 비밀번호를 반드시 암호화 처리하여 비교되도록 구현
    => password속성값으로 설정된 비밀번호 앞부분에 {noop}를 붙여 작성하면 암호화 처리하지 않아도 비교

  • authorities 속성: 권한(Role)을 속성값으로 설정
    => 권한은 ROLE 기반으로 설정 - 속성값을 ROLE_XXX 형식으로 설정
    => 사용자에게 다수의 권한을 제공할 경우, 기호로 구분하여 권한을 나열해 설정 가능

인증이 성공한 경우 Authentication Manager는 Spring Security 관련 세션(Security ContextHolder)에 인증 및 인가 관련 정보(Authentication 객체) 저장

<!-- 임의로 사용자 아이디, 비밀번호, 권한 지정 -->
 <authentication-manager>
 	<authentication-provider>
 		<user-service>
 			<user name="abc123" password="{noop}123456" authorities="ROLE_USER"/>
 			<user name="opq456" password="{noop}123456" authorities="ROLE_USER, ROLE_MANAGER"/>
 			<user name="xyz789" password="{noop}123456" authorities="ROLE_ADMIN"/>
 		</user-service>
 	</authentication-provider>
 </authentication-manager>

DelegatingFilterProxy이 뭔데?

DelegatingFilterProxy는 Spring Security에서 매우 중요한 클래스이다. 이 클래스는 서블릿 컨테이너(WAS)의 필터와 Spring ApplicationContext에 등록된 Spring Security 필터를 연결해주는 다리 역할을 한다. 이를 통해 Spring Security의 필터 체인이 서블릿 컨테이너에서 실행되는 메인 필터 체인에 포함될 수 있다.

DelegatingFilterProxy의 역할

  1. WAS(Web Application Server, 예: Tomcat)는 서블릿 기반 웹 애플리케이션을 실행할 때 필터(Filter)를 사용해 요청(Request)와 응답(Response)을 가로채고 처리할 수 있습니다. 이 필터는 웹 애플리케이션 내에서 보안, 로깅, 캐싱 등을 처리하는 데 사용됩니다.

  2. Spring Security는 WAS의 필터 체인(Filter Chain)을 통해 요청을 가로채고, 인증(Authentication)과 인가(Authorization) 과정을 처리합니다. 하지만 Spring Security의 필터들은 Spring ApplicationContext에서 빈으로 등록되어 있기 때문에, WAS의 기본 필터 체인과 직접 연결되지 않습니다.

  3. DelegatingFilterProxy는 WAS의 필터와 Spring Security 필터를 연결해줍니다.

    • WAS의 필터 체인에 등록되어, 모든 요청을 Spring Security 필터 체인으로 전달하는 역할을 합니다.

    • 이 프록시(DelegatingFilterProxy)는 Spring ApplicationContext에서 빈으로 등록된 필터(Spring Security 필터)를 찾아 실행합니다.

동작 과정

  1. 클라이언트가 서버로 HTTP 요청을 보냅니다.

  2. WAS(Tomcat, Jetty 등)는 요청을 받으면, 자신이 가지고 있는 필터 체인을 통해 요청을 처리합니다.

    • 이때, DelegatingFilterProxy가 필터 체인에 등록되어 있으면, WAS는 요청을 DelegatingFilterProxy로 넘깁니다.
  3. DelegatingFilterProxy는 Spring의 ApplicationContext에 등록된 Spring Security 필터를 호출합니다.

  4. Spring Security 필터는 요청에 대해 인증(Authentication)인가(Authorization)를 처리합니다.

    • 만약 인증된 사용자라면 요청을 처리하고, 인증되지 않았거나 권한이 없는 사용자라면 적절한 응답을 보냅니다.
  5. 필터가 처리를 완료하면 요청이 WAS에 다시 전달되고, 이후의 다른 필터들이 요청을 처리하거나 서블릿으로 요청이 넘어갑니다.

DelegatingFilterProxy는 총 12개의 Filter를 클라이언트가 요청시 자동으로 실행함

Spring Security Filter의 종류

  1. SecurityContextPersistenceFilter : SecurityContextRepository에서 SecurityContext를 가져오거나 생성하는 필터

  2. LogoutFilter : 로그아웃 요청을 처리하는 필터

  3. UsernamePasswordAuthenticationFilter : 아이디와 비밀번호를 사용하는 Form 기반 유저 인증을 처리하는 필터
    => Authentication 객체를 만들고 AuthenticationManager에게 인증 처리 위임
    => AuthenticationManager는 실질적인 인증에 대한 검증 단계를 총괄하는 AuthenticationProvider에게
    인증 처리를 위임 - UserDetailService와 같은 서비스를 사용해서 인증 처리

  4. ConcurrentSessionFilter : 동시 세션과 관련된 필터 - 이중 로그인 방지

  5. RememberMeAuthenticationFilter : 세션이 사라지거나 만료 되더라도 쿠키 또는 DB를 사용하여
    저장된 토큰을 기반으로 인증 처리하는 필터

  6. AnonymousAuthenticationFilter : 사용자 정보가 인증되지 않았다면 익명 사용자 토큰을 반환하는 필터

  7. SessionManagementFilter : 로그인 후 Session과 관련된 작업을 처리하는 필터

  8. ExceptionTranslationFilter : 필터 체인 내에서 발생되는 인증 및 인가 관련 예외를 처리하는 필터

  9. FilterSecurityInterceptor : 권한 부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한 부여 결정 및 접근 제어를 처리하는 필터

  10. HeaderWriterFilter: Request의 HTTP 헤더를 검사해 Header를 추가하거나 빼주는 필터

  11. CorsFilter : 허가된 사이트나 클라이언트의 요청인지 검사하는 필터

  12. CsrfFilter : CSRF Tocken을 사용하여 CSRF 공격을 막아주는 기능을 제공하는 필터


데이터베이스를 이용한 인증 방법

  • 위에서 정의한 임의로 사용자 아이디, 비밀번호 권한을 지정하는 방식이 아닌 데이터베이스를 이용하여 인증 처리를 할 것이다.

security-context.xml

  • jdbc-user-service: Spring Security가 JDBC를 이용해 인증 처리하기 위한 엘리먼트
    => JdbcUserDetailsManager 클래스를 사용해 인증과 인가 처리
    => USERS 테이블을 생성해 사용자 정보를 저장하고 AUTHORITIES 테이블을 생성하여 사용자의 권한 정보를 저장할 경우 SQL 명령을 작성하지 않아도 기본적으로 제공되는 SQL 명령으로 인증 처리 후 사용자 정보(UserDetails 객체)를 반환

  • data-source-ref: Connection 객체를 제공하기 위한 DataSource 관련 클래스의 Spring Bean 식별자(beanName)을 속성값으로 설정

  • password-encoder: 암호화 처리된 비밀번호를 비교하기 위한 기능을 제공하는 엘리먼트

  • ref 속성: PasswordEncoder 인터페이스를 상속받은 클래스의 Spring Bean 식별자(beanName)을 속성값으로 설정

 <authentication-manager>
 	<authentication-provider>
 		<jdbc-user-service data-source-ref="dataSource"/>
 		<password-encoder ref="customPasswordEncoder"/>
 	</authentication-provider>
 </authentication-manager> 
 	
 <beans:bean class="xyz.itwill.security.CustomPasswordEncoder" id="customPasswordEncoder"/>

dataSource는 root-context.xml에 DB 정보를 담고있는 식별자임

root-context.xml

<!-- Mybatis 프레임워크에서 발생되는 로그 이벤트를 전달받아 Spring 프레임워크의 로그 구현체로
기록되도록 driverClassName 필드값과 url 필드값을 log4jdbc-log4j2-jdbc4 라이브러리에서 제공되는 값으로 변경 -->
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
	<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"/>
	<property name="url" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:xe"/>
	<property name="username" value="scott"/>
	<property name="password" value="tiger"/>
</bean>

<!-- SqlSessionFactory 관련 클래스(SqlSessionFactoryBean 클래스)를 Spring Bean으로 등록 -->
<!-- => 필드에 SqlSessionFactory 객체 생성에 필요한 값 또는 객체가 저장되도록 값 주입하거나
의존성 주입 - Setter Injection -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactoryBean">
	<!-- configLocation 필드에 Mybatis 프레임워크의 환경설정파일에 경로가 저장되도록 값 주입 -->
	<!-- [src/main/java] 또는 [src/main/resources] 폴더에 환경설정파일을 작성한 경우
	classpath 접두사를 사용해 환경설정파일의 경로로 값 주입 -->
	<!-- <property name="configLocation" value="classpath:xyz/itwill/config/mybatis-config.xml"/> -->
	<!-- [src/main/webapp] 폴더에 환경설정파일을 작성한 경우 환경설정파일을 웹자원의 경로가 저장되도록 값 주입 -->
	<property name="configLocation" value="/WEB-INF/spring/mybatis-config.xml"/>
  
	<!-- dataSource 필드에는 DataSource 관련 클래스의 Spring Bean에 식별자(beanName)을 사용해
	객체가 저장되도록 의존성 주입 -->
	<!-- => Mybatis 프레임워크의 환경설정파일에서 environment 엘리먼트와 유사한 기능 제공 -->
	<property name="dataSource" ref="dataSource"/>

	<!-- typeAliasesPackage 필드에는 DTO 클래스를 작성한 패키지가 저장되도록 값 주입 -->
	<!-- => XML 기반의 매퍼 파일에서 Java 자료형 대신 사용할 별칭을 제공하기 위해 설정 -->
	<!-- => Mybatis 프레임워크의 환경설정파일에서 typeAlias 엘리먼트의 package 엘리먼트와
	유사한 기능 제공 -->
	<property name="typeAliasesPackage" value="xyz.itwill.dto"/>

	<!-- mapperLocations 필드에 List 객체를 생성하여 저장되도록 의존성 주입 -->
	<!-- => List 객체의 요소에는 XML 기반의 매퍼 파일의 경로를 요소값으로 저장 - 매퍼 등록 -->
  	<!-- => Mybatis 프레임워크의 환경설정파일에서 mapper 엘리먼트와 유사한 기능 제공 -->
	<property name="mapperLocations">
		<list>
			<!-- [src/main/java] 폴더에 작성된 모든 XML 기반의 매퍼 파일을 매퍼로 등록 -->
			<!-- => [src/main/java] 폴더에 작성된 XML 기반의 매퍼 파일을 사용하기 위해
			classpath 접두사를 사용해 파일 경로 제공 -->
				<value>classpath:xyz/itwill/mapper/*.xml</value>
		</list>
	</property>
</bean>

<!-- SqlSession 관련 클래스(SqlSessionTemplate 클래스)를 Spring Bean으로 등록 -->
<!-- => 필드에 SqlSession 객체를 제공하는 SqlSessionFactory 객체가 저장되도록 의존성 주입
- Constructor Injection : 기본 생성자가 없으므로 Setter Injection 대신 Constructor Injection 사용 -->
<!-- => destroy-method 속성을 사용해 SqlSessionTemplate 객체가 소멸되기 전에 clearCache
	메소드가 자동 호출되도록 설정 - SqlSession 객체 사용 후 close() 메소드 호출 생략 가능 -->
<bean class="org.mybatis.spring.SqlSessionTemplate" id="sqlSession" destroy-method="clearCache">
	<constructor-arg ref="sqlSessionFactoryBean"/>
</bean>

<!-- TransactionManager 관련 클래스(DataSourceTransactionManager 클래스)를 Spring Bean으로 등록 -->
<!-- => Spring Bean의 식별자(beanName)를 반드시 [transactionManager]로 설정 -->
<!-- => dataSource 필드에 TransactionManager 객체에 의해 트렌젝션이 관리될 DataSource 객체가
	저장 되도록 의존성 주입 - Setter Injection -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
	<property name="dataSource" ref="dataSource"/>
</bean>

<!-- annotation-driven : @Transcational 어노테이션을 사용해 TransactionManager 객체로
	트렌젝션 관리 기능을 제공하는 엘리먼트 -->
<tx:annotation-driven/>

<!-- multipart/form-data를 사용하려면 기존에는 servlet-context.xml에 설정했는데, Spring Security를 사용할 때는 root-context.xml에 설정해줘야 한다.(꼭!!!!!!) -->
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="filterMultipartResolver">
	<property name="maxUploadSize" value="20971520"/>
	<property name="defaultEncoding" value="utf-8"/>
</bean>

CustomPasswordEncoder

  • 비밀번호를 전달받아 암호화 처리하여 반환하거나 암호화 처리된 비밀번호를 비교한 결과를 반환하는 기능의 메소드가 작성된 클래스
// => PasswordEncoder 인터페이스를 상속받아 작성
public class CustomPasswordEncoder implements PasswordEncoder {
	//매개변수로 전달받은 문자열을 암호화 처리해 반환하는 메소드
	@Override
	public String encode(CharSequence rawPassword) {
		return rawPassword.toString();
	}

	//매개변수로 전달받은 암호화된 문자열과 일반 문자열을 비교하여 비교결과를 논리값으로 반환하는 메소드
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		return rawPassword.toString().equals(encodedPassword);
	}
}

SQL 설정

Spring Security는 SQL 컬러명이 다 정해져 있음. 내가 원하는 컬럼명을 쓰면 다 오류 => 정해진 컬럼만 사용하자

create table users(username varchar2(100) primary key, password varchar2(100), enabled varchar2(1));

insert into users values('abc123', '123456', '1');
insert into users values('opq456', '123456', '1');
insert into users values('xyz789', '123456', '1');
insert into users values('test000', '123456', '0');
commit;

create table authorities(username varchar2(100), authority varchar2(50)
	    , CONSTRAINT authorities_username_fk foreign key(username) REFERENCES users(username));
-- 외래 키 설정으로 authorities 테이블의 username은 users 테이블의 username과 연결됨

create unique index authorities_username_index on authorities(username, authority);    

insert into authorities values('abc123', 'ROLE_USER');
insert into authorities values('opq456', 'ROLE_USER');
insert into authorities values('opq456', 'ROLE_MANAGER');
insert into authorities values('xyz789', 'ROLE_ADMIN');
insert into authorities values('test000', 'ROLE_USER');
commit;

CSRF Token

  • CSRF(Cross-Site Request Forgery) 공격: 사이트의 요청을 위조하는 공격 방법

  • CSRF 공격을 방어하기 위해 Spring Security에서 발급된 CSRF Token을 hidden으로 전달

  • 서버에 전달된 요청이 실제 서버에서 허용된 요청인지를 확인하기 위해 CSRF Token 발급

  • 서버에서는 뷰페이지를 생성할 때마다 랜덤으로 토큰을 발행하여 세션에 저장하고 사용자가 서버의 페이지를 요청할 때 hidden 타입으로 토큰을 전달해 세션에 저장된 토큰과 비교하여 클라이언트 확인하는 방식으로 작동

  • 일치 여부를 확인한 토큰은 삭제하고 새로운 뷰에 대한 토큰을 다시 발행

  • 폼 태그 만들 때마다 꼭 csrf 토큰을 전달해줘야됨

<%-- multipart/form-data 사용하려면 web.xml, root-context.xml 설정 => Spring Security에서만 --%>
<form method="post" enctype="multipart/form-data">
		이름 : <input type="text" name="name">
        <%-- 토큰 인증 --%>
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token }">
		<button type="submit">제출</button>
</form>
  • CSRF 토큰을 생성해서 전달하고 싶지 않다면 security-context.xml에 설정

  • csrf: CSRF 토큰 사용 여부를 설정하기 위한 엘리먼트

  • disabled 속성: false 또는 true(기본값) 중 하나를 속성값으로 설정
    => disabled 속성값을 [true]로 설정하면 CSRF 토큰을 발생하지 않는다

  • 프론트엔드 개발자가 AJAX를 사용하지 않으면 토큰 넘기는 걸 비활성화 처리하고 사용, 그리고 Restful로 만들 때도 사용 안함
    => 단점: 비활성화 처리하면 보안에 취약해짐
    => 그러므로 가능하면 토큰인증 방식으로 넘기자

<http>
	<csrf disabled="true"/>
</http>

로그인 인증 실패시 발생되는 에러 메세지 제공

  • 인증 실패시 발생되는 에러 메세지를 제공하기 위한 메세지 파일(Properties 파일) 설정
    root-context.xml
<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="messageSource">
	<property name="basenames">
		<list>
          	<!-- 해당 경로에 properties 파일 설정 -->
			<value>/WEB-INF/message/security_message</value>
		</list>
	</property>
	<property name="cacheSeconds" value="60"/>
	<property name="defaultEncoding" value="utf-8"/>
</bean>

security_message.properties

AbstractUserDetailsAuthenticationProvider.badCredentials = \uC544\uC774\uB514 \uB610\uB294 \uBE44\uBC00\uBC88\uD638\uAC00 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
AbstractUserDetailsAuthenticationProvider.disabled = \uBE44\uD65C\uC131\uD654 \uCC98\uB9AC\uB41C \uC0AC\uC6A9\uC790 \uC785\uB2C8\uB2E4.
AbstractUserDetailsAuthenticationProvider.credentialsExpired = \uBBF8\uBE4C\uBC88\uD638 \uC720\uD6A8\uAE30\uAC04\uC774 \uC9C0\uB0AC\uC2B5\uB2C8\uB2E4.
AbstractUserDetailsAuthenticationProvider.locked = \uC7A0\uACA8\uC838 \uC788\uB294 \uC0AC\uC6A9\uC790\uC785\uB2C8\uB2E4.
AbstractUserDetailsAuthenticationProvider.expired = \uC720\uD6A8\uAE30\uAC04\uC774 \uC9C0\uB09C \uC0AC\uC6A9\uC790\uC785\uB2C8\uB2E4.
  • badCredentials: 아이디 또는 비밀번호가 맞지 않을 때 표시되는 메시지.

  • disabled: 계정이 비활성화된 경우 출력되는 메시지.

  • credentialsExpired: 비밀번호의 유효기간이 만료되었을 때.

  • locked: 계정이 잠긴 상태에서 로그인하려고 할 때.

  • expired: 계정 자체가 만료된 경우.

2024-09-20

SQL(security_user, security_auth 테이블)

create table security_user(
	userid varchar2(100) primary key,   -- 사용자 ID(Primary Key)
	passwd varchar2(100),               -- 비밀번호
	name varchar2(50),                  -- 이름
	email varchar2(100),                -- 이메일
	enabled varchar2(1));               -- 계정 활성화 여부 (1: 활성화, 0: 비활성화)

-- security_auth 테이블 생성: 사용자 권한 정보를 저장
create table security_auth(
	userid varchar2(100),  -- security_user 테이블의 userid와 연결될 외래 키
	auth varchar2(50),     -- 사용자의 권한 (예: ROLE_USER, ROLE_ADMIN 등)
	
	-- constraint: 제약 조건을 정의하는 키워드. 여기서는 외래 키 제약을 설정
	-- auth_userid_fk: 외래 키 제약 조건의 이름
	constraint auth_userid_fk
	foreign key(userid)  -- security_auth 테이블의 userid가 security_user 테이블의 userid를 참조함
	references security_user(userid)  -- security_user 테이블의 userid 컬럼을 참조
);

-- 같은 사용자(userid)가 동일한 권한(auth)을 
-- 중복해서 가질 수 없도록 보장하는 것
create unique index 
	auth_userid_index on 
    security_auth(userid, auth); 

SecurityUser(DTO)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SecurityUser {
	// 사용자 ID를 저장하는 필드
	private String userid;
    // 사용자 비밀번호 저장하는 필드
	private String passwd;
    // 사용자 이름을 저장하는 필드
	private String name;
    // 사용자 이메일 주소를 저장하는 필드
	private String email;
    // 사용자 게정 활성화 상태를 나타내는 필드
	private String enabled;
    // SecurityAuth 객체의 리스트로, 
    // 사용자의 권한 정보를 나타내는 필드
	private List<SecurityAuth> securityAuthList;	
}

SecurityAuth(DTO)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SecurityAuth {
	// 사용자 ID를 저장하는 필드
	private String userid;
	// 사용자 권한을 나타내는 필드
	private String auth;
}

SecurityUserMapper.java

public interface SecurityUserMapper {
	int insertSecurityUser(SecurityUser user);
	int insertSecurityAuth(SecurityAuth auth);
	SecurityUser selectSecurityUserByUserid(String userid);
}

SecurityUserMapper.java

<mapper namespace="xyz.itwill.mapper.SecurityUserMapper">
	<insert id="insertSecurityUser">
		insert into security_user values(#{userid}, #{passwd}, #{name}, #{email}, #{enabled})
	</insert>
	
	<insert id="insertSecurityAuth">
		insert into security_auth values(#{userid}, #{auth})
	</insert>

	<!-- 사용자 정보만 먼저 조회 -->
	<select id="selectSecurityUserByUserid" resultMap="securityUserResultMap">
		select userid, passwd, name, email, enabled from security_user where userid=#{userid}
	</select>

	<!-- 권한 정보를 별도로 조회 -->
	<select id="selectSecurityAuthByUserid" resultType="SecurityAuth">
		select userid, auth from security_auth where userid=#{userid}
	</select>

	<!-- Join을 사용하지 않고 검색하는 방법 -->
	<resultMap type="SecurityUser" id="securityUserResultMap">
		<id column="userid" property="userid"/>
		<result column="passwd" property="passwd"/>
		<result column="name" property="name"/>
		<result column="email" property="email"/>
		<result column="enabled" property="enabled"/>
		<collection property="securityAuthList" 
        	select="selectSecurityAuthByUserid" 
        	column="userid"/>
	</resultMap>
</mapper>

resultMap 이해안가서 다시 정리

<resultMap type="SecurityUser" id="securityUserResultMap">
    <id column="userid" property="userid"/>
    <result column="passwd" property="passwd"/>
    <result column="name" property="name"/>
    <result column="email" property="email"/>
    <result column="enabled" property="enabled"/>
    <collection property="securityAuthList" select="selectSecurityAuthByUserid" column="userid"/>
</resultMap>
  • <resultMap>: MyBatis에서 SQL조회 결과를 SecurityUser 객체에 매핑하는 설정을 정의한다.

    • type="SecurityUser": 이 resultMap이 반환할 객체 타입을 설정하고, 여기서는 SecurityUser 클래스에 값을 넣어주게 된다.
    • id="securityUserResultMap": 이 resultMap의 식별자(아이디). 나중에 다른 곳에서 이 resultMap을 참조할 때 사용된다.
  • <id column="userid" property="userid"/>:

    • SQL 조회 결과의 userid 컬럼 값을 SecurityUser 객체의 userid 필드에 매핑한다.
    • 이 부분은 SQL의 기본 키(primary key)에 해당하는 컬럼과 객체의 필드를 연결하는 역할을 한다.
  • <result column="passwd" property="passwd"/>:

    • SQL 결과의 passwd 컬럼 값을 SecurityUser 객체의 passwd 필드에 매핑한다.
      => name, email, enabled 필드 동일
  • <collection>:

    • collection은 일대다 관계를 매핑할 때 사용한다.

    • property="securityAuthList":
      SecurityUser 객체의 securityAuthList 필드와 매핑된다. 이는 List 타입의 필드로, 여러 개의 SecurityAuth 객체를 담을 수 있다.

    • select="selectSecurityAuthByUserid": 이 필드는 securityAuthList를 채우기 위해 사용할 별도의 SQL 쿼리를 정의한다. 이 경우, selectSecurityAuthByUserid라는 쿼리를 호출하여 security_auth 테이블에서 권한 리스트를 조회한다.

    • column="userid": security_user.userid를 기준으로 권한 정보를 조회하기 위해 selectSecurityAuthByUserid 쿼리에 userid를 전달한다.

SecurityUserDAO.java

public interface SecurityUserDAO {
	int insertSecurityUser(SecurityUser user);
	int insertSecurityAuth(SecurityAuth auth);
	SecurityUser selectSecurityUserByUserid(String userid);
}

SecurityUserDAOImpl.java

// 이 클래스가 데이터 액세스 객체(DAO)임을 나타내며, Spring 컨테이너에 의해 관리되는 Bean으로 등록됩니다.
@Repository  
// Lombok에서 제공하는 어노테이션으로, final로 선언된 필드에 대한 생성자를 자동으로 생성해 줍니다.
@RequiredArgsConstructor  
public class SecurityUserDAOImpl implements SecurityUserDAO {  
 	// MyBatis의 SqlSession 객체로, SQL 실행 및 매퍼 매핑을 위한 객체입니다. final로 선언되었으므로 생성자를 통해 주입됩니다.
	private final SqlSession sqlSession; 
	
	@Override
	public int insertSecurityUser(SecurityUser user) {
		// sqlSession 객체를 사용해 SecurityUserMapper 인터페이스를 매핑하여 SQL 실행
		// user 객체를 전달하여 security_user 테이블에 새로운 사용자 정보를 삽입
		return sqlSession.getMapper(SecurityUserMapper.class).insertSecurityUser(user);
	}

	@Override
	public int insertSecurityAuth(SecurityAuth auth) {
		// sqlSession 객체를 사용해 SecurityUserMapper 인터페이스를 매핑하여 SQL 실행
		// auth 객체를 전달하여 security_auth 테이블에 새로운 권한 정보를 삽입
		return sqlSession.getMapper(SecurityUserMapper.class).insertSecurityAuth(auth);
	}

	@Override
	public SecurityUser selectSecurityUserByUserid(String userid) {
		// sqlSession 객체를 사용해 SecurityUserMapper 인터페이스를 매핑하여 SQL 실행
		// userid를 전달하여 해당 ID에 해당하는 사용자의 정보를 security_user 테이블에서 조회
		// 사용자 정보를 SecurityUser 객체로 반환
		return sqlSession.getMapper(SecurityUserMapper.class).selectSecurityUserByUserid(userid);
	}
}

SecurityUserController.java

@Controller
@RequiredArgsConstructor
public class SecurityUserController {
	private final SecurityUserService securityUserService;
	
	@RequestMapping(value = "/member/add", method = RequestMethod.GET)
	@ResponseBody
	public String addUser() {
		SecurityUser user1=new SecurityUser("abc123", "123456", "홍길동", "abc@itwill.xyz", "1", null);
		SecurityUser user2=new SecurityUser("opq456", "123456", "임꺽정", "opq@itwill.xyz", "1", null);
		SecurityUser user3=new SecurityUser("xyz789", "123456", "전우치", "xyz@itwill.xyz", "1", null);
		
		securityUserService.addSecurityUser(user1);
		securityUserService.addSecurityUser(user2);
		securityUserService.addSecurityUser(user3);
		
		SecurityAuth auth1=new SecurityAuth("abc123", "ROLE_USER");
		SecurityAuth auth2=new SecurityAuth("opq456", "ROLE_USER");
		SecurityAuth auth3=new SecurityAuth("opq456", "ROLE_MANAGER");
		SecurityAuth auth4=new SecurityAuth("xyz789", "ROLE_ADMIN");
		
		securityUserService.addSecurityAuth(auth1);
		securityUserService.addSecurityAuth(auth2);
		securityUserService.addSecurityAuth(auth3);
		securityUserService.addSecurityAuth(auth4);
		
		return "success";
	}
}
  • 해당 테이블에 값을 집어넣는 코드이다. 하지만 비밀번호 같은 경우 암호화 처리되어서 값을 넣어야 한다.

  • Spring Security는 비밀번호는 암호화 처리 필수!!

=> 암호화 설정은 BCryptPasswordEncoder 클래스를 security-context.xml에서 Spring Bean으로 등록한 후 SecurityUserController.java에 필드로 등록하여 자동으로 의존성 주입이 되게 만든 후 해당 클래스의 메소드를 호출하여 비밀번호를 감싸줘야 암호화 처리가 된다.

security-context.xml(BCryptPasswordEncoder 암호화)

  • BCryptPasswordEncoder 클래스
    • 문자열을 암호화 처리하거나 암호화 처리된 문자열을 비교하여 비교결과를 제공하기 위한 클래스
    • passwordEncoder 인터페이스를 수현한 클래스이므로 필드에 PasswordEncoder로 작성하면 자동으로 의존성 주입이 됨
<beans:bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="passwordEncoder"/>

SecurityUserController.java(PasswordEncoder 필드)

  • security-context.xml에서 BCryptPasswordEncoder를 Spring Bean으로 등록한 후 PasswordEncoder 인터페이스를 필드로 생성하여 BCryptPasswordEncoder가 의존성 주입될 수 있게 함

  • 의존성 주입된 BCryptPasswordEncoder 클래스의 메소드를 이용하여 비밀번호를 암호화처리하여 테이블에 주입될 수 있게 함

@Controller
@RequiredArgsConstructor
public class SecurityUserController {
	private final SecurityUserService securityUserService;
    // BCryptPasswordEncoder 의존성 주입될 수 있게 함
	private final PasswordEncoder passwordEncoder;
	
	@RequestMapping(value = "/member/add", method = RequestMethod.GET)
	@ResponseBody
	public String addUser() {
    	// passwordEncoder.encode() 메소드를 이용하여 비밀번호 암호화
		SecurityUser user1=new SecurityUser("abc123", passwordEncoder.encode("123456"), "홍길동", "abc@itwill.xyz", "1", null);
		SecurityUser user2=new SecurityUser("opq456", passwordEncoder.encode("123456"), "임꺽정", "opq@itwill.xyz", "1", null);
		SecurityUser user3=new SecurityUser("xyz789", passwordEncoder.encode("123456"), "전우치", "xyz@itwill.xyz", "1", null);
		
		securityUserService.addSecurityUser(user1);
		securityUserService.addSecurityUser(user2);
		securityUserService.addSecurityUser(user3);
		return "success";
	}       

Spring Security 커스텀 인증

  • Spring Security 커스텀 인증을 주로 사용하기 때문에 꼭 기억하기!!

  • authentication-manager : 인증 관리자를 등록하기 위한 엘리먼트

  • authentication-provider : 인증 제공자를 등록하기 위한 엘리먼트
    => 실질적인 인증 처리를 진행하는 기능 제공

    • user-service-ref 속성 : 인증에 필요한 사용자 정보 및 권한 정보를 테이블에서 검색해 UserDetails 객체(CustomUserDetails 객체)로 반환하는 메소드가 작성된 클래스의 Spring Bean의 식별자(beanName)를 속성값으로 설정
  • password-encoder : 비밀번호를 암호화 처리하거나 암호화 처리된 비밀번호를 비교하기 위한 기능을 제공하는 엘리먼트

    • ref 속성 : PasswordEncoder 인터페이스를 상속받은 클래스의 Spring Bean 식별자(beanName)을 속성값으로 설정
 <authentication-manager>
    <!-- UserDetails객체를 반환하는 메소드가 작성된 식별자로 ref 설정 -->
 	<authentication-provider user-service-ref="customUserDetailsService">
      	<!-- BCryptPasswordEncoder클래스의 식별자 => passwordEncoder -->
 		<password-encoder ref="passwordEncoder"/>
 	</authentication-provider>
 </authentication-manager> 

UserDetails(CustomUserDetails.java) 중요❗

  • 인증된 사용자 정보와 권한 정보를 저장하기 위한 클래스

  • 쉽게 말해 로그인 정보가 UserDetails에 다 저장되어있음

  • 인증 성공 후 사용자 정보 및 권한 정보가 저장된 UserDetails 객체를 Spring Security로 부터 제공받아 사용 가능하지만 사용자 정보 및 권한 정보를 원하는 형태로 제공받기 위해 클래스 작성

  • UserDetails 인터페이스를 상속받아 작성(간단)

  • User 클래스를 상속받아서도 작성 가능하지만 설정할 게 많음(복잡)

  • UserDetails or User 클래스 상속받아서 작성

@Data
public class CustomUserDetails implements UserDetails {
	// UserDetails 인터페이스는 직렬화를 지원하는 Serializable 인터페이스를 상속받고 있기 때문에 CustomUserDetails 클래스는 직렬화 대상이 된다.
	// 직렬화: 객체를 바이트 스트림으로 변환하는 과정이다. 예를 들어, 객체를 파일에 저장하거나 네트워크로 전송할 때 사용
	private static final long serialVersionUID = 1L;
	
	//인증된 사용자 정보가 저장될 필드 작성
	private String userid;
	private String passwd;
	private String name;
	private String email;
	private String enabled;
	
	//인증된 사용자의 권한 정보가 저장될 필드 작성
	private List<GrantedAuthority> secuthryAuthList;

	//매개변수로 전달받은 SecurityUser 객체의 필드값을 CustomUserDetails 객체의 필드에 저장
	public CustomUserDetails(SecurityUser user) {
		this.userid=user.getUserid();
		this.passwd=user.getPasswd();
		this.name=user.getName();
		this.email=user.getEmail();
		this.enabled=user.getEnabled();
		
		this.secuthryAuthList=new ArrayList<GrantedAuthority>();
		//검색된 사용자의 권한 정보를 GrantedAuthority 객체로 생성하여 List 객체의 요소값으로 저장
		for(SecurityAuth auth : user.getSecurityAuthList()) {
			secuthryAuthList.add(new SimpleGrantedAuthority(auth.getAuth()));
		}
	}
	
	//인증된 사용자의 권한정보를 반환하는 메소드
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return secuthryAuthList;
	}

	//인증된 사용자의 비밀번호를 반환하는 메소드
	@Override
	public String getPassword() {
		return passwd;
	}

	//인증된 사용자의 식별자(아이디)를 반환하는 메소드
	@Override
	public String getUsername() {
		return userid;
	}

	//인증된 사용자의 유효기간 상태를 반환하는 메소드
	// => false : 사용자 유효기간 초과 상태, true : 사용자 유효기간 미초과 상태
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	//인증된 사용자의 잠금 상태를 반환하는 메소드
	// => false : 잠금 상태, true : 해제 상태
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	//인증된 사용자 비밀번호의 유효기간 상태를 반환하는 메소드
	// => false : 사용자 비밀번호 유효기간 초과 상태, true : 사용자 비밀번호 유효기간 미초과 상태	
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	//인증된 사용자의 활성화 상태를 반환하는 메소드
	// => false : 사용자 비활성화 상태, true : 사용자 비활성화 상태  
	@Override
	public boolean isEnabled() {
		if(enabled.equals("0")) {
			return false; 
		} else {
			return true; 
		}
	}
}
  • 오버라이드된 메서드 중에 DTO 필드에 해당되는 메서드만 찾아서 사용하면 된다.
    => 세팅해주는 느낌

  • secuthryAuthList는 하나의 사용자가 여러 개의 권한을 가질 수 있으니까 List로 만드는 것

  • 인증된 사용자 정보와 권한 정보를 저장하기 위한 클래스를 만들어줬으면 사용할 수 있는 서비스로 만들어줘야 한다.
    => CustomUserDetailsService.java

CustomUserDetailsService.java

  • 테이블에 저장된 사용자 정보 및 권한 정보를 검색하여 인증 처리 후 사용자 정보 및 권한 정보가 저장된 UserDetails 객체를 반환하는 메소드가 작성된 클래스

  • UserDetailsService 인터페이스를 상속받아 작성

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
	private final SecurityUserDAO securityUserDAO;
	
	// username(사용자 ID)를 기준으로 사용자 정보를 조회하는 메서드
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// DB에서 사용자 정보를 조회합니다.
		SecurityUser user = securityUserDAO.selectSecurityUserByUserid(username);

		// 사용자가 없으면 예외를 발생
		if (user == null) {
			throw new UsernameNotFoundException(username);
		}
		
		// 조회한 사용자 정보를 기반으로 CustomUserDetails 객체를 생성하여 반환
		return new CustomUserDetails(user);
	}
}

커스텀 인증 흐름

Spring Security의 커스텀 인증 흐름에서는 CustomDetailsService를 구현하여 사용자의 정보를 로드하고, 이를 CustomUserDetails객체로 반환한다.

CustomUserDetails는 인증된 사용자 정보를 담고 있으며, 권한 정보도 관리한다. 이 모든 설정을 security-context.xml에서 Spring Security가 인식할 수 있도록 연결해준다.

2024-09-23

로그인 페이지 커스텀(security-context.xml)

  • form-login: form 태그를 사용한 로그인 페이지를 설정하기 위한 엘리먼트
    => form-login 엘리먼트를 설정하지 않은 경우 Spring Security에서 제공하는 로그인 페이지(/login) 사용

  • login-page 속성: 아이디와 비밀번호를 입력받기 위한 페이지의 경로를 속성값으로 설정

  • login-processing-url 속성: 아이디와 비밀번호를 전달받아 인증 처리하는 페이지의 경로를 속성값으로 설정

  • username-parameter: 로그인 처리 페이지에게 아이디를 전달할 때 사용할 이름(name 속성값)을 속성값으로 설정
    => 미설정시 아이딩는 반드시 [username] 속성값으로 설정

  • password-parameter: 로그인 처리 페이지에게 비밀번호를 전달할 때 사용할 이름(name 속성값)을 속성값으로 설정
    => 미설정시 아이디는 반드시 [password] 속성값으로 설정

  • default-target-url 속성: 인증 성공 후 이동될 페이지 경로를 속성값으로 설정

  • authentication-failure-forward-url: 인증 실패 후 이동될 페이지 경로를 속성값으로 설정

  • authentication-failure-handler-ref : 인증 실패 후 호출될 메소드가 작성된 클래스의 Spring 식별자(beanName)를 속성값으로 설정

  • authentication-success-forward-url: 인증 성공 후 이동될 페이지 경로를 속성값으로 설정

  • authentication-success-handler-ref: 인증 성공 후 호출될 메소드가 작성된 클래스의 Spring 식별자(beanName)를 속성값으로 설정

 <!--
 <form-login login-page="/user_login" login-processing-url="/user_login"
 	username-parameter="userid" password-parameter="passwd"
 	default-target-url="/"
	authentication-failure-handler-ref="customLoginFailureHandler" 
	authentication-success-handler-ref="customLoginSuccessHandler"/>
-->
 		
 <form-login login-page="/user_login" login-processing-url="/user_login"
 	username-parameter="userid" password-parameter="passwd"
 	default-target-url="/"
 	authentication-failure-handler-ref="customLoginFailureHandler"/>

LoginController.java

@RequestMapping(value = "/user_login", method = RequestMethod.GET)
public String userLogin() {
	return "form_login";
}

form_login.jsp(로그인 페이지)

<body>
	<h1>로그인</h1>
	<hr>
	<form action="<c:url value="/user_login"/>" method="post" id="loginForm">
	<table>
		<tr>
			<td>아이디</td>
			<%-- 인증 처리 페이지에 아이디를 전달하는 name 속성값은 반드시 [username]으로 설정 --%>
			<%-- <td><input type="text" name="username" id="userid"></td> --%>
			<td><input type="text" name="userid" id="userid" value="${userid }"></td>
			<c:remove var="userid"/>
		</tr>
		<tr>
			<td>비밀번호</td>
			<%-- 인증 처리 페이지에 비밀번호를 전달하는 name 속성값은 반드시 [password]으로 설정 --%>
			<%-- <td><input type="password" name="password" id="passwd"></td> --%>
			<td><input type="password" name="passwd" id="passwd"></td>
		</tr>
		<tr>
			<td colspan="2"><button type="submit">로그인</button></td>
		</tr>
	</table>
	<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
	</form>
	<%-- SPRING_SECURITY_LAST_EXCEPTION : Spring Security에 의해 마지막에 발생된 예외(Exception 객체)가
	Session Scope 속성값으로 저장된 속성명 --%>
	<%-- => Spring Security에 의해 예외가 발생된 경우 태그를 포함하여 출력 처리 --%>
	<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION }">
		<hr>
		<%-- <h3 style="color: red;">아이디 또는 비밀번호가 맞지 않습니다.</h3> --%>
      	<%-- WEB-INF -> message -> security_message.properties에 저장해놓은 거 호출되는 것 --%>
		<h3 style="color: red;">${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message }</h3>
		<%-- 예외가 저장된 Session Scope 속성값 제거 --%>
		<c:remove var="SPRING_SECURITY_LAST_EXCEPTION"/>
	</c:if>
	
	<hr>
	<h3><a href="<c:url value="/"/>">메인페이지</a></h3>
	
	<script type="text/javascript">
	$("#loginForm").submit(function() {
		if($("#userid").val() == "") {
			alert("아이디를 입력해 주세요.");
			return false;
		}		
		
		if($("#passwd").val() == "") {
			alert("비밀번호를 입력해 주세요.");
			return false;
		}
	});
	</script>
</body>

인증 실패 후 실행될 기능을 제공하기 위한 클래스(CustomLoginFailurehandler.java)

  • 인증 실패 후 실행될 기능을 제공하기 위한 클래스
    => 로그인 실패 횟수 누적, 게정 비활성화 처리 등의 기능 구현

  • AuthenticationFailureHandler 인터페이스를 상속받아 작성하거나 AuthenticationFailureHandler
    인터페이스를 상속받은 자식클래스를 상속받아 작성

  • 인증 실패시 Session Scope 속성값으로 아이디를 저장하고 로그인 페이지로 이동하는 기능을 제공
    => SimpleUrlAuthenticationFailureHandler 클래스를 상속받아 작성

  • SimpleUrlAuthenticationFailureHandlerAuthenticationFailureHandler 인터페이스의 기본 구현체이다. 이 클래스를 사용하면 로그인 실패 시 단순히 특정 URL로 리다이렉트하는 동작을 기본으로 제공한다.
    => 주로 로그인 실패 시 실패 페이지로 리다이렉트하려는 경우 사용한다.

  • Spring Bean으로 등록 => security-context.xml에 등록

@Component
public class CustomLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
	//인증 실패시 자동 호출되는 메소드
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		// 로그인 실패 시 사용자가 입력한 'userid' 값을 세션에 저장
		// => 다음 로그인 시 또는 실패 메세지를 출력할 때 사용할 수 있음
		// => request.getSession(): 현재 세션을 가져옴(세션을 가져오거나, 없으면 새로 생성)
		// => setAttribute 세션에 저장
		request.getSession().setAttribute("userid", request.getParameter("userid"));

		// 로그인 실패 후 리다이렉트할 기본 URL 설정
		setDefaultFailureUrl("/user_login");

		// 기본적인 로그인 실패 처리
		super.onAuthenticationFailure(request, response, exception);
	}
}

로그인 되면 => security-context.xml default-target-url 속성의 "/" main으로 이동됨
=> / 요청 처리 메소드는 HomeController.java에서 처리

인증 성공 후 실행될 기능을 제공하기 위한 클래스(CustomLoginSuccessHandler.java)

  • 인증 성공 후 실행될 기능을 제공하기 위한 크래스
    => 마지막 로그인 날짜 변경 처리 또는 로그인 실패 횟수 초기화 등의 기능 구현 AuthenticationSuccessHandler 인터페이스를 상속받아 작성하거나 AuthenticationSuccessHandler 인터페이스를 상속받은 자식클래스를 상속받아 작성

  • 인증 성공 후 사용자 권한에 따라 다른 페이지로 이동하는 기능을 제공

@Component
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
	//인증 성공시 자동 호출되는 메소드
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		
		//로그인 사용자의 권한을 저장하기 위한 List 객체 생성
		List<String> roleNames=new ArrayList<String>();
		
		//Authentication.getAuthorities() : 인증된 사용자에 대한 모든 권한정보(GrantedAuthority 객체)가 
		//요소값으로 저장된 List 객체를 반환하는 메소드
		// => GrantedAuthority 객체 : 사용자의 권한정보가 저장된 객체
		// => CustomUserDetails.java에 인증된 사용자의 모든 권한 정보가 저장될 필드 참고
		for(GrantedAuthority authority : authentication.getAuthorities()) {
			//GrantedAuthority.getAuthority() : GrantedAuthority 객체에 저장된 권한정보를 문자열로 반환하는 메소드
			roleNames.add(authority.getAuthority());
		}
		
		//Collection<T>.contains(T Obj) : Collection 객체에 저장된 요소값을 매개변수로 전달받은
		//값과 비교하여 요소값이 없으면 [false]를 반환하고 요소값이 있으면 [true]를 반환하는 메소드 
		if(roleNames.contains("ROLE_ADMIN")) {
			//클라이언트에게 URL 주소를 전달하여 응답 처리
			response.sendRedirect(request.getContextPath()+"/admin/");
		} else if(roleNames.contains("ROLE_MANAGER")) {
			response.sendRedirect(request.getContextPath()+"/manager/");
		} else if(roleNames.contains("ROLE_USER")) {
			response.sendRedirect(request.getContextPath()+"/user/");
		} else {
			response.sendRedirect(request.getContextPath()+"/guest/");
		}
	}
}

로그아웃(security-context.xml)

  • logout: 로그아웃 처리 기능을 설정하기 위한 엘리먼트

  • logout-url 속성: 로그아웃 처리를 제공하기 위한 요청 페이지의 경로를 속성값으로 설정

  • logout-success-url 속성: 로그아웃 처리 후 요청 페이지의 경로를 속성값으로 설정

  • invalidate-session 속성: false 또는 true(기본값) 중 하나를 속성ㄱ밧으로 설정
    => invalidate-session 속성값을 [true]로 설정하면 로그아웃 처리 후 세션 언바인딩 처리

  • delete-cookies 속성: 로그아웃 처리 후 삭제될 쿠키의 이름을 속성값으로 설정 delete-cookies 속성: 로그아웃 처리 후 삭제될 쿠키의 이름을 속성값으로 설정

 <logout logout-url="/logout" logout-success-url="/" 
         invalidate-session="true"
 		delete-cookies="JSESSIONID"/>

로그인 사용자 정보가 저장(HomeController.java)

  • 요청 처리 메소드의 매개변수를 Principal 인터페이스로 작성하면 Front Controller에게 principal 객체를 제공받아 사용 가능
    => Principal 객체 로그인 사용자 정보가 저장된 객체 - 로그인 사용자의 아이디 제공

  • Principal 인터페이스 >> Authentication 인터페이스 >> AbstractAuthenticationToken 추상클래스 >> UsernamePasswordAuthenticationToken 클래스 - 구현 클래스

  • 요청 처리 메소드의 매개변수를 Authentication 인터페이스로 작성하면 Front Controller에게 Authentication 객체를 제공받아 사용 가능
    => Authentication: 로그인 사용자 및 권한 정보가 저장된 객체

  • Authentication.getprincipal(): 로그인 사용자 및 권한 정보가 저장된 UserDetailes 객체(CustomUserDetailes 객체)를 반환하는 메소드

@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Authentication authentication) {
	if(authentication != null) {//로그인 사용자가 있는 경우
		//Authentication.getPrincipal() : 로그인 사용자 및 권한 정보가 저장된 UserDetailes
		//객체(CustomUserDetailes 객체)를 반환하는 메소드
		// => Object 객체를 반환하므로 명시적 객체 형변환 후 사용
		CustomUserDetails user=(CustomUserDetails)authentication.getPrincipal();
			
		log.warn("아이디 = "+user.getUserid());
		log.warn("이름 = "+user.getName());
		log.warn("이메일 = "+user.getEmail());
	}
    // main_page.jsp
	return "main_page";
}

main_page.jsp(/ 루트 경로 요청 페이지)[로그인되었을 때랑 안되었을 때]

  • security 태그 라이브러리를 JSP 문서에 포함해 Spring Security 관련 태그 사용 가능 => <sec:>

  • form 태그를 사용할 때 기존에 사용하던 방법은 input에 hidden을 주고 csrf 토큰을 줘야했지만 csrfInput 태그를 사용하면 손쉽게 줄 수 있다. <sec:> 필수

  • csrfInput 태그 : CSRF 토큰을 전달하기 위한 태그
    => <sec:csrfInput/>

<%-- authorize 태그 : 권한을 비교하여 태그의 포함 여부를 설정하기 위한 태그 --%>
<%-- access 속성 : 권한(Role)을 속성값으로 설정 - SpEL 사용 가능 --%>
<%-- 비로그인 사용자인 경우 태그가 포함되도록 설정  --%>
<%-- isAnonymous(): Anonymous 사용자(인증 받지 않은 사용자)인 경우에만 접근 가능을 나타내는 표현식 --%>
<sec:authorize access="isAnonymous()">
	<h3><a href="<c:url value="/user_login"/>">로그인</a></h3>
</sec:authorize>
	
<%-- 로그인 사용자 정보(Authentication 객체의 Principal 필드값)Scope 속성값으로 
저장하여 사용 가능 --%>
<sec:authentication property="principal" var="loginUser"/>
	
<%-- 로그인 사용자인 경우 태그가 포함되도록 설정  --%>
<sec:authorize access="isAuthenticated()">
	<%-- authentication 태그 : Authentication 객체로 인증된 사용자 정보(UserDetails 객체)를
	제공하기 위한 태그 - 인증된 사용자에게 필요한 정보를 제공 --%>
	<%-- property 속성 : 인증된 사용자의 값이 저장된 필드의 이름을 속성값으로 설정 --%>
	<%-- <h3><sec:authentication property="principal.username"/>, 환영합니다.</h3> --%>
	<%-- UserDetails 인터페이스를 상속받은 클래스로 인증된 사용자 정보가 저장되어 있는 경우
	클래스의 필드값을 제공받아 사용 가능 --%>
	<%-- <h3><sec:authentication property="principal.userid"/>, 환영합니다.</h3> --%>
	<%-- <h3><sec:authentication property="principal.name"/>, 환영합니다.</h3> --%>
	<h3>${loginUser.name}, 환영합니다.</h3>
		
	<%-- 로그아웃 처리 기능을 제공하는 페이지는 반드시 form 태그를 사용해 요청 --%>
	<%-- => CSRF 토큰을 전달하기 위한 form 태그 사용 --%>
	<form action="<c:url value="/logout"/>" method="post">
		<%-- <input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }"> --%>
		<%-- csrfInput 태그 : CSRF 토큰을 전달하기 위한 태그 --%>
		<sec:csrfInput/>			
		<button type="submit">로그아웃</button>	
	</form>
</sec:authorize>

403에러 페이지 커스텀 마이징

security-context.xml

  • access-denied-handler: AccessDeniedException이 발생된 경우 403 에러코드 대신 에러 페이지로 응답 처리하기 위한 기능을 제공하는 엘리먼트

  • error-page 속성: 에러 메세지를 제공하기 위한 페이지의 경로를 속성값으로 설정

  • AccessDeniedException
    - Spring Security에서 인증된 사용자가 허락되지 않은 자원에 접근하려 할 때 발생하는 예외이다. 즉, 사용자가 시스템에 인증(로그인)되어 있지만, 해당 자원에 접근할 수 있는 권한이 부족한 경우에 발생한다.

<http>
	<access-denied-handler error-page="/access_denied"/>
</http>
  • ref 속성: 접근이 제한된 페이지를 요청할 경우 호출도리 메소드가 작성된 클래스의 Spring Bean(beanName)를 속성값으로 설정
<http>
	<access-denied-handler ref="customAccessDeniedHandler"/>
</http>

CustomAccessDeniedHandler.java

  • 접근이 제한된 페이지를 요청할 경우 호출될 메소드가 작성된 클래스

  • 계정 잠금 기능 활성화 등의 명령 작성

  • AccessDeniedHandler 인터페이스를 상속받아 작성

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
	//접근이 제한된 페이지를 요청할 경우 자동 호출되는 메소드
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		response.sendRedirect(request.getContextPath()+"/access_denied");
	}
}

LoginController.java

@RequestMapping(value = "/access_denied", method = RequestMethod.GET)
	public String accessDenied() {
		return "access_denied";
	}

access_denied.jsp

<body>
	<h1>에러페이지</h1>
	<hr>
	<h3 style="color: red;">권한이 없어 페이지 접근이 불가능합니다.<br>
	관리자에게 문의해 주세요.</h3>
	<hr>
	<h3><a href="<c:url value="/"/>">메인페이지</a></h3>
</body>

2024-09-24

자동 로그인

security-context.xml

  • remember-me: 자동 로그인 기능을 제공하기 위한 엘리먼트

  • 아이디와 비밀번호로 인증 처리하기 전에 [rememver-me]라는 이름의 쿠키를 검색하여 인증 처리되도록 설정

  • [remember-me]라는 이름의 쿠키에는 아이디와 비밀번호로 인증 성공할 경우 인증 관련 토큰을 자동 생성하여 쿠키값으로 저장 - 쿠키 유지 시간: 2주

  • token-validity-seconds 속성: 인증 관련 토큰의 유지시간(초)을 속성값으로 설정
    => 인증 관련 토큰을 쿠키에 저장하는 것은 보안상 위험하므로 persistent_logins 테이블을 생성해 인증 관련 토큰을 저장하여 사용할 수 있도록 설정하는 것을 권장

create table persistent_logins(
	username varchar2(100), 
    series varchar2(100) primary key, 
    token varchar2(100), 
    last_used timestamp
);
  • data-source-ref: DataSource 관련 클래스의 Spring Bean 식별자를 속성값으로 설정

  • 인증 성공 시 인증 관련 토큰을 발급받아 테이블의 행으로 자동 삽입하고 인증처리시 테이블의 행을 자동 검색해 사용

 <remember-me token-validity-seconds="604800" data-source-ref="dataSource"/>
  • 로그아웃 시에는 [remember-me] 쿠키 삭제
<logout logout-url="/logout" logout-success-url="/" 
invalidate-session="true" delete-cookies="JSESSIONID, remember-me"/>

form_login.jsp

  • 브라우저가 종료되어도 로그인이 유지되는 기능을 제공하기 위한 태그
<%-- => input 태그의 type 속성값을 [checkbox]로 설정하고 name 속성값을 [remember-me]로 설정 --%>
<input type="checkbox" name="remember-me">자동 로그인

다중 로그인 방지

security-context.xml

  • session-management: 사용자 정보 및 권한정보가 저장된 세션을 관리하기 위한 엘리먼트

  • concurrency-control: 세션의 허용 갯수를 설정하기 위한 엘리먼트
    => 다중 로그인 방지하기 위해 사용 - 동시 접속 차단

  • max-sessions 속성: 세션의 최대 허용 갯수를 속성값으로 설정
    => 세션의 최대 허용 갯수를 초과할 경우 기존 세션을 무효화 처리하고 새로운 세션에 사용자 정보 및 권한 정보를 저장해 사용

  • expired-url 속성: 세션의 최대 허용 갯수를 초과할 경우 요청할 페이지의 경로를 속성값으로 설정

  • error-if-maximum-exceeded 속성: false(기본값) 또는 true 중 하나를 속성값으로 설정
    => [false]로 설정하면 기존 세션을 무효화 처리하고 새로운 세션을 사용하지만 [true]로 설정하면 기존 세션을 유지하고 새로운 세션을 미생성하여 로그인 처리 불가능(원래는 잘 사용안함)

<session-management>
  <concurrency-control max-session="1" expired-url="/session_error"/>
</session-management>
profile
최선을 다해 꾸준히 노력하는 개발자 망고입니당 :D

0개의 댓글