1. SecurityContextPersistenceFilter이다. 요청이 들어오면 SecurityContext 객체를 생성, 저장, 조회한다. 새로운 SecurityContext를 SecurityContextHolder에 저장한다.
2. LogoutFilter이다. 사용자의 로그아웃 요청을 처리한다. 설정된 로그아웃 URL로 오는 요청을 감시하여 해당 사용자를 로그아웃 처리한다.
3. UsernamePasswordAuthenticationFilter이다. 설정된 로그인 URL로 오는 요청을 감시하며, 사용자 인증을 처리한다. 인증 실패 시 AuthenticationFailureHandler를 실행한다.
4. DefaultLoginPageGeneratingFilter이다. 사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 기본 로그인 페이지를 생성한다.
5. BasicAuthenticationFilter이다. HTTP 요청의 Basic 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장한다.
6. RememberMeAuthenticationFilter이다. SecurityContext에 인증 객체가 있는지 확인하고, RememberMe 인증 토큰으로 컨텍스트에 주입한다.
7. AnonymousAuthenticationFilter이다. SecurityContextHolder에 인증 객체가 있는지 확인하고, 필요한 경우 Authentication 객체를 주입한다.
8. SessionManagementFilter이다. 인증된 사용자의 세션을 관리한다. 세션 고정 보호, 동시 로그인 확인 등의 세션 관련 활동을 수행한다.
9. ExceptionTranslationFilter이다. 필터 체인 내에서 발생하는 모든 보안 관련 예외(AccessDeniedException, AuthenticationException)를 처리한다.
10. FilterSecurityInterceptor이다. HTTP 리소스의 보안 처리를 수행한다. 접근 권한을 확인하고 권한이 없는 경우 예외를 발생시킨다.
11. 사용자 접근 권한이 업데이트되면 해당 업데이트 정보를 SecurityContext Repository에 업데이트한다.
이후 SecurityContextPersistenceFilter 로 사용자의 접근이 들어오면 해당 Repository를 조회하여 사용자의 최신 권한 정보를 가져와 인증 과정에 반영한다
파일 구성
우선 Spring Boot 프로젝트의 build.gradle
또는 pom.xml
파일에 아래와 같은 의존성을 추가
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
implementation 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.springframework.security:spring-security-core'
implementation 'org.springframework.security:spring-security-config'
implementation 'org.springframework.security:spring-security-web'
implementation 'org.springframework.boot:spring-boot-starter-logging'
}
application.yml
파일에 MySQL 데이터베이스 설정을 추가
spring:
datasource:
url: jdbc:mysql://localhost:3306/securitydb
username: yourUsername
password: yourPassword
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: org.scoula.security.account.domain
MySQL에 사용자 정보와 권한 정보를 담을 테이블을 생성
tbl_member
테이블은 사용자 정보를 저장하고, tbl_member_auth
는 사용자 권한을 관리한다.
CREATE TABLE tbl_member (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(128) NOT NULL,
email VARCHAR(50) NOT NULL,
reg_date DATETIME DEFAULT CURRENT_TIMESTAMP,
update_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE tbl_member_auth (
username VARCHAR(50) NOT NULL,
auth VARCHAR(50) NOT NULL,
PRIMARY KEY(username, auth),
FOREIGN KEY (username) REFERENCES tbl_member(username)
);
권한 정보를 담는 클래스
package org.scoula.security.account.domain;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
@Data
public class AuthVO implements GrantedAuthority {
private String username;
private String auth;
@Override
public String getAuthority() {
return auth;
}
}
사용자 정보를 담는 클래스
package org.scoula.security.account.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberVO {
private String username;
private String password;
private String email;
private Date regDate;
private Date updateDate;
private List<AuthVO> authList; // 권한 목록
}
MyBatis 설정 파일로, 데이터베이스 테이블과 VO 클래스 간 매핑 설정을 작성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"<http://mybatis.org/dtd/mybatis-3-config.dtd>">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="org.scoula.security.account.domain"/>
</typeAliases>
</configuration>
사용자 정보와 권한을 가져오는 SQL 매퍼
<?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="org.scoula.security.account.mapper.UserDetailsMapper">
<resultMap id="authMap" type="AuthVO">
<result property="username" column="username"/>
<result property="auth" column="auth"/>
</resultMap>
<resultMap id="memberMap" type="MemberVO">
<id property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="regDate" column="reg_date"/>
<result property="updateDate" column="update_date"/>
<collection property="authList" resultMap="authMap"/>
</resultMap>
<select id="get" resultMap="memberMap">
SELECT m.username, password, email, reg_date, update_date, auth
FROM tbl_member m
LEFT OUTER JOIN tbl_member_auth a ON m.username = a.username
WHERE m.username = #{username}
</select>
</mapper>
Spring Security 설정 파일
사용자 권한 및 인증 절차를 정의
package org.scoula.security.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@Log4j
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/security/all").permitAll()
.antMatchers("/security/admin").hasRole("ADMIN")
.antMatchers("/security/member").hasRole("MEMBER")
.and()
.formLogin().loginPage("/security/login").defaultSuccessUrl("/")
.and()
.logout().logoutSuccessUrl("/security/logout")
.and()
.csrf().disable(); // 개발 시엔 csrf를 임시로 disable
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
사용자 정보를 관리하는 UserDetails
를 확장한 클래스
package org.scoula.security.account.domain;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
@Getter
@Setter
public class CustomUser extends User {
private MemberVO member;
public CustomUser(MemberVO vo) {
super(vo.getUsername(), vo.getPassword(), vo.getAuthList());
this.member = vo;
}
}
UserDetailsService
인터페이스를 구현하여 사용자 정보를 가져오는 클래스
package org.scoula.security.service;
import lombok.RequiredArgsConstructor;
import org.scoula.security.account.domain.CustomUser;
import org.scoula.security.account.domain.MemberVO;
import org.scoula.security.account.mapper.UserDetailsMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserDetailsMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MemberVO vo = mapper.get(username);
if (vo == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
return new CustomUser(vo);
}
}
로그인 및 권한 기반 페이지 접근을 처리하는 컨트롤러
package org.scoula.controller;
import lombok.extern.log4j.Log4j;
import org.scoula.security.account.domain.CustomUser;
import org.scoula.security.account.domain.MemberVO;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/security")
@Log4j
public class SecurityController {
@GetMapping("/all")
public String doAll() {
log.info("모든 사용자 접근 가능");
return "all";
}
@GetMapping("/member")
public String doMember(@AuthenticationPrincipal CustomUser customUser) {
MemberVO member = customUser.getMember();
log.info("멤버 페이지 접근 - 사용자 정보: " + member.getUsername());
return "member";
}
@GetMapping("/admin")
public String doAdmin(@AuthenticationPrincipal CustomUser customUser) {
MemberVO member = customUser.getMember();
log.info("관리자 페이지 접근 - 사용자 정보: " + member.getUsername());
return "admin";
}
@GetMapping("/login")
public String login() {
log.info("로그인 페이지");
return "login";
}
@GetMapping("/logout")
public String logout() {
log.info("로그아웃 페이지");
return "logout";
}
}
/all
: 모든 사용자에게 접근 허용된 페이지./member
: 멤버 권한을 가진 사용자만 접근 가능한 페이지./admin
: 관리자 권한을 가진 사용자만 접근 가능한 페이지./login
: 사용자 로그인 페이지./logout
: 사용자 로그아웃 페이지.사용자가 로그인할 수 있는 페이지
<!DOCTYPE html>
<html xmlns:th="<http://www.thymeleaf.org>">
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
<h1>로그인 페이지</h1>
<form th:action="@{/security/login}" method="post">
<label>아이디: <input type="text" name="username"></label><br>
<label>비밀번호: <input type="password" name="password"></label><br>
<button type="submit">로그인</button>
</form>
</body>
</html>
멤버 권한 사용자 전용 페이지
<!DOCTYPE html>
<html xmlns:th="<http://www.thymeleaf.org>">
<head>
<meta charset="UTF-8">
<title>멤버 페이지</title>
</head>
<body>
<h1>멤버 전용 페이지</h1>
<p>환영합니다, 멤버님!</p>
<a th:href="@{/security/logout}">로그아웃</a>
</body>
</html>
관리자 권한 사용자 전용 페이지
<!DOCTYPE html>
<html xmlns:th="<http://www.thymeleaf.org>">
<head>
<meta charset="UTF-8">
<title>관리자 페이지</title>
</head>
<body>
<h1>관리자 전용 페이지</h1>
<p>환영합니다, 관리자님!</p>
<a th:href="@{/security/logout}">로그아웃</a>
</body>
</html>
이제 모든 설정이 완료되었습니다. 프로젝트를 실행하고 다음 단계를 통해 동작을 확인
SpringBootApplication
클래스를 실행하여 서버를 실행/security/login
으로 이동하여 로그인 페이지가 정상적으로 표시되는지 확인/security/all
: 모든 사용자 접근 가능./security/member
: 로그인 후 멤버 권한 사용자만 접근 가능./security/admin
: 로그인 후 관리자 권한 사용자만 접근 가능.