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 ๋ง์คํฐํ๋ค... ๋๋ถ์ ์ข์ ์ธ์ฌ์ดํธ ์ป๊ณ ๊ฐ๋๋ค!