Spring Security๋ ์คํ๋ง ๊ธฐ๋ฐ์ ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์(์ธ์ฆ, ์ธ๊ฐ, ๊ถํ)์ ๋ด๋นํ๋ ์คํ๋ง ํ์ ํ๋ ์์ํฌ์ด๋ค.
์ธ์ฆ(Authentication)
์ธ์ฆ(Authentication)์ ์ฌ์ฉ์์ ์ ์์ ์ ์ฆํ๋ ๊ณผ์
์ธ๊ฐ(Authorization)
์ธ๊ฐ(Authorization)๋ ์ฌ์ดํธ์ ํน์ ๋ถ๋ถ์ ์ ๊ทผํ ์ ์๋์ง ๊ถํ์ ํ์ธํ๋ ์์
์ด๋ฌํ ์คํ๋ง ์ํ๋ฆฌํฐ๋ CSRF ๊ณต๊ฒฉ(์ฌ์ฉ์์ ๊ถํ์ ๊ฐ์ง๊ณ ํน์ ๋์์ ์ํํ๋๋ก ์ ๋ํ๋ ๊ณต๊ฒฉ), ์ธ์ ๊ณ ์ ๊ณต๊ฒฉ(์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ํ์ทจํ๊ฑฐ๋ ๋ณ์กฐํ๋ ๊ณต๊ฒฉ)์ ๋ฐฉ์ดํด์ค๋ค.

์ฐ๋ฆฌ๋ ์ฌ๊ธฐ์
UsernamePasswordAuthenticationFilter์FilterSecurityInterceptor๋ฅผ ์ง์คํด์ ๋ณด์์ผ ํ๋ค.
์์ด๋์ ํจ์ค์๋๊ฐ ๋์ด์ค๋ฉด ์ธ์ฆ ์์ฒญ์ ์์ํ๋ ์ธ์ฆ ๊ด๋ฆฌ์ ์ญํ ์ ์ํํ๋ค.
AuthenticationSuccessHandler๋ฅผ, ์ธ์ฆ์ ์คํจํ๋ฉด AuthenticationFailureHandler๋ฅผ ์คํํ๋ค.๊ถํ ๋ถ์ฌ ์ฒ๋ฆฌ๋ฅผ ์์ํ์ฌ ์ ๊ทผ ์ ์ด ๊ฒฐ์ ์ ์ฝ๊ฒ ํ๋ ์ ๊ทผ ๊ฒฐ์ ๊ด๋ฆฌ์ ์ญํ ์ ์ํํ๋ค.

dependencies{
// ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ์คํํฐ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-security'
// ํ์๋ฆฌํ์์ ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ์์กด์ฑ ์ถ๊ฐ
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
// ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ํ
์ํธํ๊ธฐ ์ํ ์์กด์ฑ ์ถ๊ฐ
testImplementation 'org.springframework.security:spring-security-test'
}
package me.shinsunyoung.springbootdeveloper.domain;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "users")
@Builder
public class User implements UserDetails { // UserDetails๋ฅผ ์์๋ฐ์ ์ธ์ฆ ๊ฐ์ฒด๋ก ์ฌ์ฉ
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "password")
private String password;
@Builder
public User(Long id, String email, String password) {
this.id = id;
this.email = email;
this.password = password;
}
@Override // ๊ถํ ๋ฐํ
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("user"));
}
// ์ฌ์ฉ์์ id๋ฅผ ๋ฐํ(๊ณ ์ ํ ๊ฐ)
@Override
public String getUsername() {
return email;
}
// ์ฌ์ฉ์์ ํจ์ค์๋ ๋ฐํ
@Override
public String getPassword(){
return password;
}
// ๊ณ์ ๋ง๋ฃ ์ฌ๋ถ ๋ฐํ
@Override
public boolean isAccountNonExpired() {
return true; // true๋ฉด ๋ง๋ฃ๋์ง ์์์
}
// ๊ณ์ฉก ์ ๊ธ ์ฌ๋ถ ๋ฐํ
@Override
public boolean isAccountNonLocked() {
return true; // true๋ฉด ๊ณ์ ์ ๊ธ X
}
// ํจ์ค์๋์ ๋ง๋ฃ ์ฌ๋ถ ๋ฐํ
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// ๊ณ์ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ ๋ฐํ
@Override
public boolean isEnabled() {
return true;
}
}
| ๋ฉ์๋ | ๋ฐํ ํ์ | ์ค๋ช |
|---|---|---|
| getAuthorities | Collection< ? extends GrantedAuthority> | ์ฌ์ฉ์๊ฐ ๊ฐ์ง๊ณ ์๋ ๊ถํ์ ๋ชฉ๋ก์ ๋ฐํํ๋ค. |
| getUsername() | String | ์ฌ์ฉ์๋ฅผ ์๋ณํ ์ ์๋ ์ฌ์ฉ์ ์ด๋ฆ์ ๋ฐํํ๋ค. ์ด๋ ์ฌ์ฉ๋๋ ์ฌ์ฉ์์ ์ด๋ฆ์ ๋ฐ๋์ ๊ณ ์ ํด์ผ ํ๋ค. ํ์ฌ ์ฝ๋๋ email์ด ๊ณ ์ ์ฝ๋์ด๋ค. |
| getPassword() | String | ์ฌ์ฉ์์ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ฐํํ๋ค. ์ด๋ ์ ์ฅ๋์ด ์๋ ๋น๋ฐ๋ฒํธ๋ ์ํธํํด์ ์ ์ฅํด์ผ ํ๋ค. |
| isAccountNonExpired() | boolean | ๊ณ์ ์ด ๋ง๋ฃ๋์๋์ง ํ์ธํ๋ ๋ฉ์๋์ด๋ค. ๋ง์ฝ ๋ง๋ฃ๋์ง ์์๋ฐ๋ฉด true๋ฅผ ๋ฐํํ๋ค. |
| isAccountNonLocked() | boolean | ๊ณ์ ์ด ๋ง๋ฃ๋์๋์ง ํ์ธํ๋ ๋ฉ์๋์ด๋ค. ๋ง์ฝ ๋ง๋ฃ๋์ง ์์๋ฐ๋ฉด true๋ฅผ ๋ฐํํ๋ค. |
| isCredetialsNonExpired() | boolean | ๋น๋ฐ๋ฒํธ๊ฐ ๋ง๋ฃ๋์๋์ง ํ์ธํ๋ ๋ฉ์๋์ด๋ค. ๋ง์ฝ ๋ง๋ฃ๋์ง ์์์ ๋๋ true๋ฅผ ๋ฐํํ๋ค. |
| isEnabled() | boolean | ๊ณ์ ์ด ์ฌ์ฉ ๊ฐ๋ฅํ์ง ํ์ธํ๋ ๋ฉ์๋์ด๋ค. ๋ง์ฝ ์ฌ์ ๊ฐ๋ฅํ๋ค๋ฉด true ๋ฐํํ๋ค. |
package me.shinsunyoung.springbootdeveloper.repository;
import me.shinsunyoung.springbootdeveloper.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
package me.shinsunyoung.springbootdeveloper.service;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.repository.UserRepository;
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 UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
// ์ฌ์ฉ์ ์ด๋ฆ(email)์ผ๋ก ์ฌ์์์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฉ์๋
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException(email));
}
}
loadUserByUsername() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํด์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๋ก์ง ์์ฑpackage me.shinsunyoung.springbootdeveloper.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final UserDetailsService userService;
@Bean // 1. ์คํ๋ง ์ํ๋ฆฌํฐ ๊ธฐ๋ฅ ๋นํ์ฑํ
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers(new AntPathRequestMatcher("/static/**"));
}
@Bean // 2. ํน์ HTTP ์์ฒญ์ ๋ํ ์น ๊ธฐ๋ฐ ๋ณด์ ๊ตฌ์ฑ
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth // 3. ์ธ์ฆ, ์ธ๊ฐ ์ค์
.requestMatchers(
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/signup"),
new AntPathRequestMatcher("/user")
).permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin // 4. ํผ ๊ธฐ๋ฐ ๋ก๊ทธ์ธ ์ค์
.loginPage("/login")
.defaultSuccessUrl("/articles")
)
.logout(logout -> logout.logoutSuccessUrl("/login") // 5. ๋ก๊ทธ์์ ์ค์
.invalidateHttpSession(true)
)
.csrf(AbstractHttpConfigurer::disable) // 6. CSRF ๋นํ์ฑํ
.build();
}
// 7. ์ธ์ฆ ๊ด๋ฆฌ์ ๊ด๋ จ ์ค์
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailsService userDetailsService) throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userService);
// 8. ์ฌ์ฉ์ ์ ๋ณด ์๋น์ค๋ฅผ ์ค์
authProvider.setPasswordEncoder(bCryptPasswordEncoder);
return new ProviderManager(authProvider);
}
// 9. ํจ์ค์๋ ์ธ์ฝ๋๋ก ์ฌ์ฉํ ๋น ๋ฑ๋ก
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
requestMatchers() : ํน์ ์์ฒญ๊ณผ ์ผ์นํ๋ url์ ๋ํ ์ก์ธ์ค๋ฅผ ์ค์ ํ๋ค.permitAll() : ๋๊ตฌ๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ฒ ์ค์ ํ๋ค. ์ฆ, "/login", "/signup", "/user" ๋ก ์์ฒญ์ด ์ค๋ฉด ์ธ์ฆ/์ธ๊ฐ ์์ด๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.anyRequest() : ์์์ ์ค์ ํ url์ด์ธ์ ์์ฒญ์ ๋ํด ์ค์ ํ๋ค.authenticated() : ๋ณ๋์ ์ธ๊ฐ๋ ํ์ํ์ง ์์ง๋ง ์ธ์ฆ์ด ์ฑ๊ณต๋ ์ํ์ฌ์ผ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.loginPage() : ๋ก๊ทธ์ธ ํ์ด์ง ๊ฒฝ๋ก๋ฅผ ์ค์ ํ๋ค.defaultSucessUrl() : ๋ก๊ทธ์ธ์ด ์๋ฃ๋์์ ๋ ์ด๋ํ ๊ฒฝ๋ก๋ฅผ ์ค์ ํ๋ค.logoutSuccessUrl() : ๋ก๊ทธ์์์ด ์๋ฃ๋์์ ๋ ์ด๋ํ ๊ฒฝ๋ก๋ฅผ ์ค์ ํ๋ค.invalidateHttpSession() : ๋ก๊ทธ์์ ์ดํ์ ์ธ์
์ ์ ์ฒด ์ญ์ ํ ์ง ์ฌ๋ถ๋ฅผ ์ค์ ํ๋ค.userDetailsService() : ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์๋น์ค๋ฅผ ์ค์ ํ๋ค. ์ด๋ ์ค์ ํ๋ ์๋น์ค ํด๋์ค๋ UserDetailsService๋ฅผ ์์๋ฐ์ ํด๋์ค์ฌ์ผ ํ๋ค.passwrodEncoder() : ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํํ๊ธฐ ์ํ ์ธ์ฝ๋๋ฅผ ์ค์ ํ๋ค.package me.shinsunyoung.springbootdeveloper.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AddUserRequest {
private String email;
private String password;
}
package me.shinsunyoung.springbootdeveloper.service;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.User;
import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest;
import me.shinsunyoung.springbootdeveloper.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public Long save(AddUserRequest dto){
return userRepository.save(User.builder()
.email(dto.getEmail())
// ํจ์ค์๋ ์ํธํ
.password(bCryptPasswordEncoder.encode(dto.getPassword()))
.build()).getId();
}
}
package me.shinsunyoung.springbootdeveloper.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest;
import me.shinsunyoung.springbootdeveloper.service.UserService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@RequiredArgsConstructor
@Controller
public class UserApiController {
private final UserService userService;
@PostMapping("/user")
public String signup(AddUserRequest request) {
userService.save(request); // ํ์ ๊ฐ์
๋ฉ์๋ ํธ์ถ
return "redirect:/login"; // ํ์ ๊ฐ์
์ด ์๋ฃ๋ ์ดํ์ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๋ค.
}
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "redirect:/login";
}
}
http://localhost:8080/articles๋ก ์ ๊ทผํ๊ฒ ๋๋ฉด articles๋ ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ๋ค์ด๊ฐ ์ ์๋ ํ์ด์ง์ด๋ฏ๋ก ๋ก๊ทธ์ธ ํ์ด์ง๋ก redirect ๋๋ค.

ํ์๊ฐ์
๋ฒํผ์ ๋๋ฅด๊ฒ ๋๋ฉด ํ์ ๊ฐ์
ํ์ด์ง๋ก ์ด๋ํ๊ณ ํ์ ๊ฐ์
ํ์ด์ง๋ permitAll() ๋ฉ์๋์ด๊ธฐ ๋๋ฌธ์ ๋ณ๋ ์ธ์ฆ ์์ด ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.

๋น๋ฐ๋ฒํธ๊ฐ ์ํธํ๋์ด ์ ์ฅ๋์ด์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค..

ํ์๊ฐ์
์ ์งํํ๊ฒ ๋๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๊ฒ ๋๊ณ ๊ฐ์
ํ ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ์ฌ ๋ก๊ทธ์ธ์ ์งํํ๋ค.

์ ์์ ์ผ๋ก ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ฉด ๊ธ ๋ชฉ๋ก ํ์ด์ง๋ก ์ด๋ํ๊ฒ ๋๋ค.

๋ก๊ทธ์์ ๋ฒํผ์ ๋๋ฅด๊ฒ ๋๋ฉด ์ธ์ฆ ์ ๋ณด๊ฐ ์์ผ๋ฏ๋ก ๋ค์ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๋ค.
์ ์ฒด ํ๋ซํผ์ ๋ก๊ทธ์ธ/๋ก๊ทธ์์๊ณผ ํน์ ํ์ด์ง์ ๋ํ ์ ๊ทผ์ ํน์ ์ฌ์ฉ์์๊ฒ๋ง ํด์ค ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉ๋ ๊ฒ ๊ฐ๋ค.
์คํ๋ง ์ํ๋ฆฌํฐ๋ ์คํ๋ง ๊ธฐ๋ฐ์ ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์(์ธ์ฆ, ์ธ๊ฐ, ๊ถํ)์ ๋ด๋นํ๋ ์คํ๋ง ํ์ ํ๋ ์์ํฌ์ด๋ฉฐ ์คํ๋ง ์ํ๋ฆฌํฐ๋ ํํฐ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ค.
๊ฐ ํํฐ์์๋ ์ธ์ฆ๊ณผ ์ธ๊ฐ์ ๊ด๋ จ๋ ์์ ์ ์ฒ๋ฆฌํ๊ณ ๊ธฐ๋ณธ์ ์ผ๋ก ์ธ์ ๊ณผ ์ฟ ๊ธฐ ๋ฐฉ์์ผ๋ก ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ค.
UserDetails ๊ฐ์ฒด์ ๋ด๋๋ค. ์ด ํด๋์ค๋ฅผ ์์๋ฐ์ ๋ค ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ํด ์ฌ์ฉํ๋ฉด ๋๋ค.UserDetailService๋ฅผ ์ฌ์ฉํ๋ค. ์ด ํด๋์ค๋ฅผ ์์๋ฐ์ ๋ค loadUserByUsename()์ ์ค๋ฒ๋ผ์ด๋ํ๋ฉด ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ค๋ฒ๋ผ์ด๋๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค.http://atin.tistory.com/590?pidx=0
https://taetoungs-branch.tistory.com/204?pidx=1
์คํ๋ง ๋ถํธ 3 ๋ฐฑ์๋ ๊ฐ๋ฐ์ ๋๊ธฐ: ์๋ฐ ํธ - ์ ์ ์ -
spring security ๋ง์คํฐํ๋ค... ๋๋ถ์ ์ข์ ์ธ์ฌ์ดํธ ์ป๊ณ ๊ฐ๋๋ค!