[Security] Security ํ์๊ฐ์ , ๋ก๊ทธ์ธ
by rlaehddnd0422์ด๋ฒ ํฌ์คํ ์์๋ ๊ฐ๋จํ๊ฒ Spring Security๋ฅผ ์ด์ฉํด ํ์ ๊ฐ์ ๊ณผ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ด ์๋ค.
build.gradle ์ค์
์์กด๊ด๊ณ์ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
์คํ๋ง ์ํ๋ฆฌํฐ ์์กด์ฑ ์ถ๊ฐ ์
1. ์๋ฒ ์คํ ์ Spring Security์ ์ด๊ธฐํ ์์ ๋ฐ ๋ณด์ ์ค์ ๋ฉ๋๋ค.
2. ๋ณ๋์ ์ค์ ์ด๋ ๊ตฌํ์ ํ์ง ์์๋ ๊ธฐ๋ณธ์ ์ธ ์น ๋ณด์ ๊ธฐ๋ฅ์ด ์๋ฒ์ ์ฐ๋๋ฉ๋๋ค.
3. ๋ชจ๋ ์์ฒญ์ ์ธ์ฆ์ด ๋์ด์ผ ์ ๊ทผ ๊ฐ๋ฅ
4. ๊ธฐ๋ณธ ๋ก๊ทธ์ธ ํ์ด์ง ์ ๊ณต
+ application.properties์ ๊ธฐ๋ณธ name/password ์ค์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
ํํฐ ๋ฑ๋ก
Request์ ๋ํด์ ํํฐ๊ฐ ๊ฐ๋ก์ฑ์ด ์ธ์ฆ ์ ๊ฐ์ฒด๋ฅผ ๋ฐ์ ์ ์๋๋ก ํน์ URI ์ ์ ์ loginForm์ผ๋ก ์ด๋ํ๋๋ก ์ค์ ํด๋ด ์๋ค.
Spring Security๋ ์๋์ ๊ฐ์ด SecurityFilterChain์ ์ฌ์ฉํ๋๋ก ๊ถ์ฅํฉ๋๋ค.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated() // ๋ก๊ทธ์ธํด์ผ ๋ค์ด์ฌ์ ์์
.antMatchers("/manager/**").access("hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login") // login ์ฃผ์๊ฐ ํธ์ถ์ด๋๋ฉด ์ํ๋ฆฌํฐ๊ฐ ๋์์ฑ์ ๋์ ๋ก๊ทธ์ธ ์งํํด์ค.
.defaultSuccessUrl("/");
return http.build();
}
- SecurityFilterChain ๋ฑ๋ก
- http.csrf().disable() : CSRF ๋ฐฉ์ง. CSRF(Cross-Site Request Forgery) ๋ก๋ถํฐ ๋ณดํธ
- http.authorizeRequests()
- antMatchers(String).authenticated() : String URI ์ ๋ํด ์ ๊ทผํ๊ธฐ ์ํด์๋ ๋ก๊ทธ์ธ ํ์
- antMatchers(String).access("ROLE") : ํด๋น ROLE์ ๊ฐ์ ธ์ผ ์ ๊ทผํ ์ ์์
- loginPage(String) : ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ง์ํ๋ ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋ณ๋๋ก ๋ก๊ทธ์ธ ํ์ด์ง URI ๋ฑ๋ก
- loginProcessUrl("/login") : /login ์ URI ์์ฒญ์ด ์ค๋ฉด ์ํ๋ฆฌํฐ๊ฐ ๋์์ฑ์ ๋ก๊ทธ์ธ์ ์งํํฉ๋๋ค.
- defaultSuccessUrl() : ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ด๋ํ URI, ์ง์ ํ์ง ์์ผ๋ฉด ์ด์ ์ ์์ฒญํ๋ URI๋ก ์๋์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธํฉ๋๋ค.
ํ์๊ฐ์ Controller
@Slf4j
@Controller
@RequiredArgsConstructor
public class IndexController {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/joinForm")
public String joinForm() {
return "joinForm";
}
@PostMapping("/join")
public String join(User user) {
user.setRole("USER");
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
userRepository.save(user);
log.info("user = {} ", user);
return "redirect:/loginForm";
}
joinForm์์ Post ์ ์ก๋ ํ๋ผ๋ฏธํฐ๋ฅผ user์ ๋ด๊ณ , BCryptPasswordEncoder๋ฅผ ํตํด ํด์ฑํจ์๋ก ์ํธํ๋ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํฉ๋๋ค.
+ ์ํ๋ฆฌํฐ๋ก ๋ก๊ทธ์ธํ๊ธฐ ์ํด์๋ ๋ฐ๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ์์ ๊ฐ์ด ์ํธํ ํด์ผํฉ๋๋ค.
๊ฐ์ฒด์ ๊ฒฝ์ฐ ์๋์ผ๋ก @ModelAttribute๊ฐ ์ ์ฉ๋ฉ๋๋ค.
๋ก๊ทธ์ธ Controller
@GetMapping("/loginForm")
public String login() {
return "loginForm";
}
loginForm
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>๋ก๊ทธ์ธ ํ์ด์ง</title>
</head>
<body>
<h1>๋ก๊ทธ์ธ ํ์ด์ง</h1>
<hr/>
<!-- ์ํ๋ฆฌํฐ๋ x-www-form-url-encoded ํ์
๋ง ์ธ์ -->
<form action="/login" method="post">
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password"/>
<button>๋ก๊ทธ์ธ</button>
</form>
<a href="/joinForm">ํ์๊ฐ์
์ ์์ง ํ์ง ์์ผ์
จ๋์?</a>
</body>
</html>
์ ๋ ฅํ ID์ PW๋ฅผ /login ์ผ๋ก Post ์ ์กํฉ๋๋ค.
์ด ๋ ์์์ ๋ฑ๋กํ ํํฐ์ ์ํด (http.loginProcessingUrl("/login")) login ์ฃผ์๊ฐ ํธ์ถ์ด ๋๋ฉด ์ํ๋ฆฌํฐ๊ฐ ๋์์ฑ์ ๋์ ๋ก๊ทธ์ธ์ ์งํํฉ๋๋ค.
+ loginProcessingUrI๋ฅผ ์ฌ์ฉํ๋ฉด ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ๋ด๋ถ์์ AuthenticationManager์ AuthenticationProvider๋ฅผ ์๋์ผ๋ก ์์ฑํฉ๋๋ค.
๋ฐ๋ผ์ .loginProcessingUrl์ ์ฌ์ฉํ๋ฉด ์คํ๋ง ์ํ๋ฆฌํฐ๋ ๋ด๋ถ์์ ์ฌ์ฉ์๊ฐ ํด๋น URL๋ก ๋ก๊ทธ์ธ์ ์๋ํ ๋ ์ธ์ฆ ์ฒ๋ฆฌ๋ฅผ ์ํด AuthenticationManager์ AuthenticationProvider๋ฅผ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค. ๋ฐ๋ผ์ ๋ณ๋๋ก AuthenticationManager์ AuthenticationProvider๋ฅผ ๊ตฌ์ฑํ์ง ์์๋ ๊ธฐ๋ณธ์ ์ธ ์ธ์ฆ ์ฒ๋ฆฌ๋ฅผ ์ํํ ์ ์์ต๋๋ค.
- ๋ก๊ทธ์ธ ์๋ฃ ํ, Security Session์ ์ธ์ ์ ๋ณด๋ฅผ ์ ์ฅํด์ผ ํฉ๋๋ค.
- ์ฌ๊ธฐ์ ๋ค์ด๊ฐ ์ ์๋ ๊ฐ์ฒด๋ Authentication ๊ฐ์ฒด ๋ฟ์ ๋๋ค. Authentication ๊ฐ์ฒด ์์๋ User ์ ๋ณด๋ฅผ ์ ์ฅํด์ผ ํ๋๋ฐ ์ด ๋ User๋ ๋ฐ๋์ UserDetails ํ์ ์ด์ด์ผ ํฉ๋๋ค.
์ฝ๊ฒ ๋งํด, ์ํ๋ฆฌํฐ๊ฐ /login ์์ฒญ์ ๋์์ฑ์ ๋ก๊ทธ์ธ์ ์งํํฉ๋๋ค.
๋ก๊ทธ์ธ ์งํ ์๋ฃ๊ฐ ๋๋ฉด, SecurityContextHolder์ ์ํ๋ฆฌํฐ ์ธ์ ์ ์ ์ฅํด์ผ ํฉ๋๋ค.
์ด ๊ณผ์ ์์ ์ฌ๊ธฐ(SecurityContextHolder)์ ๋ค์ด๊ฐ ์ ์๋ ๊ฐ์ฒด๋ Authentication ๋ฟ์ด๊ณ ,
Authentication ๊ฐ์ฒด ์์๋ ์ ์ ์ ๋ณด๊ฐ ๋ค์ด๊ฐ ์์ด์ผ ํ๋๋ฐ, ์ ์ ์ ๋ณด๋ ๋ฐ๋์ UserDetails ํ์ ์ด์ด์ผ ํฉ๋๋ค.
SecurityContextHolder(Session(Authentication(UserDetails)))
๋ฐ๋ผ์ UserDetails๋ฅผ ๊ตฌํํ ๊ตฌํ์ฒด PrincipalDetails๋ฅผ Authentication ๊ฐ์ฒด์ ๋ด๋๋ก ํฉ์๋ค.
์ฐ์ UserDetails ๊ตฌํ - PrincipalDetails
public class PrincipalDetails implements UserDetails {
private User user;
public PrincipalDetails(User user) {
this.user = user;
}
// ํด๋น User์ ๊ถํ์ ๋ฆฌํดํ๋ ๊ณณ
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(
new GrantedAuthority(){
@Override
public String getAuthority() {
return user.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// ๊ณ์ ๋ง๋ฃ์ฌ๋ถ
@Override
public boolean isAccountNonExpired() {
return true;
}
// ๊ณ์ ์ ๊นx ์ฌ๋ถ
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// ํด๋ฉด ๊ณ์
@Override
public boolean isEnabled() {
// 1๋
๋์ ๋ก๊ทธ์ธ ์ํ๋ฉด ํด๋ฉด๊ณ์ ์ผ๋ก ํ๊ธฐ๋กํจ
// ํ์ฌ์๊ฐ - ๋ง์ง๋ง๋ก๊ทธ์ธ๋ ์ง => 1๋
์ด๊ณผํ๋ฉด false
// else true
return true;
}
}
UserDetailsService ๊ตฌํ - PrincipalDetailService
// ์ํ๋ฆฌํฐ ์ค์ ์์ loginProcessingUrl("/login");
// /login ์์ฒญ์ด์ค๋ฉด ์๋์ผ๋ก UserDetailsService ํ์
์ผ๋ก Ioc ๋์ด์๋ loadUserByUsername ์ด ์คํ
@Slf4j
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
// ์ํ๋ฆฌํฐ session(๋ด๋ถ Authentication(๋ด๋ถ UserDetails))
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username = {}",username);
User user = userRepository.findByUsername(username);
if (user != null) {
return new PrincipalDetails(user);
}
return null;
}
}
- ์ํ๋ฆฌํฐ๋ ์๋์ผ๋ก UserDetailsServcie ํ์ ์ผ๋ก Ioc ๋์ด์๋ PrincipalDetailsService ๊ฐ ์คํ๋๋ฉด์, loadUserByUsername ํจ์๊ฐ ์๋์ผ๋ก ์คํ๋ฉ๋๋ค.
- ์ด ํจ์์์ DB์ ์ฟผ๋ฆฌ๋ฉ์๋๋ฅผ ๋ ๋ ค ์ผ์นํ๋ ํ์์ ์ฐพ๊ณ ์ฐพ์๊ฒฝ์ฐ PrincipalDetails (UserDetails) ์ ์ ์ ์ ๋ณด๋ฅผ ๋ด์ ๋ฆฌํดํฉ๋๋ค.
- ์ด์ ์ํ๋ฆฌํฐ Session ์์ UserDetails ํ์ ์ธ PrincipalDetails ๊ฐ์ฒด๋ฅผ ๋ด์ฅํ๊ณ ์๋ Authentication ๊ฐ์ฒด๊ฐ ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค. ( loadByUsername ํจ์์์ ์๋ํ )
์๋ฌธ์ ?
UserDetailsService ์ธํฐํ์ด์ค์ loadUserByUsername(String username)์ ๊ตฌํํด์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ DB์์ ์กฐํํ๊ณ ๋ฐํํฉ๋๋ค. ํ์ง๋ง ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด ๋น๋ฐ๋ฒํธ๋ฅผ ์ฒดํฌํ๋ ์ฝ๋๋ ์์ด๋ณด์ด๋๋ฐ, ์๋ชป๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํ๋ฉด ๋ก๊ทธ์ธ์ ์คํจํฉ๋๋ค.
์ด๋์์ ๊ฐ ๋น๋ฐ ๋ฒํธ ์ฒดํฌ๋ฅผ ํ๊ณ ์๋ ๊ฒ ๊ฐ๊ธดํ๋ฐ ๊ณผ์ฐ ์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒ์ฌํ๋ ๊ฑธ๊น์?
๋ฐ๋ก DaoAuthenticationProvider์์ ๊ฒ์ฌํฉ๋๋ค.
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
...
<์ฐธ๊ณ ์๋ฃ>
'๐ Backend > Spring Security' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Security] OAuth2.0 ๋ค์ด๋ฒ, ์นด์นด์ค ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ (0) | 2023.06.01 |
---|---|
[Security] OAuth2.0 ๋ก๊ทธ์ธ ํ์ฒ๋ฆฌ - ๊ถํ ๋ถ์ฌ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ (0) | 2023.05.31 |
[Security] OAuth2.0์ ์ด์ฉํ ์์ ๋ก๊ทธ์ธ (๊ตฌ๊ธ) (0) | 2023.05.31 |
[Security] ๊ถํ ์ฒ๋ฆฌ @PreAuthorize, @PostAuthorize, @Secured (0) | 2023.05.29 |
[Security] Spring Security๋? (1) | 2023.05.29 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
Study Repository
rlaehddnd0422