์ธํ๋ฐ ์คํ๋ง ์ํ๋ฆฌํฐ ๊ฐ์ข๋ฅผ ํ์ตํ๊ณ ์ ๋ฆฌํ ๋ด์ฉ์ ๋๋ค
- Installing MySQL 5.7
- Normal User : user / 123
- Admin User : admin / !@#
๋น๋ฐ๋ฒํธ๋ ํ๋ฌธ์ด ์๋ ๋จ๋ฐฉํฅ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ์ธ์ฝ๋ฉํด์ ์ ์ฅํด์ผ ํ๋ค
- {id}encodePassword
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
- BCryptPasswordEncoder
- NoOpPasswordEncoder
- Pbkdf2PasswordEncoder
- ScryptPasswordEncoder
- StandardPasswordEncoder
@AutoConfigureMockMvc
๋ฅผ ์ฌ์ฉํ๋ฉด MockMvc ํ
์คํธ๋ฅผ ์งํํ ์ ์๋ค
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SpringBootTest {
}
@Test
@WithAnonymousUser
public void index_anonymous() throws Exception {
mockMvc.perform(get(INDEX_PAGE))
.andDo(print())
.andExpect(status().isOk());
}
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "user", roles="USER")
public @interface WithNormalUser {
}
@Test
@WithNormalUser
public void index_user() throws Exception {
mockMvc.perform(get(INDEX_PAGE))
.andDo(print())
.andExpect(status().isOk());
}
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "admin", roles="ADMIN")
public @interface WithAdminUser {
}
@Test
@WithAdminUser
public void admin_admin() throws Exception {
mockMvc.perform(get(ADMIN_PAGE))
.andDo(print())
.andExpect(status().isOk());
}
- SecurityContext ์ ๊ณต
- ํ๋์ Thread์์ Authentication ๊ณต์ ํ๊ธฐ ์ํด์ ThreadLocal ์ฌ์ฉ
Authentication
๋ Principal๊ณผ GrantAuthority ์ ๊ณต- Principal์ ์ฌ์ฉ์์ ๋ํ ์ ๋ณด
- GrantAuthority๋ ๊ถํ ์ ๋ณด (์ธ๊ฐ ๋ฐ ๊ถํ ํ์ธํ ๋ ์ฌ์ฉ)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// ์ฌ์ฉ์ ์ ๋ณด
Object principal = authentication.getPrincipal();
// ์ฌ์ฉ์ ๊ถํ
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// ์ธ์ฆ ์ฌ๋ถ
boolean authenticated = authentication.isAuthenticated();
UserDetailsService ํด๋์ค๋ DAO๋ก ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์์ ์ ์ํํ๋ค. ์ค์ ์ธ์ฆ์ AuthenticationManager ์ธํฐํ์ด์ค๊ฐ ์ํํ๋ค.
- ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ธ์ฆ์ AuthenticationManager๊ฐ ์ํ
- SecurityContext๋ ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ฐ๊ณ ์์
- ๋๋ถ๋ถ AuthenticationManager ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ
ProviderManager
๊ตฌํ์ฒด ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ค
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
- UsernamePasswordAuthenticationToken์ DaoAuthenticationProvider๊ฐ ์ธ์ฆํ๋ ์์ ์ ์ฒ๋ฆฌ
- UserDetailsService ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ํด๋์ค์
loadUserByUsername
๋ฉ์๋๋ฅผ ํธ์ถ - AccountService ํด๋์ค์ loadUserByUsername ๋ฉ์๋๋ User ๊ฐ์ฒด๋ฅผ ๋ฐํ
- User ํด๋์ค๋ UserDetails ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ๊ตฌ์ฒด ํด๋์ค
public class AccountService implements UserDetailsService {
@Autowired
AccountRepository accountRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.findByUsername(username);
if (account == null) {
throw new UsernameNotFoundException(username);
}
return User.builder()
.username(account.getUsername())
.password(account.getPassword())
.roles(account.getRole())
.build();
}
}
java.lang
ํจํค์ง์์ ์ ๊ณตํ๋ ์ฐ๋ ๋ ๋ฒ์ ๋ณ์- ์ฐ๋ ๋ ์์ค์ ๋ฐ์ดํฐ ์ ์ฅ์
- ๊ฐ์ ์ฐ๋ ๋ ๋ด์์๋ง ๊ณต์
- ๊ฐ์ ์ฐ๋ ๋๋ผ๋ฉด ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋ฉ์๋์ ๋งค๊ฐ๋ณ์๋ก ๋๊ฒจ์ค ํ์ ์์
public class AccountContext {
private static final ThreadLocal<Account> ACCOUNT_THREAD_LOCAL
= new ThreadLocal<>();
public static void setAccount(Account account) {
ACCOUNT_THREAD_LOCAL.set(account);
}
public static Account getAccount() {
return ACCOUNT_THREAD_LOCAL.get();
}
}
-
UsernamePasswordAuthenticationFilter
-
SecurityContextPersistenceFilter
HttpSessionSecurityContextRepository
์ ์ฅ์๋ฅผ ํตํด SecurityContext ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค- ๊ธฐ๋ณธ ์ ๋ต์ผ๋ก Http ์ธ์ ์ ์ ์ฅํ๊ณ ๋ณต์ํ๋ค
- Repository์์ ๊ฐ์ ธ์จ SecurityContext ์ ๋ณด๋ฅผ ๋ค์ SecurityContextHolder์ ๋ฃ์ด ์ค๋ค
- FilterChainProxy๋ ์์ฒญ(HttpServletRequest)์ ๋ฐ๋ผ ์ ํฉํ
SecurityFilterChain
์ ์ฌ์ฉ - ๊ธฐ๋ณธ ์ ๋ต์ผ๋ก
DefaultSecurityFilterChain
์ ์ฌ์ฉ DefaultSecurityFilterChain
๋ Filter ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ง๊ณ ์๋ค- SecurityFilterChain์ ์ฌ๋ฌ๊ฐ ๋ง๋ค๊ณ ์ถ์ผ๋ฉด SecurityConfig ํด๋์ค๋ฅผ ์ฌ๋ฌ๊ฐ ๋ง๋ ๋ค
- ์ด ๋ SecurityConfig๊ฐ ์์ถฉํ ์ ์์ผ๋ Order ์ด๋ ธํ ์ด์ ์ ํตํด ์ฐ์ ์์๋ฅผ ์ง์ ํ๋ค
- Filter ๊ฐ์๋ SecurityConfig ์ค์ ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ค
- FilterChainProxy๋ ํํฐ๋ฅผ ํธ์ถํ๊ณ ์คํํ๋ค
- WebAsyncManagerIntergrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CsrfFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareReqeustFilter
- AnonymouseAuthenticationFilter
- SessionManagementFilter
- ExeptionTranslationFilter
- FilterSecurityInterceptor
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/", "/info").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated();
http.formLogin();
http.httpBasic();
}
}
- ์ผ๋ฐ์ ์ธ ์๋ธ๋ฆฟ ํํฐ
- ์๋ธ๋ฆฟ ํํฐ ์ฒ๋ฆฌ๋ฅผ ์คํ๋ง์ ๋ค์ด์๋ ๋น์ผ๋ก ์์ํ๊ณ ์ถ์ ๋ ์ฌ์ฉํ๋ ์๋ธ๋ฆฟ ํํฐ
- ํ๊ฒ ๋น ์ด๋ฆ์ ์ค์
- ์คํ๋ง ๋ถํธ(์๋ ์ค์ ) ์์ด ์คํ๋ง ์ํ๋ฆฌํฐ ์ค์ ํ ๋๋
AbstractSecurityWebApplicationInitializer
๋ฅผ ์ฌ์ฉํด์ ๋ฑ๋ก - ์คํ๋ง ๋ถํธ๋ฅผ ์ฌ์ฉํ ๋๋ ์๋์ผ๋ก ๋ฑ๋ก (
SecurityFilterAutoConfiguration
) FilterChainProxy
๋ springSecurityFilterChain ์ด๋ฆ์ผ๋ก ๋น ๋ฑ๋ก
public abstract class AbstractSecurityWebApplicationInitializer
implements WebApplicationInitializer {
private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";
public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";
...
}
Access Control ๊ฒฐ์ ์ ๋ด๋ฆฌ๋ ์ธํฐํ์ด์ค, ๊ตฌํ์ฒด 3๊ฐ์ง๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ค
- AffirmativeBased : ์ฌ๋ฌ Voter ์ค์ ํ ๋ช ์ด๋ผ๋ ํ์ฉํ๋ฉด ์ธ๊ฐ (๊ธฐ๋ณธ ์ ๋ต)
- ConsensusBased : ๋ค์๊ฒฐ
- UnanimousBased : ๋ง์ฅ์ผ์น
public interface AccessDecisionManager {
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
- Authentication์ด ํน์ ํ Object์ ์ ๊ทผํ ๋ ํ์ํ ConfigAttribute๋ฅผ ๋ง์กฑํ๋์ง ํ์ธ
- WebExpressionVoter : ์น ์ํ๋ฆฌํฐ์์ ์ฌ์ฉํ๋ ๊ธฐ๋ณธ ๊ตฌํ์ฒด, ROLE_XXX ์ผ์นํ๋์ง ํ์ธ
- RoleHierarchyVoter : ๊ณ์ธตํ Role ์ง์
RoleHierarchyImpl
๊ฐ์ฒด์ Role ๊ณ์ธต์ ์ค์
public AccessDecisionManager accessDecisionManager() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
webExpressionVoter.setExpressionHandler(handler);
List<AccessDecisionVoter<? extends Object>> voters = Arrays.asList(webExpressionVoter);
return new AffirmativeBased(voters);
}
- FilterChainProxy๊ฐ ํธ์ถํ๋ ์ํ๋ฆฌํฐ ํํฐ ๋ชฉ๋ก ์ค์ ํ๋์ด๋ฉฐ, ๋๋ถ๋ถ ๊ฐ์ฅ ๋ง์ง๋ง์ ์์นํจ
- ์ธ์ฆ์ด ๋ ์ํ์์ ํน์ ๋ฆฌ์์ค์ ์ ๊ทผํ ์ ์๋์ง Role์ ํ์ธํจ
AccessDecisionManager
๋ฅผ ์ฌ์ฉํด์ Access Control ๋๋ ์์ธ ์ฒ๋ฆฌํ๋ ํํฐ
- FilterSecurityInterceptor ํด๋์ค์ ๋ถ๋ชจ ํด๋์ค
- ํํฐ ์ฒด์ธ์์ ๋ฐ์ํ๋
AccessDeniedException
๊ณผAuthenticationException
์ ์ฒ๋ฆฌํ๋ ํํฐ
- ์ธ์ฆ์ ์คํจํ ๋ ๋ฐ์ํ๋ ์์ธ
- AbstractSecurityInterceptor ํ์ ํด๋์ค์์ ๋ฐ์ํ๋ ์์ธ๋ง ์ฒ๋ฆฌ
- ์ต๋ช ์ฌ์ฉ์๋ผ๋ฉด AuthenticationEntryPoint ์คํ (๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋)
- ์ต๋ช ์ฌ์ฉ์๊ฐ ์๋๋ผ๋ฉด AccessDeniedHandler์๊ฒ ์์
์ธ์ฆ์ด ํ์์๋ ํ์ด์ง๋ฅผ ์ ์ํ ๋ favicon.ico์ ๊ฐ์ ์ ์ ์์์ ์์ฒญํ๋ ๊ฒฝ์ฐ์ FilterChainProxy ๋ฆฌ์คํธ์ ํํฐ๋ฅผ ํ๊ฒ ๋๋ค. ์๋ ์ด๋ฏธ์ง์์ favicon.ico๋ฅผ ์์ฒญํ๋ฉด DefaultLoginPageGeneratingFilter ํํฐ๊ฐ ์ธ์ฆ์ ์ํด์ login ์์ฒญ์ ๋ค์ ํ๊ฒ๋๋ค.
์ด๋ฌํ ์ ์ ์์์ ํํฐ์์ ์ ์ธํ๊ธฐ ์ํด์๋ ๋ค์๊ณผ ๊ฐ์ด WebSecurity์ ignoring
์ ์ค์ ํด์ผ ํ๋ค. CommonLocations์ 5๊ฐ์ ์์์
๋ํด ํํฐ๋ฅผ ๋ฌด์ํ๋๋ก ํ๋ค.
@Override
public void configure(WebSecurity web) {
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
WebSecurityConfigurerAdapter
์์ ๋ฐ์ ํด๋์ค์์ ์ ์ ์์์ ๋ฌด์ํ๋๋ก ์ค์ ํ๊ณ , ๋ค์ ์ธ์ฆ์ด ํ์์๋ ํ์ด์ง๋ฅผ
์ ์ํ๊ฒ ๋๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์คํ๋ง ํํฐ๋ฅผ ์ ์ฉํ์ง ์๊ณ ๋ฐ๋ก ์ ์ ์์์ ์ ๋ฌํ๋ค.
์คํ๋ง MVC์ Async ๊ธฐ๋ฅ์ ์ฌ์ฉํ ๋๋ SecurityContext๋ฅผ ๊ณต์ ํ๋๋ก ๋์์ฃผ๋ ํํฐ
- PreProcess: SecurityContext๋ฅผ ์ค์ ํ๋ค.
- Callable: ๋น๋ก ๋ค๋ฅธ ์ฐ๋ ๋์ง๋ง ๊ทธ ์์์๋ ๋์ผํ SecurityContext๋ฅผ ์ฐธ์กฐํ ์ ์๋ค.
- PostProcess: SecurityContext๋ฅผ ์ ๋ฆฌ(clean up)ํ๋ค.
MVC ์์ฒญ์ด ๋ค์ด์ค๋ ์ฐ๋ ๋ ์์
์ ์๋ฃํ๊ณ ๋์๋ SecurityContextHolder
์์๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋์ผํ๊ฒ ์ป์ ์ ์๋ค. ๊ทธ ์ญํ ์
WebAsyncManagerIntegrationFilter๊ฐ ์ํํ๋ค.
@Controller
public class SampleController {
@GetMapping("/async-handler")
@ResponseBody
public Callable<String> asyncHandler() {
// http-nio-8080-exec ์ฐ๋ ๋
SecurityLogger.log("MVC");
return () -> {
// task-1 ์ฐ๋ ๋
SecurityLogger.log("Callable");
return "Async Handler";
};
}
}
WebAsyncManagerIntegrationFilter๋ SecurityContextCallableProcessingInterceptor
๋ฅผ ์ฌ์ฉํด์ SecurityContextHolder์
SecurityContext ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค.
- SecurityContextHolder ๊ธฐ๋ณธ ์ ๋ต์
ThreadLocal
- @Async ์๋น์ค์์ SecurityContextHolder๊ฐ ๊ณต์ ๋์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํจ
- SecurityContextHolder ์ ๋ต์ ๋ค์ ์ฝ๋์ ๊ฐ์ด ๋ฐ๊พธ๋ฉด ์ฐ๋ ๋ ๊ณ์ธต ์ฌ์ด์์๋ SecurityContextHolder ์ ๋ณด๊ฐ ๊ณต์ ๋๋ค
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
SecurityContextRepository
๋ฅผ ์ฌ์ฉํด์ ๊ธฐ์กด์ SecurityContext ์ ๋ณด๋ฅผ ์ฝ์ด์ค๊ฑฐ๋ ์ด๊ธฐํํ๋ค
- ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉํ๋ ์ ๋ต์ HTTP Session ์ฌ์ฉ (HttpSessionSecurityContextRepository)
- Spring-Session๊ณผ ์ฐ๋ํ์ฌ ์ธ์ ํด๋ฌ์คํฐ๋ฅผ ๊ตฌํํ ์ ์๋ค
์๋ต ํค๋์ ์ํ๋ฆฌํฐ ๊ด๋ จ ํค๋๋ฅผ ์ถ๊ฐํด์ฃผ๋ ํํฐ
- XContentTypeOptionsHeaderWriter : ๋ง์ ํ์ ์ค๋ํ ๋ฐฉ์ด.
- XXssProtectionHeaderWriter : ๋ธ๋ผ์ฐ์ ์ ๋ด์ฅ๋ XSS ํํฐ ์ ์ฉ.
- CacheControlHeadersWriter : ์บ์ ํ์คํ ๋ฆฌ ์ทจ์ฝ์ ๋ฐฉ์ด.
- HstsHeaderWriter : HTTPS๋ก๋ง ์ํตํ๋๋ก ๊ฐ์ .
- XFrameOptionsHeaderWriter : clickjacking ๋ฐฉ์ด.
CSRF ์ดํ ๋ฐฉ์ง ํํฐ
- ์ธ์ฆ๋ ์ ์ ์ ๊ณ์ ์ ์ฌ์ฉํด์ ์ ์์ ์ธ ๋ณ๊ฒฝ ์์ฒญ์ ๋ง๋ค์ด ๋ณด๋ด๋ ๊ธฐ๋ฒ
- ์๋ํ ์ฌ์ฉ์๋ง ๋ฆฌ์์ค๋ฅผ ๋ณ๊ฒฝํ ์ ์๋๋ก ํ์ฉํ๋ ํํฐ
- CSRF ํ ํฐ์ ์ฌ์ฉํ์ฌ ์ฒดํฌ
form ํ์์ hidden ํ์ ์ผ๋ก csrf ํ ํฐ ๊ฐ์ด ํฌํจ๋์ด ์๋ค
Postman์ ์ด์ฉํด์ /signup
POST ์์ฒญ์ ๋ณด๋ด๋ฉด, 401 Unauthorized ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. ์ด์ ๋ csrf ํ ํฐ ๊ฐ์ด ์์ด์ ํผ ์ธ์ฆ์ด ๋์ง ์๊ธฐ
๋๋ฌธ์ ๋ฐ์ํ๋ค.
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SignUpControllerTest {
@Autowired
MockMvc mockMvc;
// SignUp Get ์์ฒญ
@Test
public void signUpForm() throws Exception {
mockMvc.perform(get("/signup"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("_csrf")));
}
// SignUp Post ์์ฒญ, csrf ํ ํฐ์ ํฌํจ
@Test
public void processSignUp() throws Exception {
mockMvc.perform(post("/signup")
.param("username", "jayden")
.param("password", "123")
.with(csrf()))
.andDo(print())
.andExpect(status().is3xxRedirection());
}
}
http.csrf().disable();
์ฌ๋ฌ LogoutHanlder๋ฅผ ์ฌ์ฉํ์ฌ ๋ก๊ทธ์์์ ํ์ํ ์์
์ ์ํํ๋ค. ๊ทธ๋ฆฌ๊ณ LogoutSuccessHandler
๋ฅผ ์ฌ์ฉํด์
๋ก๊ทธ์์ ํ์ฒ๋ฆฌ๋ฅผ ํ๋ค.
- CsrfLogoutHandler
- SecurityContextLogoutHandler
- SimpleUrlLogoutSuccessHandler
ํผ ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํ๋ ์ธ์ฆ ํํฐ
- ์ฌ์ฉ์๊ฐ ํผ์ ์
๋ ฅํ ์ ๋ณด๋ฅผ ํ ๋๋ก Authentication ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ
AuthenticationManager
๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ์ ์๋ํ๋ค - AuthenticationManager(ProviderManager)๋ ์ฌ๋ฌ
AuthenticationProvider
๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ์ ์๋ํ๋๋ฐ, ๊ทธ ์คDaoAuthenticationProvider
๋ UserDetailsService๋ฅผ ์ฌ์ฉํ์ฌ UserDetails ์ ๋ณด๋ฅผ ๊ฐ์ ธ์์ ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ์ ๋ณด์ ๋์ผํ์ง ๋น๊ตํ๋ค
๊ธฐ๋ณธ ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ์์ฑํ๋ ํํฐ
http.formLogin()
.usernameParameter("app_username")
.passwordParameter("app_password");
์ปค์คํ
๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ๋ฑ๋กํ๋ฉด FilterChainProxy์์ DefaultLoginPageGeneratingFilter
์ DefaultLogoutPageGeneratingFilter
๋ ํํฐ๊ฐ ์ ์ธ๋จ
http.formLogin()
.loginPage("/login");
๊ธฐ๋ณธ ๋ก๊ทธ์์ ํ์ด์ง๋ฅผ ์์ฑํ๋ ํํฐ
๋ก๊ทธ์ธ/๋ก๊ทธ์์ ํผ ํ์ด์ง๋ฅผ ์ปค์คํฐ๋ง์ด์ง ํ๊ธฐ ์ํด์ LogInOutController
๋ฅผ ์์ฑํ๋ค. ์ด ์ปจํธ๋กค๋ฌ๋ Get ์์ฒญ์ผ๋ก ๋ก๊ทธ์ธ/๋ก๊ทธ์์
ํ์ด์ง๋ฅผ ๋ฐํํ๋ค.
@Controller
public class LogInOutController {
@GetMapping("/login")
public String loginForm() {
return "/login";
}
@GetMapping("/logout")
public String logoutForm() {
return "/logout";
}
}
SpirngSecurity
์ค์ ์์ ๋ก๊ทธ์ธ ํผ ํ์ด์ง URL๊ณผ ๋ก๊ทธ์์ URL์ ์ค์ ํ๋ค.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/", "/info", "/signup").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
http.httpBasic();
http.formLogin()
.loginPage("/login")
.permitAll();
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/");
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
- Http Basic ์ธ์ฆ์ ์ง์ํ๋ ํํฐ
- ์์ฒญ ํค๋์ ์์ด๋์ ํจ์ค์๋๋ฅผ ๋ณด๋ด๋ฉด ๋ธ๋ผ์ฐ์ ๋๋ ์๋ฒ๊ฐ ๊ทธ ๊ฐ์ ์ฝ์ด์ ์ธ์ฆํ๋ ๋ฐฉ์
- ์ ๋ณด๋ Base64 ์ธ์ฝ๋ฉ ๋์ด ๋ณด๋ด์ง๊ณ ์ฝ์ ๋ ๋ค์ ๋์ฝ๋ฉํด์ ๊ฐ์ ์ฝ๋๋ค
- ์ค๋ํํ๋ฉด ์์ฒญ ์ ๋ณด๋ฅผ ์ฝ๊ฒ ์ทจ๋ํ๋ ์ํ์ด ์๊ธฐ ๋๋ฌธ์ HTTPS๋ฅผ ์ฌ์ฉํ ๊ฒ์ ๊ถ์ฅ
http.httpBasic();
ํ์ฌ ์์ฒญ๊ณผ ๊ด๋ จ ์๋ ์บ์๋ ์์ฒญ์ด ์๋์ง ์ฐพ์์ ์ ์ฉํ๋ ํํฐ
- ์บ์๋ ์์ฒญ์ด ์๋ค๋ฉด, ํ์ฌ ์์ฒญ ์ฒ๋ฆฌ
- ์บ์๋ ์์ฒญ์ด ์๋ค๋ฉด, ์บ์๋ ์์ฒญ ์ฒ๋ฆฌ
๋์๋ณด๋(๋ก๊ทธ์ธ์ด ํ์ํ ํ์ด์ง) ํ์ด์ง๋ฅผ ์ ์ํ๋ ค๊ณ ํ๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๋ค. ๋ก๊ทธ์ธ ํ์ด์ง์์ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ํํ๊ณ ๋๋ฉด,
RequestCacheAwareFilter
์์ ์บ์ํ ์์ฒญ(๋์๋ณด๋ ํ์ด์ง๋ก ์ด๋ํ๋ ค๋ ์์ฒญ)์ ์ํํ๋ค.
public class RequestCacheAwareFilter extends GenericFilterBean {
private RequestCache requestCache;
public RequestCacheAwareFilter() {
this(new HttpSessionRequestCache());
}
public RequestCacheAwareFilter(RequestCache requestCache) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
// ์บ์๋ ์์ฒญ์ด ์๋์ง ์ฒดํฌํ๊ณ ํ์ฌ ์์ฒญ์ ์ฒ๋ฆฌํ ์ง ์บ์๋ ์์ฒญ์ ์ฒ๋ฆฌํ ์ง ๊ฒฐ์
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
}
์ํ๋ฆฌํฐ ๊ด๋ จ ์๋ธ๋ฆฟ API๋ฅผ ๊ตฌํํด์ฃผ๋ ํํฐ
- HttpServletRequest#authenticate(HttpServletResponse)
- HttpServletRequest#login(String, String)
- HttpServletRequest#logout()
- AsyncContext#start(Runnable)
SecurityContext์ Authentication์ด null ๊ฐ์ด๋ฉด, ์ต๋ช Authentication์ ์์ฑํด์ ๋ฃ์ด์ค๋ค. Authentication์ด null ๊ฐ์ด ์๋๋ฉด, ์๋ฌด์ผ๋ ํ์ง ์๋ ํํฐ์ด๋ค. (null object pattern)
์คํ๋ง ์ํ๋ฆฌํฐ๋ ๋ณ๋์ ์ค์ ์ด ์์ด๋ AnonymousUser๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์์ฑํ๋ค. Principal์ anonymousUser์ด๊ณ ๊ถํ์ ROLE_ANONYMOUS๋ก ์ค์ ํ๋ค.
- ์ธ์
๋ณ์กฐ ๋ฐฉ์ง ์ ๋ต ์ค์
- ์ธ์ ๋ณ์กฐ ๋ฐฉ์ง ์ ๋ต์ผ๋ก changeSessionId๋ก ์ค์
http.sessionManagement() .sessionFixation() .changeSessionId();
- ์ ํจํ์ง ์์ ์ธ์ ์ ๋ฆฌ๋ค์ด๋ ํธ ์ํฌ URL ์ค์
- ๋์์ฑ ์ ์ด
- ์ธ์ ๊ฐ์ ์ ์ด
- ์ถ๊ฐ ๋ก๊ทธ์ธ์ ๋ง์์ง ์ฌ๋ถ (๊ธฐ๋ณธ๊ฐ์ false)
http.sessionManagement() .maximumSessions(1) .maxSessionsPreventsLogin(true);
- ์ธ์
์์ฑ ์ ๋ต
- ALWAYS
- NEVER
- IF_REQUIRED
- STATELESS
try-catch
๊ตฌ๋ฌธ์ผ๋ก ๊ฐ์ธ๊ณFilterSecurityInterceptor
๋ฅผ ์ฒ๋ฆฌํ๋ค- FilterSecurityInterceptor๋
AccessDecisionManager
๋ฅผ ์ด์ฉํด์ ์ธ๊ฐ ์ฒ๋ฆฌ๋ฅผ ํจ - AuthenticationEntryPoint, AccessDeniedException ์์ธ๋ฅผ ์ฒ๋ฆฌํจ
- Http ๋ฆฌ์์ค ์ํ๋ฆฌํฐ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ ํํฐ
AccessDecisionManager
๋ฅผ ์ฌ์ฉํ์ฌ ์ธ๊ฐ๋ฅผ ์ฒ๋ฆฌ
http.authorizeRequests()
.mvcMatchers("/", "/info", "/signup").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
- ์ธ์ ์ด ์ฌ๋ผ์ง๊ฑฐ๋ ๋ง๋ฃ๊ฐ ๋๋๋ผ๋ ์ฟ ํค ๋๋ DB๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ฅ๋ ํ ํฐ ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ์ ์ง์ํ๋ ํํฐ
ํ์ด์ง์ ์ ์ํ๋ฉด ์๋ฒ์์ ์ธ์ ์ด ์์ฑ๋๊ณ ์น ๋ธ๋ผ์ฐ์ ์ฟ ํค์ ์ธ์ ์์ด๋ ์ ๋ณด๊ฐ ๋ด๊ธด๋ค. ๋ก๊ทธ์ธ ํ๊ณ ๋๋ฉด ์๋ฒ๋ ํด๋น ์ธ์ ์ ์ธ์ฆ๋ ์ธ์ ์ผ๋ก ์ทจ๊ธํ๋ค.
์ฌ์ฉ์๊ฐ ์น ๋ธ๋ผ์ฐ์ ์ฟ ํค์์ ์ธ์ ์์ด๋๋ฅผ ์ญ์ ํ๊ฒ ๋๋ฉด, ์ธ์ฆ๋ ์ธ์ ์ด ์๋๊ธฐ ๋๋ฌธ์ ์๋ฒ๋ ๋ค์ ๋ก๊ทธ์ธ ์ฐฝ์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋๋ค.
์ธ์
์์ด๋๋ฅผ ์ญ์ ํ๋ฉด SecurityContextHolder
์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ ์๋ฒ๋ ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๋ก ํ๋จํ๊ณ ์ธ์ฆ์ด ํ์ํ ํ์ด์ง์ ์ ์์ ๋ง๋๋ค.
๋ค์๊ณผ ๊ฐ์ด rememberMe
์ค์ ์ ํ๊ณ ๋ก๊ทธ์ธํ ๋, remember-me ํ๋ผ๋ฏธํฐ๋ฅผ ๋๊ธฐ๋ฉด remember-me ์ฟ ํค ์ ๋ณด๊ฐ ์๊ธฐ๊ฒ ๋๋ค.
remember-me ์ฟ ํค์๋ ์ฌ์ฉ์ ์ด๋ฆ๊ณผ ์ ํจ ๊ธฐ๊ฐ ์ ๋ณด๋ฅผ ํฌํจํ๊ณ ์๋ค.
http.rememberMe()
.userDetailsService(accountService)
.key("remember-me");
์์์ ํ ๊ฒ์ฒ๋ผ ๋ค์ ์ธ์
์์ด๋๋ฅผ ์ญ์ ํ๊ณ ๋์ ๋ค์ ์ธ์ฆ์ด ํ์ํ ํ์ด์ง๋ฅผ ์์ฒญํ๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ์ง ์๋๋ค. ํํฐ ์ฒด์ธ ๋ชฉ๋ก์์
RememberMeAuthenticationFilter
๊ฐ RememberMeAuthenticationToken ์ ๋ณด๋ฅผ ์ด์ฉํด์ ์ธ์ฆํ๊ณ , ์ธ์ฆ๋ ์ ๋ณด๋ฅผ ๋ค์
SecurityContextHolder
์ ๋ฃ์ด์ค๋ค.
ํฌ๋กฌ ์น ๋ธ๋ผ์ฐ์ ์์ ํ์ฌ ์ ์ํ ํ์ด์ง์ ์ฟ ํค ์ ๋ณด๋ฅผ ์ฝ๊ฒ ํ์ธํ ์ ์๋ ํ๋ฌ๊ทธ์ธ์ผ๋ก EditThisCookie๋ฅผ ์ค์นํด์ ์ฌ์ฉํ๋ค.
Filter๋ฅผ ์์ฑํ๋ ๊ฒ์ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ด ์์ง๋ง ์ด๋ฒ์ ์ถ๊ฐํ๋ LoggingFilter๋ GenericFilterBean
ํด๋์ค๋ฅผ ์์ ๋ฐ์์ ๊ตฌํํ๋๋ก ํ๋ค. GenericFilterBean
ํด๋์ค์๋ ๊ธฐ๋ณธ์ ์ธ ์ค์ ์ด ๋์ด ์๊ธฐ ๋๋ฌธ์
์์ ๋ฐ์ ํด๋์ค๊ฐ doFilter ๋ฉ์๋๋ง ์ค๋ฒ๋ผ์ด๋ ํ๋ฉด ๋๋ค.
public class LoggingFilter extends GenericFilterBean {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
chain.doFilter(request, response);
stopWatch.stop();
logger.info(stopWatch.prettyPrint());
}
}
์๋ก ์์ฑํ LoggingFilter
ํํฐ๋ฅผ ํํฐ ๋ชฉ๋ก์์ ์ํ๋ ์์น๋ก ์ค์ ํ ์ ์๋ค. WebAsyncManagerIntegrationFilter
ํํฐ๋ ํํฐ ๋ชฉ๋ก์์ ๊ฐ์ฅ ์ฒซ ๋ฒ์งธ์ ์์นํ๋ ํํฐ์ด๋ค.
http.addFilterBefore(new LoggingFilter(), WebAsyncManagerIntegrationFilter.class);
- ์คํ๋ง ์ํ๋ฆฌํฐ ๊ธฐ๋ฅ์ ์น ๋๋ ๋ฐ์คํฌํ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ฌ์ฉํ ์ ์๋๋ก ๋์์ฃผ๋ ๊ธฐ๋ฅ
- ๋ฉ์๋ ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ๋ค์๊ณผ ๊ฐ์ ์ค์ ํด๋์ค๋ฅผ ์์ฑํด์ผ ํ๋ค
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class MethodSecurity {
}
- ๊ถํ์ ๋ฐ๋ผ ํน์ ๋ฉ์๋๋ฅผ ์คํ ์ ๋ฌด๋ฅผ ์ค์ ํ๊ณ ์ถ์ผ๋ฉด
@Secured
์ ๋ ธํ ์ด์ ๊ณผ ํจ๊ป ROLE_USER ์ด๋ฆ์ ์ถ๊ฐํ๋ค @Secured
,@RolesAllowed
,PreAuthorize
์ ๋ ธํ ์ด์ ๋ค์ dashboard ๋ฉ์๋๋ฅผ ํธ์ถํ๊ธฐ ์ ์ ๊ถํ ๊ฒ์ฌ๋ฅผ ์ํํ๋ค@PostAuthorize
์ ๋ ธํ ์ด์ ์ dashboard ๋ฉ์๋๋ฅผ ์คํํ ์ดํ์ ๊ถํ์ ์ฒดํฌํ๋ค
@Secured("ROLE_USER")
@RolesAllowed("ROLE_USER")
@PreAuthorize("hasRole(USER)")
public void dashboard() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
System.out.println("================");
System.out.println(authentication);
System.out.println(authentication.getName());
}
๊ธฐ์กด์ Principal ์ ๋ณด๋ฅผ ์ป์ผ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ค. ์ฒซ๋ฒ์งธ ๋ฐฉ๋ฒ์ ํตํด ์ป์ Principal ๊ฐ์ฒด์์๋ ์ฌ์ฉ์ ์ด๋ฆ ์ ๋ณด๋ง ๊ฐ์ ธ์ฌ ์ ์๋ ๋จ์ ์ด ์๋ค. ๋ ๋ฒ์งธ ๋ฐฉ๋ฒ์ ์ฐ๋ฆฌ๊ฐ ์ ์ธํ ๋๋ฉ์ธ ํ์ ์ ํด๋์ค๋ก ๋ณํํ๋ฉด ์ด๋ฆ, ์ญํ , ๋น๋ฐ๋ฒํธ ์ ๋ณด๋ฅผ ์ป์ ์ ์๋ค.
-
Argument์ Principal๋ฅผ ์ถ๊ฐ
@GetMapping(value = "/dashboard") public String dashboard(Model model, Principal principal) { model.addAttribute("message", "Hello " + principal.getName()); AccountContext.setAccount(accountRepository.findByUsername(principal.getName())); sampleService.dashboard(); return "dashboard"; }
-
SecurityContextHolder์์ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
์ด๋ฒ์ ์ดํด๋ณผ @AuthenticationPrincipal
์ ๋
ธํ
์ด์
์ ์ฌ์ฉํ๋ฉด ์ฐ๋ฆฌ๊ฐ ์ ์ธํ ๋๋ฉ์ธ ํ์
์ Principal ์ ๋ณด๋ฅผ
๋งค๊ฐ๋ณ์๋ก ๋ฐ์ ์ ์๋ค. ์ ๋
ธํ
์ด์
์ ํ์ธํ๊ณ ArgumentResolver
๊ฐ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ง๋ค์ด์ ๋ฃ์ด์ค๋ค.
@GetMapping(value = "/")
public String index(Model model, @AuthenticationPrincipal UserAccount userAccount) {
if (userAccount == null) {
model.addAttribute("message", "Hello Spring Security");
} else {
model.addAttribute("message", "Hello " + userAccount.getUsername());
}
return "index";
}
UserAccount ํด๋์ค๋ User ํด๋์ค๋ฅผ ์์ ๋ฐ์ ๊ตฌํํ๋ค.
@Getter
public class UserAccount extends User {
private Account account;
public UserAccount(Account account) {
super(account.getUsername(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_" + account.getRole())));
this.account = account;
}
}
UserAccount ๊ฐ์ฒด์์ Account ์ ๋ณด๋ง ๊ฐ์ ธ์ค๊ณ ์ถ์ ๋๋ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ค
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentUser {
}
@GetMapping(value = "/")
public String index(Model model, @CurrentUser Account account) {
if (account == null) {
model.addAttribute("message", "Hello Spring Security");
} else {
model.addAttribute("message", "Hello " + account.getUsername());
}
return "index";
}