스프링 시큐리티 프로엠워크를 활용하여 인증/인가를 구현하고 회원 정보 저장(영속성)은 MySQL 데이터베이스를 활용하여 구현한다.
스프링부트 애플리케이션은 서블릿 컨테이너라는 톰캣 서블릿 컨테이너 위에 존재하는데, 클라이언트에게서 요청이 오면, 먼저 서블릿 컨테이너가 요청을 받아 필터를 거친 후에 스프링부트 컨트롤러에 요청이 도착한다. 스프링 시큐리티는 Security Config라는 자바 컨피르 파일을 등록해두면 이 컨피그 파일이 특정한 Filter를 만들어 클라이언트의 요청을 가로챈다. 이후 클라이언트가 가고싶어하는 목적지 이전에 해당 클라이언트가 특정한 권한을 가지고 있는지 분석한다.
만약 로그인을 진행해야 하는경우, 필터에서 모든 유저에 대해 접근을 허용하여 로그인 컨트롤러에서 로그인을 진행한 후, 세션에 로그인 정보를 등록한다. 이후 특정 컨트롤러에 접근할 때는 시큐리티 컨피그가 세션에 등록되어있는 유저 정보를 통해 필터를 통과시키고 특정 페이지에 접근하도록 돕는다.
Spring Web
Lombok
Mustache
Spring Security
Spring Data JPA
MySQL Driver
package com.example.testsecurity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MainController {
@GetMapping("/")
public String mainP() {
return "main";
}
}
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no,
initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Main Page</title>
</head>
<body>
page
</body>
</html>
클라이언트가 우리의 Spring Boot Application에 특정한 요청을 보내는 경우, 요청이 Servlet Container를 지나서 Spring Boot Application으로 들어오게 된다. 그때 Servlet Container는 여러개의 Filter를 가지고 있고, 요청은 이 Filter를 지나서 들어오게 된다. 여기서 SpringSecurity의 의존성을 추가하게 되면, Filter에서 해당 요청을 가로채고, 다음과 같은 사항들을 확인한다. 요청에서 클라이언트가 가고자하는 특정 경로에 대해 1.권한을 가지고 있는지, 2.로그인이 되어있는지, 3.role을 가지고 있는지 검증한다. 이와 같은 작업을 인가 작업이라고 하고, 이는 경로 요청에 대해 SpringSecurity가 대해 미리 검증하는 과정이다.
특정한 경로에 요청이 오면 Controller 클래스에 도달하기 전 필터에서 SpringSecurity가 검증을 함
1. 해당 경로의 접근은 누구에게 열려 있는지
2. 로그인이 완료된 사용자인지
3. 해당되는 role을 가지고 있는지
인가 설정을 진행하는 클래스
(엄밀하게 정의하면 SecurityFilterChain 설정을 진행함)
package com.example.testsecurity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
return http.build();
}
}
.reqeustMathers
: 경로에 대한 권한을 부여permitAll
: 모든 사용자에게 접근 허용hasRole
: 특정 권한이 있는 사용자만 접근 가능authenticated
: 로그인만 진행하면 모두 접근 가능denyAll
: 모든 사용자의 접근 제한.anyRequest
: 위에서 처리하지 않은 나머지 경로 일괄 설정..requestMatchers("/", "/login").permitAll()
main.mustache
파일이 제공된다. .authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
anyMatch
와 같은 "모든 경로 설정"은 가장 하단에서 이뤄지도록 주의해야 한다.package com.example.testsecurity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AdminController {
@GetMapping("/admin")
public String adminP() {
return "admin";
}
}
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
admin page
</body>
</html>
.requestMatchers("/admin").hasRole("ADMIN")
/admin
에 접근 가능하도록 제한.스프링은 버전에 따라 구현 방식이 변경되는데 시큐리티의 경우 특히 세부 버전별로 구현 방법이 많이 다르기 때문에 버전 마다 구현 특징을 확인해야 한다.
새로운 버전이 출시될 때마다 GitHub의 Spring 리포지터리에서 Security의 Release 항목을 통해 변경된 점을 확인할 수 있다.
WebSecurityConfigurerAdapter
상속받아 내부의 configure
메서드를 오버라이딩 하는 방식.public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").authenticated()
.anyRequest().permitAll();
}
}
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated();
return http.build();
}
}
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/join").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
스프링 시큐리티 config 클래스 설정 후 특정 경로에 대한 접근 권한이 없는경우 자동으로 로그인 페이지로 리다이렉팅 되지 않고 오류 페이지가 발생한다.
위 문제를 하결하기 위해 Config 클래스를 설정하면 로그인 페이지 설정도 진행해야 한다.
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
login page
<hr>
<form action="/loginProc" method="post" name="loginForm">
<input id="username" type="text" name="username" placeholder="id"/>
<input id="password" type="password" name="password" placeholder="password"/>
<input type="submit" value="login"/>
</form>
</body>
</html>
submit
버튼을 클릭하면 /loginProc
라는 경로로 POST
요청이 가게 된다.package com.example.testsecurity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String loginP() {
return "login";
}
}
login
을 위한 페이지가 제공된다./admin
경로로 접근해도, 자동으로 로그인 페이지로 전환되지 않는 문제가 남아있다.SpringConfig
파일에 따로 설정을 추가해줘야 한다.package com.example.testsecurity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
http
.formLogin((auth) -> auth.loginPage("/login")
.loginProcessingUrl("/loginProc")
.permitAll()
);
http
.csrf((auth) -> auth.disable());
return http.build();
}
}
.loginPage()
: 특정 로그인 페이지를 설정. 인가 관련 문제 시 SpringSecurity가 알아서 지정된 페이지로 redirect 해준다.loginProcessingUrl()
: html 로그인 폼 양식에서 특정한 경로로 SpringSecurity가 앞단(프론트)에서 받은 데이터를 보내준다..permitAll()
설정으로 모든 사용자가 접근가능하도록 설정한다.csrf((auth) -> auth.disable());
CSRF(Cross-Site Request Forgery)
- CSRF는 웹 보안 취약점 중의 하나로, 인증된 사용자의 권한을 악용하여 원치않는 작업을 수행하게 하는 공격이다.
- 작동원리
- 사용자가 신뢰하는 사이트A 에 로그인하여 인증 세션을 가지고 있다.
- 공격자는 사용자를 악의적인 사이트B로 유도한다.
- 사이트B는 사용자 모르게 사이트A로 요청을 보낸다.
- 사이트A는 유효한 세션으로 인해 이 요청을 신뢰하고 실행한다.
- CSRF 방어
- SpringSecurity의 CSRF 보호는 서버에서 생성한 토큰을 클라이언트에 보내고, 클라이언트가 요청 시 이 토큰을 함께 보내도록 하여 요청의 출처를 점으한다.
스프링 시큐리티는 사용자 인증(로그인) 시 비밀번호에 대해 단방향 해시 암호화를 진행하여 저장되어 있는 비밀번호와 대조한다.
따라서 회원가입 시 비밀번호 항목에 대해서 암호화를 진행해야 한다.
스프링 시큐리티는 암호화를 위해 BCrypt Password Encoder를 제공하고 권장한다. 따라서 해당 클래스를 return하는 메소드를 만들어 @Bean
으로 등록하여 사용하면 된다.
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
BCrypt (단방향 해시의 한 종류)
- 비밀번호 저장에 특화된 해시함수. Blowfish 암호를 기반으로 한 적응형 함수다.
- 주된 목적은 비밀번호의 안전한 보호. 공격자가 해서도니 비밀번호를 역산하는 것을 어렵게 하는 것이다.
- 솔트(Slat)를 사용하는데, 각 비밀번호에 무작위 값을 추가하여 같은 비밀번호라도 다르게 해시된다.
- 예시: "password" + "랜덤문자열" -> 해시
- 의도적으로 느리게 설계되어 해커의 무차별 대입 공격(brute-force attack)을 어렵게 만든다.
- 정당한 사용자에게는 문제가 없지만, 수천 번 시도해야 하는 해커에게는 큰 장벽이 된다.
위의 모식도는 뒤에서 실제 사용될 모식도. SpringSecurity가 회원을
인증하기 위해서는 DB가 연결되어 회원 정보가 저장되어야 한다.
회원 정보를 저장하기 위한 데이터베이스는 MySQL 엔진의 데이터베이스를 사용한다. 그리고 접근은 Spring Data JPA를 사용한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://아이피:3306/데이터베이스?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true
spring.datasource.username=아이디
spring.datasource.password=비밀번호
회원 정보를 통해 인증 인가 작업을 진행하기 대문에 사용자로 부터 회원가입을 진행한 뒤 데이터베이스에 회원 정보를 저장해야 한다.
SpringBoot Application (큰 네모) 내부에 회원 인증/인가와 관련된 로직이 있다. 중간을 기준으로 상단이 로그인을 관리하는 Security 요소들이고, 아래에 JoinController
와 JoinService
가 회원가입을 진행하는 로직이다. 회원정보를 받아서 UserRepository
를 통해 DB에 회원정보를 저장하는 로직을 표현했다.
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="/joinProc" method="post" name="joinForm">
<input type="text" name="username" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Join"/>
</form>
</body>
</html>
username
, password
를 받아 /joinProc
로 POST 요청을 보내는 html 파일import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class JoinDTO {
private String username;
private String password;
}
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.service.JoinService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class JoinController {
@Autowired
private JoinService joinService;
@GetMapping("/join")
public String joinP() {
return "join";
}
@PostMapping("/joinProc")
public String joinProcess(JoinDTO joinDTO) {
System.out.println(joinDTO.getUsername());
joinService.joinProcess(joinDTO);
return "redirect:/login";
}
}
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class JoinService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void joinProcess(JoinDTO joinDTO) {
// db에 이미 동일한 username을 가진 회원이 존재하는지?
UserEntity data = new UserEntity();
data.setUsername(joinDTO.getUsername());
data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
data.setRole("ROLE_ADMIN");
userRepository.save(data);
}
}
UserEntity
라는 바구니를 통해 Repository
에서 DB에 저장하게 된다.package com.example.testsecurity.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String role;
}
# hibernate ddl 설정
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
ddl-auto
값을 update
로 설정해두고 프로젝트 실행 시 자동으로 테이블을 생성해준다.none
값으로 변경해 테이블이 수정되지 않도록 해준다. http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login", "/join", "/joinProc").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
username에 대해서 중복된 가입이 발생하면 서비스에서 아주 치명적인 문제가 발생하기 때문에 백엔드 단에서 중복 검증과 중복 방지 로직을 작성해야 한다.
package com.example.testsecurity.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
private String username;
private String password;
private String role;
}
@Column(unique = true)
설정 부여package com.example.testsecurity.repository;
import com.example.testsecurity.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
boolean existsByUsername(String username);
}
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class JoinService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void joinProcess(JoinDTO joinDTO) {
// db에 이미 동일한 username을 가진 회원이 존재하는지?
boolean isUser = userRepository.existByUsername(joinDTO.getUsername());
if (isUser) {
return;
}
UserEntity data = new UserEntity();
data.setUsername(joinDTO.getUsername());
data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
data.setRole("ROLE_ADMIN");
userRepository.save(data);
}
}
프론트단에서도 회원가입 아이디 중복 확인 로직을 추가해야 한다.
httpXMRequest
메소드를 통해 백엔드에 미리 구현해둔 API에 이미 존재하는 username인지 검증하는 로직을 추가해야 한다.
Security가 자동으로 login 과정을 거치고, DB로부터 저장된 데이터를 기반으로 로그인 데이터를 검증하기 위해서는 UserDetailService
와 UserDetails
가 구현되어 있어야 한다.
시큐리티를 통해 인증을 진행하는 방법은 사용자가 Login 페이지를 통해 아이디, 비밀번호를 POST 요청 시 스프링 시큐리티가 데이터베이스에 저장된 회원 정보를 조회 후 비밀번호를 검증하고 서버 세션 저장소에 해당 아이디에 대한 세션을 저장한다.
UserDetailService
를 상속받아 구현해야 한다.loadUserByUsername()
가 받는 String username
은 검증을 위해 스프링 시큐리티가 입력된 username
값을 넣어준다.import com.example.testsecurity.dto.CustomUserDetails;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userData = userRepository.findByUsername(username);
if(userData != null) {
return new CustomUserDetails(userData);
}
return null;
}
}
import com.example.testsecurity.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
boolean existsByUsername(String username);
UserEntity findByUsername(String username);
}
UserDetails
는 DB로부터 특정 유저에 대한 데이터를 들고오고, 들고온 데이터를 SpringConfig에 전달해준다.import com.example.testsecurity.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class CustomUserDetails implements UserDetails {
private UserEntity userEntity;
public CustomUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
/**
* @return 사용자의 특정 권한(ROLE값)
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userEntity.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
getAuthorities()
UserEntity
의 role
값을 GrantedAuthroity
객체로 변환하여 반환한다.isAccountNonExpired()
, isAccountNonLocked()
, isCredentialsNonExpired()
, isEnabled()
ture
를 반환하여, 모든 계정이 유효하고 활성화된 상태로 간주된다./admin
페이지에 접근할 수 있다.로그인 진행 이후 특정 유저의 id, role값을 가진 세션 정보를 알아내는 방법을 알아보자!
getContext()
메서드를 통해 현재 보안 컨텍스트에 접근할 수 있다.import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Collection;
import java.util.Iterator;
@Controller
public class MainController {
@GetMapping("/")
public String mainP(Model model) {
String id = SecurityContextHolder.getContext().getAuthentication().getName();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iter = authorities.iterator();
GrantedAuthority auth = iter.next();
String role = auth.getAuthority();
model.addAttribute("id", id);
model.addAttribute("role", role);
return "main";
}
}
SecurityContextHolder.getContext().getAuthentication().getName();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iter = authorities.iterator();
GrantedAuthority auth = iter.next();
String role = auth.getAuthority();
SecurityContextHolder.getContext().getAuthentication();
Authentication
객체를 가져온다.authentication.getAuthorities();
GrantedAuthority
을 가져온다.앞서 개발 환경에서는 Security Config 클래스를 통해 csrf 설정을 비활성화했다. 배포 환경에서는 csrf 공격 방지를 위해 csrf disable 설정을 제거하고 추가적인 설정을 진행해야 한다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf((auth) -> auth.disable());
`
return http.build();
}
}
Security Config 클래스에서 csrf.disable() 설정을 진행하지 않으면 자동으로 enable 설정이 진행된다. enable 설정 시 스프링 시큐리티는 CsrfFilter를 통해 POST, PUT, DELETE 요청에 대해서 토큰 검증을 진행한다.
csrf.disable()
구문 삭제<form action="/loginReceiver" method="post" name="loginForm">
<input type="text" name="username" placeholder="아이디"/>
<input type="password" name="password" placeholder="비밀번호"/>
<input type="hidden" name="_csrf" value="{{_csrf.token}}"/>
<input type="submit" value="로그인"/>
</form>
<meta name="_csrf" content="{{_csrf.token}}"/>
<meta name="_csrf_header" content="{{_csrf.headerName}}"/>
<head>
구획에 해당 요소를 추가한다.XMLHttpRequest
요청 시 setRequestHeader
를 통해 _csrf
, _csrf_header Key
에 대한 토큰 값을 넣어 요청한다.csrf 설정 시 POST 요청으로 로그아웃을 진행해야 하지만 아래 방식을 통해 GET 방식으로 진행할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.logout((auth) -> auth.logoutUrl("/logout")
.logoutSuccessUrl("/"));
return http.build();
}
@Controller
public class logoutController {
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
return "redirect:/";
}
}
spring.mustache.servlet.expose-request-attributes=true
앱에서 사용하는 API 서버의 경우 보통 세션을 STATELESS로 관리하기 때문에 스프링 시큐리티 csrf enable 설정을 진행하지 않아도 된다.
토이 프로젝트를 진행하는 경우 또는 시큐리티 로그인 환경이 필요하지만 소수의 회원 정보만 가지며 데이터베이스란느 자원을 투자하기 힘든 경우는 회원가입 없는 InMemory 방식으로 유저를 저장하면 된다.
이 경우 InMemoryuserDeetailsManager 클래스를 통해 유저를 등록하면 된다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.builder()
.username("user1")
.password(bCryptPasswordEncoder().encode("1234"))
.roles("ADMIN")
.build();
UserDetails user2 = User.builder()
.username("user2")
.password(bCryptPasswordEncoder().encode("1234"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user1, user2);
}
}
Http Basic 인증 방식은 아이디와 비밀번호를 Base64 방식으로 인코딩한 뒤 HTTP 인증 헤더에 부착하여 서버측으로 요청을 보내는 방식.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.httpBasic(Customizer.withDefaults());
return http.build();
}
권한A, 권한B, 권한C가 존재하고 권한의 계층은 "A < B < C"라고 설정을 진행하고 싶은 경우, RoleHierarchy 설정을 진행할 수 있다.
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_C > ROLE_B\n" +
"ROLE_B > ROLE_A");
return hierarchy;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf((auth) -> auth.disable());
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login").permitAll()
.requestMatchers("/").hasAnyRole("A")
.requestMatchers("/manager").hasAnyRole("B")
.requestMatchers("/admin").hasAnyRole("C")
.anyRequest().authenticated()
);
http
.formLogin((auth) -> auth.loginPage("/login")
.loginProcessingUrl("/loginProc")
.permitAll()
);
return http.build();
}