Spring - Security
Spring - Security
Spring - Security๋?
์คํ๋ง์ผ๋ก ๊ฐ๋ฐ์ ํ๋ฉด ์ธ์ ์ ์ด์ฉํ ๋ก๊ทธ์ธ ๋ฐฉ๋ฒ์ ๋ง์ด ์ด์ฉํ๋ค.
ํ์ง๋ง ์ธ์ ์ ์ด์ฉํ์ฌ ๊ฐ๋ฐ์ ํ๋ฉด ์ฝ๋๋ก ์์ฑํด์ผํ๋ ๋ถ๋ถ๊ณผ ๋ณด์์ ์ผ๋ก ๋ ธ์ถ๋๋ ๋ถ๋ถ์ด ๋ง์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ๋๋ค.
์ด๋ฌํ ๋ถ๋ถ์ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ธ์ / ํ ํฐ ๋ฐฉ์์ ์ฝ๊ณ ๊ฐํธํ๊ฒ ์ฌ์ฉ ํ ์ ์๋๋ก ์ง์ํ๋ค.
Dependency ์ถ๊ฐ
gradle ๋น๋ ๊ธฐ์ค
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
build.gradle ์ ์์ ๊ฐ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋ ๋๋ ํ๋ก์ ํธ ์ ํ ์ ์ํ๋ฆฌํฐ ์ฒดํฌ๋ฅผ ํด์ค๋ค.
์ ์ฉ
SecurityConfig
@Configuration
@EnableWebSecurity // ํํฐ ์ฒด์ธ ๊ด๋ฆฌ ์์ ์ด๋
ธํ
์ด์
@EnableGlobalMethodSecurity(securedEnabled = true , prePostEnabled = true) // secured ์ด๋
ธํ
์ด์
ํ์ฑํ , preAuthorize / PostAuthorize ์ด๋
ธํ
์ด์
ํ์ฑํ
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ์์คํ์ผ ์ ๊ทผ ์ถ๊ฐ
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**","/css/**","/images/**","/font/**" , "/assets/**" );
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated() // ์ธ์ฆ๋ง ๋๋ฉด ๋ค์ด๊ฐ ์ ์๋ ์ฃผ์!!
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin() // ๊ธฐ๋ณธ ์ธ์ฆ ๋ก๊ทธ์ธ ํ์ด์ง
.loginPage("/login")
.usernameParameter("username") // ์์์ ์ ์ ํ๋ผ๋ฉํฐ๊ฐ
.passwordParameter("password") // ์์์ ์ ์ ํจ์ค์๋๊ฐ
.loginProcessingUrl("/login") // ์ํ๋ฆฌํฐ๊ฐ ์ฃผ์๋ฅผ ๋์์ฑ ์ํ๋ฆฌํฐ ๋ก๊ทธ์ธ์ ์งํํด์ค๋๋ค.
.defaultSuccessUrl("/")
.and()
.oauth2Login()
.loginPage("/login"); // ๊ตฌ๊ธ ๋ก๊ทธ์ธ์ด ์๋ฃ๋ ๋ค์ ์ฒ๋ฆฌ๊ฐ ํ์ํจ.
}
// ํด๋น ๋ฉ์๋์ ๋ฆฌํด๋๋ ์ค๋ธ์ ํธ๋ฅผ IoC๋ก ๋ฑ๋กํด์ค๋ค.
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
}
์ํ๋ฆฌํฐ๋ฅผ ์ ์ฉํ๊ธฐ ์ํด์ ํ๋ก์ ํธ ๋ด์ Config ํด๋์ค๋ฅผ ์์ฑํ๋ค.
Config๋ WebSecurityConfigurerAdapter์ ์์๋ฐ์ ์ฌ์ฉํ๋ฉฐ @Configuration ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ค.
์์ ๊ฐ์ด ์ค์ ํ ํ๋ก์ ํธ์ ์ ์ํ๊ฒ ๋๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก ์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋ํ๊ฒ๋๋ค.
์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ ๋ก๊ทธ์ธ์ ์ฌ์ฉํ์ง ์์ผ๋ฌ๋ฉด @EnableWebSecurity ์ด๋
ธํ
์ด์
์ ์ถ๊ฐํ๋ค.
ํด๋์ค๋ฅผ ์ ์ฑํ ํ configure ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํ์ฌ ๊ถํ์ฒ๋ฆฌ๋ฅผ ํ์ฌ ์ฌ์ฉํ๋ค.
๊ถํ ์ค์
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // .csrf().disable() / Cross-Site Request Forgery ์น์ทจ์ฝ์ฑ ํ ํฐ
.authorizeRequests()
.antMatchers("/login").permitAll()
// .antMatchers("/user/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.usernameParameter("user_id")
.passwordParameter("user_password")
.successHandler(sHandler) // ๋ก๊ทธ์ธ ์ฑ๊ณต์
.and()
.logout()
.logoutSuccessUrl("/login")
.deleteCookies("JSESSIONID") // ์ธ์
์ฟ ํค ์ญ์
.invalidateHttpSession(true)
.and()
.exceptionHandling().accessDeniedPage("/login/denied");
}
antMatchers()๋ฅผ ์ฌ์ฉํ์ฌ ์ ๊ทผ ๊ถํ์ ์ฒ๋ฆฌํ ์ ์๋ค.
- hasRole() or hasAnyRole() : ํน์ ๊ถํ์ ๊ฐ์ง๋ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์์ต๋๋ค.
- hasAuthority() or hasAnyAuthority() : ํน์ ๊ถํ์ ๊ฐ์ง๋ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์์ต๋๋ค.
- hasIpAddress() : ํน์ ์์ดํผ ์ฃผ์๋ฅผ ๊ฐ์ง๋ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์์ต๋๋ค.
- permitAll() or denyAll() : ์ ๊ทผ์ ์ ๋ถ ํ์ฉํ๊ฑฐ๋ ์ ํํฉ๋๋ค.
- rememberMe() : ๋ฆฌ๋ฉค๋ฒ ๊ธฐ๋ฅ์ ํตํด ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์์ต๋๋ค.
- anonymous() : ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ ์ ์์ต๋๋ค.
- authenticated() : ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์์ต๋๋ค.
๋ฟ๋ง ์๋๋ผ ํน์ ๋ฉ์๋์ ๊ด๋ จํ์ฌ ๊ถํ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๋ค
@EnableGlobalMethodSecurity(securedEnabled = true , prePostEnabled = true)
- securedEnabled : @Secured(โROLE_[]โ) ์ด๋ ธํ ์ด์ ํ์ฑํ
- prePostEnabled : @PreAuthorize(โhsaRole(โROLE_[]โ) or hsaRole(โROLE_[]โ)โ) / @PostAuthorize ์ด๋ ธํ ์ด์ ํ์ฑํ
์์ ๊ฐ์ด ํน์ ๋ฉ์๋์ ๊ถํ์ฒ๋ฆฌ๋ฅผ ํ ์ ์์ผ๋ฉฐ ์ต๊ทผ์ ๋์จ @Secured ์ด๋
ธํ
์ด์
์ผ๋ก ์ฝ๊ฒ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
๋ง์ฝ 2๊ฐ ์ด์์ ๊ถํ ์ฒ๋ฆฌ์์๋ @PreAuthorize๋ฅผ ์ฌ์ฉํ์ฌ or ์ ํตํด ๊ถํ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
http ์ ๊ทผ ๊ด๋ จ
์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ์ฌ POST , PUT๊ณผ ๊ฐ์ ์ ๊ทผ์ํ๊ฒ ๋๋ฉด ํต์ ์ด ๋ถ๊ฐํ๋ค๋ ์๋ฌ๋ฅผ ๋ง๋ ๋ณผ ์ ์๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ๋ณด์์ ์ํด POST์ ๊ฐ์ ์ ๊ทผ์ ์ ํํด๋จ๊ธฐ ๋๋ฌธ์ด๋ค.
์ ๊ทผ์ ํ์ฉ ํ๊ธฐ ์ํด์๋ ์๋์ ๊ฐ์ ์ค์ ์ ์ถ๊ฐํด์ค์ผํ๋ค.
http.csrf().disable() // .csrf().disable() / Cross-Site Request Forgery ์น์ทจ์ฝ์ฑ ํ ํฐ
์ต๊ทผ์๋ ์ฑ์์ ํ๋ฉด์ ๊ตฌ์ฑํ๊ณ , React๋ฅผ ํตํด ํ๋ฉด์ ๊ตฌ์ฑํ๋ฉด์ form์ด ์๋ script ์์ฒญ์ ํ๊ธฐ ๋๋ฌธ์ ์์ฃผ ์ฌ์ฉํ์ง ์๋๋ค.
ํ์๊ฐ์
ํ์๊ฐ์ ์ ๊ฐ๋จํ๊ฒ ์ํธํ๋ฅผ ์งํํ์ฌ ๋ฑ๋กํ๊ฒ๋๋ค.
@Autowired
private BCryptPasswordEncoder passwordEncoder;
// ํ์๊ฐ์
@PostMapping("join")
public String join(User user) {
user.setRole("ROLE_USER");
String rawPassword = user.getPassword();
String encPassword = passwordEncoder.encode(rawPassword);
user.setPassword(encPassword);
userRepository.save(user); // ๋น๋ฐ๋ฒํธ ์ํธํ๊ฐ ํ์ฌํ๋ค. ์ํ๋ฆฌํฐ ๋ก๊ทธ์ธ ๋ถ๊ฐ๋ฅ
return "redirect:/login";
}
์์ ๊ฐ์ด ์ ๊ทผํ๋ฉฐ IoC์ ๋ฑ๋กํ์ฌ BCryptPasswordEncoder ์ธ์ฝ๋ฉ์ ํตํด ์ํธํํ ๋ฑ๋ก์ ํด์ค๋ค.
UserDetail
์ํ๋ฆฌํฐ๋ Authentication ๊ฐ์ฒด๋ฅผ ์ธ์ ์ ๋ฃ๊ฒ๋๋๋ฐ ์ด๊ฒ์ ๊ตฌํํ๊ธฐ ์ํด์ UserDetail ์ ์์๋ฐ์ ์ฌ์ฉํ๊ฒ๋๋ค.
User Entity
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role;
@CreationTimestamp
private Timestamp createDate;
}
Authentication ๊ฐ์ฒด
// ์ํ๋ฆฌํฐ๊ฐ /login ์ฃผ์ ์์ฒญ ์ค๋ฉด ๋์์ฑ์ ๋ก๊ทธ์ธ์ ์งํ์ํจ๋ค.
// ๋ก๊ทธ์ธ์ ์งํ์ด ์๋ฃ๊ฐ ๋๋ฉด ์ํ๋ฆฌํฐ session์ผ ๋ง๋ค์ด ์ค๋๋ค (Security ContextHolder)
// ์คํ์ ํธ ํ์
=> Authentication ํ์
๊ฐ์ฒด
// Authentication ์์ User ์ ๋ณด๊ฐ ์์ด์ผ ๋จ.
// User ์ค๋ธ์ ํธํ์
=> UserDetails ํ์
๊ฐ์ฒด
// Security Session => 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> collect = new ArrayList<GrantedAuthority>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// ๊ณ์ ๋ง๋ฃ
@Override
public boolean isAccountNonExpired() {
return true;
}
// ๊ณ์ ์ ๊น
@Override
public boolean isAccountNonLocked() {
return true;
}
// ๋น๋ฐ๋ฒํธ๊ฐ ์ค๋ ๋์๋
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// ๊ณ์ ํ์ฑํ
@Override
public boolean isEnabled() {
// ์ฌ์ดํธ์์ ์ ์ ๊ฐ 1๋
๋์ ๋ก๊ทธ์ธ์ ์ํ๋ค๋ฉด!! ํด๋จผ ๊ณ์ ์ผ๋ก ํ๊ธฐ๋ก ํจ
// ํ์ฌ์๊ฐ - ๋ก๊ธด์๊ฐ => 1๋
์ ์ด๊ณผํ๋ฉด false
return true;
}
}
๋ก๊ทธ์ธ
UserDetailsService ์๋น์ค
// ์ํ๋ฆฌํฐ ์ค์ ์์ loginProcessingUrl("/login")
// /login ์์ฒญ์ด ์ค๋ฉด ์๋์ผ๋ก UserDetailsService ํ์
์ผ๋ก IoC๋์ด ์๋ loadUserByUsername ํจ์ ์คํ
@Service
public class PricipalDetailsService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
// ์ํ๋ฆฌํฐ session = Authentication = UserDetails
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if(user != null) {
return new PrincipalDetails(user);
}
return null;
}
}
[๋ก๊ทธ์ธ์๋ -> SecurityConfig loginProcessingUrl ๋์์ฑ -> PrincipalDetailsService ์์ ์ ๋ณด ํ์ธ -> UserDetails์์ฑ -> PrincipalDeatail ์์ฑ -> session(Authentication)์์ ๋ง์์ ๋ฆฌํด]
์์ ๊ฐ์ ๋ก์ง์ ํตํด ๋ก๊ทธ์ธ์ ํ ์ ์๊ฒ ๋๋ค.
Leave a comment