이 페이지의 내용은 Spring Security 6 이상 버전을 기준으로 설명하고 있습니다.
•
Spring Security 6 이상
•
Spring Boot 3.x.x
•
MySQL
•
MyBatis
Spring Security 5.7 이상 : SecurityFilterChain 빈 등록 방식
Spring Security 5.7 미만 : WebSecurityConfigurerAdapter 상속 구현 방식
스프링 시큐리티 (Spring Security)
스프링 시큐리티는 스프링 기반 애플리케이션의 인증과 권한 부여를 담당하는 프레임워크입니다. 이를 사용하여 애플리케이션의 보안을 강화하고 사용자 인증, 접근 제어, 권한 관리 등을 구현할 수 있습니다. 스프링 시큐리티는 다양한 인증 방식과 보안 기능을 제공하며, 커스터마이징이 가능하여 다양한 요구사항에 맞게 사용할 수 있습니다.
1.
AuthenticationFilter:
•
AuthenticationFilter는 Spring Security 필터 체인에서 인증을 처리하는 역할을 담당합니다. 사용자가 인증 자격 증명(일반적으로 사용자 이름 및 비밀번호)을 제출하면 이 필터가 사용자를 인증합니다.
2.
UsernamePasswordAuthenticationToken:
•
UsernamePasswordAuthenticationToken은 사용자 이름과 비밀번호를 포함하는 Authentication의 구체적인 구현입니다. 주로 폼 기반 로그인에서 사용됩니다.
3.
AuthenticationManager:
•
AuthenticationManager는 실제로 사용자를 인증하는 인터페이스입니다. 주어진 Authentication 객체를 기반으로 사용자를 인증하고 Authentication 객체를 반환합니다.
4.
ProviderManager:
•
ProviderManager는 기본 AuthenticationManager 구현 중 하나로, 여러 개의 AuthenticationProvider를 관리하고 이를 사용하여 사용자를 인증합니다.
5.
AuthenticationProvider:
•
AuthenticationProvider는 실제로 인증을 수행하는 객체입니다. 사용자가 제출한 자격 증명을 검증하고 Authentication 객체를 반환합니다. Spring Security는 다양한 인증 프로바이더를 지원하며, 대표적으로 DaoAuthenticationProvider, LdapAuthenticationProvider, OpenIDAuthenticationProvider 등이 있습니다.
6.
UserDetailsService:
•
UserDetailsService는 사용자 정보를 로드하는 인터페이스입니다. 사용자의 식별자(일반적으로 사용자 이름)를 기반으로 사용자 정보를 가져옵니다.
7.
UserDetails:
•
UserDetails는 사용자의 정보를 담는 인터페이스로, 사용자 이름, 비밀번호, 권한 등의 정보를 포함합니다. 사용자 정보를 UserDetailsService로부터 로드할 때 사용됩니다.
8.
User:
•
User는 Spring Security에서 기본적으로 제공하는 UserDetails의 구현 클래스 중 하나입니다. 사용자의 기본 정보를 저장하며, 사용자 이름, 비밀번호, 권한을 포함합니다.
9.
SecurityContextHolder:
•
SecurityContextHolder는 현재 사용자의 SecurityContext를 관리하는 유틸리티 클래스입니다. 현재 사용자의 Authentication 객체를 저장하고 관리합니다.
10.
SecurityContext:
•
SecurityContext는 현재 사용자의 보안 정보를 저장하는 컨테이너입니다. SecurityContextHolder를 사용하여 현재 사용자의 SecurityContext를 설정하고 얻을 수 있습니다.
11.
Authentication 클래스:
•
Authentication 클래스는 Spring Security에서 사용자의 인증 정보와 권한 정보를 포함하는 클래스입니다. Authentication 객체를 통해 현재 사용자의 인증 상태 및 권한을 확인할 수 있습니다.
스프링 시큐리티 요청 흐름
1.
사용자가 웹 애플리케이션에 로그인을 시도합니다.
2.
로그인 폼에서 사용자가 사용자 이름과 비밀번호를 제출하면, 이 정보는 Spring Security AuthenticationFilter로 전달됩니다.
3.
AuthenticationFilter는 UsernamePasswordAuthenticationToken을 생성하고, 사용자 이름과 비밀번호를 이 토큰에 포함시킵니다.
4.
이 토큰은 AuthenticationManager로 전달됩니다. AuthenticationManager는 ProviderManager를 포함하며, 다양한 AuthenticationProvider를 관리합니다.
5.
ProviderManager는 등록된 AuthenticationProvider들을 순회하면서 토큰을 검증하는 시도를 합니다. 각 AuthenticationProvider는 다양한 방식으로 사용자를 인증할 수 있습니다. 일반적으로 사용자 이름/비밀번호를 검증하는 DaoAuthenticationProvider를 사용합니다.
6.
DaoAuthenticationProvider는 사용자 이름을 기반으로 데이터베이스 또는 다른 저장소에서 사용자 정보를 가져오고, 비밀번호 검증을 수행합니다. 만약 사용자가 유효하면 Authentication 객체를 생성하고 반환합니다.
7.
AuthenticationManager는 검증된 Authentication 객체를 다시 AuthenticationFilter로 반환합니다.
8.
AuthenticationFilter는 사용자의 Authentication 객체를 SecurityContextHolder에 저장합니다. 이렇게 하면 현재 사용자의 보안 컨텍스트가 설정됩니다.
9.
사용자가 인증된 후, Spring Security는 요청을 계속 진행시킵니다. 사용자는 해당 리소스에 대한 권한이 있는지 확인하기 위해 권한 부여(Authorization) 단계로 이동하게 됩니다.
10.
요청된 리소스에 액세스할 때, Spring Security는 @PreAuthorize, @PostAuthorize, @Secured 등과 같은 애노테이션을 사용하여 메서드 레벨에서 권한을 확인하고, 사용자에게 액세스 권한이 있는지 확인합니다.
11.
요청된 작업이 사용자에게 허용된 경우, 리소스에 대한 요청은 성공하고 응답이 클라이언트에게 반환됩니다.
12.
만약 권한이 없는 경우, 액세스가 거부되며, 클라이언트는 적절한 오류 메시지나 리디렉션을 받게 됩니다.
스프링 시큐리티 설정
•
의존성 설정
•
기본 설정
의존성 설정
•
build.gradle (spring security 6)
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.aloha'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
SQL
복사
기본 설정
아무 설정 없이 접속하게 되면, 인증을 하기 위해서, login 페이지로 이동합니다.
그리고, 스프링 시큐리티를 포함한 스프링 부트 프로젝트가 실행될 때는, 개발용으로만 제공되는 “generated security password” 가 로그에 출력됩니다. 기본 아이디(username)와 기본 패스워드(generated password) 를 사용하여 로그인 인증 테스트를 할 수 있습니다.
•
기본 아이디 : user
•
기본 패스워드 : Using generated security password : ??????????
기본 아이디와 기본 패스워드를 입력해봅니다.
인증이 성공하고 나면, 아래와 같이 메인 페이지로 이동합니다.
스프링 시큐리티의 기능
•
인증 (Authentication):
•
인가; 권한 관리 (Authorization):
•
CSRF (Cross-Site Request Forgery) 방어
•
사용자 지정 필터 및 이벤트 핸들러
•
자동 로그인 (Remember-Me)
인증 & 인가
인증 (Authentication)
스프링 시큐리티에서 인증은 등록된 사용자인지 확인하여 입증하는 과정입니다. 사용자는 기본 아이디와 기본 패스워드를 사용하여 로그인하여 인증을 테스트할 수 있습니다.
인가 (Authorization)
스프링 시큐리티에서 인가는 사용자의 권한을 확인하여 권한에 따라 자원의 사용범위를 구분하여 허락하는 과정입니다.
스프링 시큐리티 주요 인증 방식
•
인메모리 인증 방식
•
JDBC 인증 방식
•
사용자 정의 인증 방식
@Configuration // 스프링 빈 설정 클래스로 지정
@EnableWebSecurity // 스프링 시큐리티 설정 빈으로 등록
public class SecurityConfig {
// 👮♂️🔐사용자 인증 관리 메소드
@Bean
public UserDetailsService userDetailsService() {
// return new InMemoryUserDetailsManager( ... );
// return new JdbcUserDetailsManager( ... );
}
}
Java
복사
이 메소드는 Spring Security에서 사용자 인증을 설정하고 구성하는 역할을 합니다.
@EnableWebSecurity
이 어노테이션은 해당 클래스를 스프링 시큐리티 설정용 빈으로 등록하며, 스프링 부트 애플리케이션에서 자동으로 감지됩니다.
인메모리 인증 방식
•
WebConfig.java
@Configuration // 빈 등록 설정 클래스 지정
public class WebConfig {
@Bean // 빈 등록
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
// return NoOpPasswordEncoder.getInstance();
// BCryptPasswordEncoder : BCrypt 해시 알고리즘을 사용하여 비밀번호 암호화
// NoOpPasswordEncoder : 암호화 없이 비밀번호를 저장
// ...
}
}
Java
복사
SecurityConfig.java 에서 의존 주입받고 있는 PasswordEncoder 에 대한 빈 등록을 합니다. 이때 NoOpPasswordEncoder 구현 클래스로 지정하면 비밀번호를 암호화하지 않고, BCryptPasswordEncoder 구현 클래스를 지정하면 Bcrypt 해시 알고리즘을 사용하여 비밀번호를 암호화합니다.
•
SecurityConfig.java
@EnableWebSecurity // 해당 클래스를 스프링 시큐리티 설정 빈으로 등록
public class SecurityConfig {
@Autowired
private PasswordEncoder passwordEncoder; // 비밀번호 암호화 객체
// 🔐 스프링 시큐리티 설정 메소드
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 인가 설정
http.authorizeRequests(requests -> requests
.antMatchers("/admin", "/admin/**").hasRole("ADMIN")
.antMatchers("/user", "/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/css/**", "/js/**", "/img/**").permitAll()
.antMatchers("/**").permitAll()
.anyRequest().authenticated())
;
// 로그인 설정
http.formLogin( login ->login.permitAll() );
// 로그아웃 설정
http.logout( logout -> logout.permitAll() );
return http.build();
}
// 인메모리 방식 인증
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.builder()
.username("admin") // 사용자 이름
// .password("{noop}123456") // 비밀번호 (noop: 평문 처리)
.password( passwordEncoder.encode("123456") )
.roles("ADMIN") // ROLE_ADMIN 권한
.build();
UserDetails user = User.builder()
.username("user")
// .password("{noop}123456")
.password( passwordEncoder.encode("123456") )
.roles("USER") // ROLE_USER 권한
.build();
// 인메모리 방식 인증
return new InMemoryUserDetailsManager(admin, user);
}
}
Java
복사
new InMemoryUserDetailsManager(admin, user)
인메모리 방식으로 사용자 인증 설정을 시작합니다.
admin 관리자, user 사용자 계정을 메모리에 등록합니다.
password(passwordEncoder.encode("123456"))
사용자 "user"의 패스워드를 설정하고, passwordEncoder.encode()를 사용하여 패스워드를 암호화합니다.
roles("USER")
사용자 "user"에게 "USER" 권한을 부여합니다.
ROLE_ 스프링 시큐리티 권한 접두사
Spring Security에서는 기본적으로 "ROLE_" 접두사를 자동으로 추가하여 권한을 관리합니다. 이것은 Spring Security의 표준 규칙 중 하나이며, 사용자에게 부여된 권한을 확인할 때 "ROLE_" 접두사가 필요합니다.
프로젝트 설정
application.properties
# 데이터 소스 - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/aloha?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false&autoReconnection=true&autoReconnection=true
spring.datasource.username=aloha
spring.datasource.password=123456
# Mybatis 설정
# Mybatis 설정 경로 : ~/resources/mybatis-config.xml
# mybatis.config-location=classpath:mybatis-config.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.aloha.security6.domain
# Mybatis 매퍼 파일 경로 : ~/메인패키지/mapper/**Mapper.xml
mybatis.mapper-locations=classpath:mybatis/mapper/**/**.xml
Plain Text
복사
JDBC 인증 방식
JDBC 방식으로 인증하기 위해서, 데이터베이스에 사용자 테이블과 권한 테이블을 생성합니다.
•
user
•
user_auth
사용자(회원) 테이블
-- user
CREATE TABLE `user` (
`NO` bigint NOT NULL AUTO_INCREMENT,
`USERNAME` varchar(100) NOT NULL,
`PASSWORD` varchar(200) NOT NULL,
`NAME` varchar(100) NOT NULL,
`EMAIL` varchar(200) DEFAULT NULL,
`CREATED_AT` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`UPDATED_AT` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ENABLED` int DEFAULT 1,
PRIMARY KEY (`NO`)
) COMMENT='회원';
SQL
복사
권한 테이블
-- user_auth
CREATE TABLE `user_auth` (
no bigint NOT NULL AUTO_INCREMENT -- 권한번호
, username varchar(100) NOT NULL -- 아이디
, auth varchar(100) NOT NULL -- 권한 (ROLE_USER, ROLE_ADMIN, ...)
, PRIMARY KEY(no)
);
SQL
복사
사용자 및 권한 기본 데이터
-- 기본 데이터
-- NoOpPasswordEncoder - 암호화 없이 로그인
-- 사용자
INSERT INTO user ( username, password, name, email )
VALUES ( 'user', '123456', '사용자', 'user@mail.com' );
-- 관리자
INSERT INTO user ( username, password, name, email )
VALUES ( 'admin', '123456', '관리자', 'admin@mail.com' );
-- BCryptPasswordEncoder - 암호화 시
-- 사용자
INSERT INTO user ( username, password, name, email )
VALUES ( 'user', '$2a$12$TrN..KcVjciCiz.5Vj96YOBljeVTTGJ9AUKmtfbGpgc9hmC7BxQ92', '사용자', 'user@mail.com' );
-- 관리자
INSERT INTO user ( username, password, name, email )
VALUES ( 'admin', '$2a$12$TrN..KcVjciCiz.5Vj96YOBljeVTTGJ9AUKmtfbGpgc9hmC7BxQ92', '관리자', 'admin@mail.com' );
SQL
복사
-- 권한
-- 사용자
-- * 권한 : ROLE_USER
INSERT INTO user_auth ( username, auth )
VALUES ( 'user', 'ROLE_USER' );
-- 관리자
-- * 권한 : ROLE_USER, ROLE_ADMIN
INSERT INTO user_auth ( username, auth )
VALUES ( 'admin', 'ROLE_USER' );
INSERT INTO user_auth ( username, auth )
VALUES ( 'admin', 'ROLE_ADMIN' );
SQL
복사
참고로, 비밀번호를 Bcrypt 알고리즘으로 암호화해보고 싶다면, 아래 사이트에서 할 수 있다.
•
SecurityConfig.java
@EnableWebSecurity // 해당 클래스를 스프링 시큐리티 설정 빈으로 등록
public class SecurityConfig {
@Autowired
private PasswordEncoder passwordEncoder; // 비밀번호 암호화 객체
@Autowired
private DataSource dataSource; // application.properites 에 정의한 데이터 소스를 가져오는 객체
// 🔐 스프링 시큐리티 설정 메소드
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 인가 설정
http.authorizeRequests(requests -> requests
.antMatchers("/admin", "/admin/**").hasRole("ADMIN")
.antMatchers("/user", "/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/css/**", "/js/**", "/img/**").permitAll()
.antMatchers("/**").permitAll()
.anyRequest().authenticated())
;
// 로그인 설정
http.formLogin( login ->login.permitAll() );
// 로그아웃 설정
http.logout( logout -> logout.permitAll() );
return http.build();
}
// JDBC 인증 방식
// ✅ 데이터 소스 (URL, ID, PW) - application.properties
// ✅ SQL 쿼리 등록
// ⭐ 사용자 인증 쿼리
// ⭐ 사용자 권한 쿼리
@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
// 사용자 인증 쿼리
String sql1 = " SELECT username as username, password as password, enabled "
+ " FROM user "
+ " WHERE username = ? "
;
// 사용자 권한 쿼리
String sql2 = " SELECT username as username, auth "
+ " FROM user_auth "
+ " WHERE username = ? "
;
userDetailsManager.setUsersByUsernameQuery(sql1);
userDetailsManager.setAuthoritiesByUsernameQuery(sql2);
return userDetailsManager;
}
}
Java
복사
•
application.properties
# 데이터 소스 - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/aloha?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false&autoReconnection=true&autoReconnection=true
spring.datasource.username=aloha
spring.datasource.password=123456
SQL
복사
user 로 로그인
메인 페이지(/)에 접속 (ID : user)
사용자 페이지(/user)에 접속 (ID : user)
관리자 페이지(/admin)에 접속 (ID : user)
admin 으로 로그인
사용자 페이지(/user)에 접속 (ID : user)
사용자 페이지(/user)에 접속 (ID : admin)
관리자 페이지(/admin)에 접속 (ID : admin)
사용자 정의 인증 방식
Mybatis 나, JPA 등의 데이터 접근 로직을 로그인 사용자 인증과 연결하여 인증되도록 스프링 시큐리티 설정을 지정하는 방식이다.
따라서, Mybatis 나 JPA 등으로 사용자 인증 및 권한 조회를 위한 데이터베이스 접근 로직을 먼저 작성한다.
•
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aloha.security6.mapper.UserMapper">
<resultMap type="Users" id="userMap">
<id property="no" column="no" />
<result property="no" column="no" />
<result property="username" column="username" />
<result property="password" column="password" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="enabled" column="enabled" />
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
<collection property="authList" resultMap="authMap"></collection>
</resultMap>
<resultMap type="UserAuth" id="authMap">
<result property="no" column="no" />
<result property="username" column="username" />
<result property="auth" column="auth" />
</resultMap>
<!-- 회원 조회 - id -->
<select id="login" resultMap="userMap">
SELECT u.no
,u.username
,password
,name
,email
,enabled
,created_at
,updated_at
,auth
FROM user u LEFT OUTER JOIN user_auth auth ON u.username = auth.username
WHERE u.username = #{username}
</select>
</mapper>
Java
복사
resultMap
SQL 쿼리의 결과 집합을 자바 객체로 매핑하는 방법을 정의하는 XML 태그
•
type 속성: ResultMap이 매핑할 자바 객체의 클래스를 지정합니다.
•
id 속성: ResultMap의 고유 식별자입니다.
<resultMap type="패키지명.클래스명" id="아이디">
<id property="PK변수명" column="PK컬럼명" />
<result property="변수명1" column="컬럼명1" />
<result property="변수명2" column="컬럼명2" />
...
<collection property="변수명10" resultMap="매핑된resultMap의 ID"></collection>
</resultMap>
Java
복사
•
id : 객체와 DB 의 PK(기본키) 를 지정하는 태그
◦
property : 매핑할 객체의 변수명
◦
column : 매핑할 DB 테이블의 컬럼명
•
result : 매핑할 객체의 변수명과 DB 테이블의 컬럼명을 지정하는 태그
◦
property : 매핑할 객체의 변수명
◦
column : 매핑할 DB 테이블의 컬럼명
collection
컬렉션 속성에 대한 매핑 정보를 지정하는 태그
@Data
public class Users {
...
List<UserAuth> authList;
}
Java
복사
위와 같이 클래스의 멤버변수로 컬렉션(리스트)가 있다면, 조인 쿼리 등으로 1:N 의 데이터를 매핑할 수 있다.
<collection property="authList" resultMap="authMap"></collection>
XML
복사
•
UserMapper.java
@Mapper
public interface UserMapper {
// 사용자 인증(로그인) - id
public Users login(String username);
}
Java
복사
•
CustomUserDetailsService.java
/**
* UserDetailsService
* : Spring Security에서 사용자 정보를 데이터베이스에서 가져와서,
* 사용자 인증을 수행하기 위한 인터페이스
* * 위 인터페이스를 구현하여 loadUserByUsername() 재정의하면,
* * 데이터베이스나 다른 소스로부터 사용자 인증정보를 가져와서 스프링 시큐리티에 전달해줄 수 있다.
*/
@Slf4j
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 사용자 정의 사용자 인증 메소드
* UserDetails
* ➡ Users
* ⬆ CustomUser
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username: " + username);
Users users = userMapper.login(username);
log.info("users : " + users);
CustomUser customUser = null;
if( users != null )
customUser = new CustomUser(users);
return customUser;
}
}
Java
복사
스프링 시큐리티의 UserDetailsService를 구현하여 사용자 상세 정보를 얻어오는 메서드를 재정의한다. 이 작업을 통해, 데이터베이스나 다른 소스로부터 사용자 인증정보를 가져와서 스프링 시큐리티에 전달해줄 수 있다.
•
SecurityConfig.java
@EnableWebSecurity // 해당 클래스를 스프링 시큐리티 설정 빈으로 등록
public class SecurityConfig {
@Autowired
private PasswordEncoder passwordEncoder; // 비밀번호 암호화 객체
@Autowired
private DataSource dataSource; // application.properites 에 정의한 데이터 소스를 가져오는 객체
@Autowired
private CustomDetailsService customDetailsService;
// 🔐 스프링 시큐리티 설정 메소드
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 인가 설정
http.authorizeRequests(requests -> requests
.antMatchers("/admin", "/admin/**").hasRole("ADMIN")
.antMatchers("/user", "/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/css/**", "/js/**", "/img/**").permitAll()
.antMatchers("/**").permitAll()
.anyRequest().authenticated())
;
// 로그인 설정
http.formLogin( login ->login.permitAll() );
// 사용자 정의 인증 방식
http.userDetailsService(customDetailsService);
// 로그아웃 설정
http.logout( logout -> logout.permitAll() );
return http.build();
}
}
Java
복사
userDetailsService() 메소드에 사용자 정의 인증 구현 클래스를 지정해주고 passwordEncoder 를 지정해준다.
•
WebConfig.java
@Configuration // 빈 등록 설정 클래스 지정
public class WebConfig {
@Bean // 빈 등록
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
// return NoOpPasswordEncoder.getInstance();
// BCryptPasswordEncoder : BCrypt 해시 알고리즘을 사용하여 비밀번호 암호화
// NoOpPasswordEncoder : 암호화 없이 비밀번호를 저장
// ...
}
}
Java
복사
CSRF (Cross-Site Request Forgery)
세션의 등록된 인증된 사용자의 정보를 다른 악의적이 사이트에서 탈취하여, 악의적인 요청을 하는데, 마치 인증된 사용자가 요청을 보낸 것처럼 요청을 위조하는 웹 보안 공격
CSRF 공격 원리
1.
희생자는 은행 웹 사이트에 로그인합니다. 로그인 후 세션 토큰(세션 ID)이 생성됩니다.
2.
희생자는 악의적인 웹 사이트를 방문합니다. 이 웹 사이트에는 CSRF 공격을 실행하는 악의적인 코드가 포함되어 있습니다.
3.
희생자가 악의적인 웹 사이트를 방문하면, 이 웹 사이트는 은행 웹 사이트로 희생자 대신 요청을 보내도록 브라우저를 조작합니다. 이 요청에는 희생자의 인증 정보(세션 토큰)이 포함됩니다.
4.
은행 웹 사이트는 브라우저로부터 받은 요청을 처리하고, 이 요청이 유효한 것처럼 인식합니다. 결과적으로 은행 계좌 이체 또는 다른 민감한 작업이 이루어질 수 있습니다.
CSRF 방어
스프링 시큐리티에는 기본적으로 CSRF 방지 기능이 활성화 되어 있습니다.
CSRF 공격 원리에서 알 수 있듯이, CSRF 공격은 사이트간 요청 위조를 통해 발생하는 웹 보안 공격입니다. 따라서, CSRF 공격을 방어 하기 위해서는 해당 요청이 타 사이트에서의 요청이 아니라, 현재 사이트와 연결된 유효한 요청이라는 것을 확인해야합니다.
이를 위해서, 서버 측에서 응답한 뷰(html) 를 통해 요청한 것인지 확인하기 위해서, 서버는 CSRF 토큰을 발행합니다. 이 CSRF 토큰을 전달 받은 클라이언트가 다음 요청 시에 CSRF 토큰과 함께 요청을 보내면 스프링 시큐리티에서 이를 인식하여 유효한 요청으로 인식합니다.
스프링 시큐리티 의존성을 포함한 후, POST 방식으로 Form 요청을 보내면 403 Forbidden 상태코드를 응답받게 됩니다. 403 상태코드는 서버의 자원에 대한 권한이 없어 요청이 거부되었음을 나타내는 상태코드 입니다. 이렇게 응답되는 이유는 스프링 시큐리티에 CSRF 방지 기능이 기본으로 활성화 되어 있고, 유효한 요청을 증명하기 위한 CSRF 토큰과 함께 요청하지 않았기 때문입니다.
따라서, 스프링 시큐리티 CSRF 방지 기능이 활성화 되어있는 상태에서 요청을 보낼 때에는, 항상 CSRF 토큰을 포함하여 요청을 해야합니다.
CSRF Token
<!-- CSRF 토큰 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
HTML
복사
CSRF Token 을 Form 에 포함한 예시
<form method="post" action="/submit">
<!-- CSRF 토큰 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- 다른 입력 필드들 -->
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">로그인</button>
</form>
HTML
복사
Thymeleaf 템플릿을 사용하는 경우, Spring Security는 자동으로 CSRF 토큰을 Form에 삽입합니다.
•
SecurityConfig.java
@EnableWebSecurity // 해당 클래스를 스프링 시큐리티 설정 빈으로 등록
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder; // 비밀번호 암호화 객체
@Autowired
private DataSource dataSource; // application.properites 에 정의한 데이터 소스를 가져오는 객체
@Autowired
private CustomDetailsService customDetailsService;
// 🔐 스프링 시큐리티 설정 메소드
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 인가 설정
http.authorizeRequests(requests -> requests
.antMatchers("/admin", "/admin/**").hasRole("ADMIN")
.antMatchers("/user", "/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/css/**", "/js/**", "/img/**").permitAll()
.antMatchers("/**").permitAll()
.anyRequest().authenticated())
;
// 로그인 설정
http.formLogin( login ->login.permitAll() );
// 사용자 정의 인증 방식
http.userDetailsService(customDetailsService);
// 로그아웃 설정
http.logout( logout -> logout.permitAll() );
// CSRF 비활성화
http.csrf().disabled();
return http.build();
}
}
Java
복사
CSRF 방지 기능은 기본적으로 활성화 되어 있고, 개발 모드에서 비활성화하고 싶다면, http.csrf().disable(); 메소드를 호출하여 비활성화 할 수 있다.
자동 로그인 - RememberMe
로그인 시, 자동 로그인 여부를 스프링 시큐리티에 전달하고, 세션(브라우저) 종료 후 다시 접속할 때, 쿠키에 저장된 인증 토큰을 이용해서 스프링 시큐리티가 persistent_logins 테이블과 인증토큰을 매칭하여 사용자 인증을 자동으로 해주는 기능
•
자동 로그인 테이블 정의 - persistent_logins
•
PersistentRepository 토큰 정보 객체 빈 등록
•
자동 로그인 설정 - rememberMe
•
Form 요청 remember-me 요청 파라미터를 포함하여 로그인
자동 로그인 정보 테이블
create table persistent_logins (
username varchar(64) not null
, series varchar(64) primary key
, token varchar(64) not null
, last_used timestamp not null)
;
SQL
복사
데이터베이스에 자동 로그인 테이블을 생성해준다.
자동 로그인 설정 - rememberMe
•
PersistentRepository 토큰정보 객체 빈 등록
•
SecurityConfig.java
@EnableWebSecurity // 해당 클래스를 스프링 시큐리티 설정 빈으로 등록
public class SecurityConfig {
@Autowired
private PasswordEncoder passwordEncoder; // 비밀번호 암호화 객체
@Autowired
private DataSource dataSource; // application.properites 에 정의한 데이터 소스를 가져오는 객체
// 🔐 스프링 시큐리티 설정 메소드
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 인가 설정
http.authorizeRequests(requests -> requests
.antMatchers("/admin", "/admin/**").hasRole("ADMIN")
.antMatchers("/user", "/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/css/**", "/js/**", "/img/**").permitAll()
.antMatchers("/**").permitAll()
.anyRequest().authenticated())
;
// 로그인 설정
http.formLogin( login ->login.permitAll() );
// 사용자 정의 인증 방식
http.userDetailsService(customDetailsService);
// 로그아웃 설정
http.logout( logout -> logout.permitAll() );
// 자동 로그인
http.rememberMe()
.key("aloha")
.tokenRepository( tokenRepository() )
.tokenValiditySeconds( 60 * 60 * 24 * 7 );
// CSRF 비활성화
http.csrf().disabled();
return http.build();
}
// PersistentRepository 토큰정보 객체 - 빈 등록
@Bean
public PersistentTokenRepository tokenRepository() {
// JdbcTokenRepositoryImpl : 토큰 저장 데이터 베이스를 등록하는 객체
JdbcTokenRepositoryImpl repositoryImpl = new JdbcTokenRepositoryImpl();
// 토큰 저장소를 사용하는 데이터 소스 지정
repositoryImpl.setDataSource(dataSource);
// persistent_logins 테이블 자동 생성
// repositoryImpl.setCreateTableOnStartup(true);
try {
repositoryImpl.getJdbcTemplate().execute(JdbcTokenRepositoryImpl.CREATE_TABLE_SQL);
} catch (Exception e) {
log.error("persistent_logins 테이블이 이미 생성되었습니다.");
}
return repositoryImpl;
}
}
Java
복사
Form 요청 remeber-me 요청 파라미터를 포함하여 로그인
•
login.html
<form action="/login" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="text" name="username" placeholder="아이디"> <br>
<input type="password" name="pw" placeholder="비밀번호"> <br>
<!-- 자동로그인(Remember-Md) 기능 -->
<!-- remember-me 요청 파라미터로 자동 로그인 여부를 전달 -->
<input type="checkbox" name="remember-me" id="remember-me">
<label for="remember-me">자동 로그인</label> <br>
</form>
HTML
복사
아래의 remember-me 요청 파라미터를 포함하여 로그인 요청을 보낸다.
<input type="checkbox" name="remember-me" id="remember-me">
HTML
복사