Open skarltjr opened 3 years ago
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
-SecurityContextHolder SecurityContext 제공, 기본적으로 ThreadLocal을 사용한다. 안에 들어있는 객체는 모두 인증된 객체
-SecurityContext Authentication 제공.
-Authentication Principal과 GrantAuthority 제공.
-Principal “누구"에 해당하는 정보. -UserDetailsService에서 리턴한 그 객체. 객체는 UserDetails 타입.
-GrantAuthority: “ROLE_USER”, “ROLE_ADMIN”등 Principal이 가지고 있는 “권한”을 나타낸다. 인증 이후, 인가 및 권한 확인할 때 이 정보를 참조한다.
-UserDetails 애플리케이션이 가지고 있는 유저 정보와 스프링 시큐리티가 사용하는 Authentication 객체 사이의 어댑터.
-UserDetailsService 유저 정보를 UserDetails 타입으로 가져오는 DAO (Data Access Object) 인터페이스.
< AuthenticationManager 구현체가 ProviderManager >
인자로 받은 Authentication이 유효한 인증인지 확인하고 Authentication 객체를 리턴한다.
인증을 확인하는 과정에서 비활성 계정, 잘못된 비번, 잠긴 계정 등의 에러를 던질 수 있다.
Authentication authenticate(Authentication authentication) throws AuthenticationException;
인자로 받은 Authentication 사용자가 입력한 인증에 필요한 정보(username, password)로 만든 객체. (폼 인증인 경우) Authentication
유효한 인증인지 확인 사용자가 입력한 password가 UserDetailsService를 통해 읽어온 UserDetails 객체에 들어있는 password와 일치하는지 확인 해당 사용자 계정이 잠겨 있진 않은지, 비활성 계정은 아닌지 등 확인
Authentication 객체를 리턴 Authentication
스프링의 Transaction을 쓰는것도 ThreadLocal을 사용하는것
Java.lang 패키지에서 제공하는 쓰레드 범위 변수. 즉, 쓰레드 수준의 데이터 저장소. -같은 쓰레드 내에서만 공유. -따라서 같은 쓰레드라면 해당 데이터를 메소드 매개변수로 넘겨줄 필요 없음. -SecurityContextHolder의 기본 전략.
ex)
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();
}
}
위와 같은 클래스를 설정하여 사용하면 파라미터로 Account정보를 넘겨주지않아도 된다.
SecurityContextHolder에서 이미 사용하기때문에 이것을 이용한다. 내부적으로 이런 방법으로 구현되어있다~
AuthenticationManager가 인증을 마친 뒤 리턴 받은 Authentication 객체의 행방은?
UsernamePasswordAuthenticationFilter -폼 인증을 처리하는 시큐리티 필터 -★인증된 Authentication 객체를 SecurityContextHolder에 넣어주는 필터★ -SecurityContextHolder.getContext().setAuthentication(authentication)
SecurityContextPersistenceFilter -SecurityContext를 HTTP session에 캐시(기본 전략)하여 여러 요청에서 Authentication을 ★공유할 수 있 공유하는 필터. -SecurityContextRepository를 교체하여 세션을 HTTP session이 아닌 다른 곳에 저장하는 것도 가능하다
스프링 시큐리티 Filter와 FilterChainProxy
이 모든 필터는 FilterChainProxy가 호출한다. FilterChainProxy -> Filters
이 필터들은 config(SecurityConfig)에 따라 생성된다. 만약 config에서 http.httpBasic():을 설정하지 않는다면 BasicAuthenticationFilter는 필터에 추가되지 않는다.
Access Control 결정을 내리는 인터페이스로, 구현체 3가지를 기본으로 제공한다.
AccessDecisionVoter
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* .mvcMatchers("/User").hasRole("USER") 일 때 Admin은 유저이면서 admin롤을 갖는다
* 따라서 admin도 user role에 접근할 수 있어야하는데 이대로면 admin은 user role로 접근할 수 없다.
*/
public SecurityExpressionHandler securityExpressionHandler() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
/** 게층구조를 설정해주고*/
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
/** 핸들러에 계층구조를 설정*/
return handler;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**").permitAll()
.mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
.mvcMatchers("/User").hasRole("USER")
.anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
.expressionHandler(securityExpressionHandler());
//.and() //and로 이어가거나 아니면 Request내용 이후 다른 내용을 추가할 때 새로 http로 시작해주거나
http
.formLogin(); //로그아웃도 가능
http
.httpBasic(); //httpBasic도 사용하겠다
}
}
AccessDecisionManager - 이미 인증을 거친 사용자가 특정한 리소스, 매서드에 접근할 때 이를 허용할 지를 판단 = 인가 는 어디에서 사용되는가 ?
인증 과정에서 발생하는 예외 처리 ExceptionTranslationFilter
AuthenticationException 발생 시 / 인증에 실패한 경우 -> 다시 인증을 하도록
AccessDeniedException 발생 시 / 인증은 됐는데
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**").permitAll()
.mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
.mvcMatchers("/User").hasRole("USER")
.anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
.expressionHandler(securityExpressionHandler());
//.and() //and로 이어가거나 아니면 Request내용 이후 다른 내용을 추가할 때 새로 http로 시작해주거나
http
.formLogin(); //로그아웃도 가능
http
.httpBasic(); //httpBasic도 사용하겠다
}
/** 요청 - http filter 적용 / static resource - web */
@Override
public void configure(WebSecurity web) throws Exception {
// web.ignoring().mvcMatchers("/favicon.ico"); // 파비콘 요청은 시큐리티 안걸리도록 그러나 다 설정하기 귀찮으니
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
// 정적리소스 모두에 대해 ignore 적용
}
Generally mvcMatcher is more secure than an antMatcher. As an example:
antMatchers("/secured") matches only the exact /secured URL mvcMatchers("/secured") matches /secured as well as /secured/, /secured.html, /secured.xyz
★ @Async어노테이션을 서비스에서 사용하는경우
쓰레드가 다르기 때문에 SecurityContext를 공유받지 못한다. -> 설정
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**").permitAll()
.mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
.mvcMatchers("/User").hasRole("USER")
.anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
.expressionHandler(securityExpressionHandler());
//.and() //and로 이어가거나 아니면 Request내용 이후 다른 내용을 추가할 때 새로 http로 시작해주거나
http
.formLogin(); //로그아웃도 가능
http
.httpBasic(); //httpBasic도 사용하겠다
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
// 어디까지 자원을 공유할 것인가? -> 기본은 쓰레드로컬
}
SecurityContextRepository를 사용해서 기존의 SecurityContext를 읽어오거나 초기화 한다.
응답 헤더에 시큐리티 관련 헤더를 추가해주는 필터
thymeleaf 2.1이상부터는 form에 대해서 자동으로 csrf토큰을 설정해준다.
@SpringBootTest
@AutoConfigureMockMvc
class SignUpControllerTest {
@Autowired
MockMvc mockMvc;
@Test
@DisplayName("회원가입")
void signUp() throws Exception {
mockMvc.perform(get("/signup"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("_csrf")));
}
@Test
@DisplayName("회원가입처리")
void processSignUp() throws Exception {
mockMvc.perform(post("/signup")
.param("username", "kiseok")
.param("password", "123")
.with(csrf()))
.andDo(print())
.andExpect(status().is3xxRedirection());
}
}
여러 LogoutHandler를 사용하여 로그아웃시 필요한 처리를 하며 이후에는 LogoutSuccessHandler를 사용하여 로그아웃 후처리를 한다.
LogoutHandler // 로그아웃 처리 CsrfLogoutHandler SecurityContextLogoutHandler
LogoutSuccessHandler // 로그아웃 이후 어떻게 할지 SimplUrlLogoutSuccessHandler
로그아웃 필터 설정
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
// .logoutRequestMatcher()
// .invalidateHttpSession(true)
// .deleteCookies()
// .addLogoutHandler()
// .logoutSuccessHandler();
폼 로그인을 처리하는 인증 필터
Basic 인증이란?
현재 요청과 관련 있는 캐시된 요청이 있는지 찾아서 적용하는 필터. 캐시된 요청이 없다면, 현재 요청 처리 캐시된 요청이 있다면, 해당 캐시된 요청 처리 ex) 로그인이 필요한 admin페이지를 갈 때 로그인이 안된상태라면 로그인창을 먼저띄워준다. 그 다음 캐시에 저장된 원래 목적지 (admin페이지)로 가도록
기본으로 만들어 사용할 “익명 Authentication” 객체를 설정할 수도 있다.
http.anonymous()
.principal()
.authorities()
.key()
세션 변조 방지 전략 설정: sessionFixation 세션 변조
// 즉 공격자가 웹 서버에서 쿠키로 받아온 sessionId를 희생자에게 보내면 희생자는 이걸갖고 웹서버에 접근
// 그럼 공격자 = 희생자로 웹서버가 취급 -> 공격자가 희생자의 정보등을 사용할 수 있게된다.
-> 그럼 인증을 마칠 때 마다 session을 변경하도록 해주면 방지가능
none newSession migrateSession (서블릿 3.0- 컨테이너 사용시 기본값) changeSessionId (서브릿 3.1+ 컨테이너 사용시 기본값)
유효하지 않은 세션을 리다이렉트 시킬 URL 설정 (로그아웃했을 때) invalidSessionUrl
동시성 제어: maximumSessions (한 아이디가 무한대로 로그인) 추가 로그인을 막을지 여부 설정 (기본값, false)
http
.sessionManagement()
.maximumSessions(1) // 한 곳에서만 로그인가능 - 아이디 돌려쓰거나 못하도록
.maxSessionsPreventsLogin(true); // 기본값은 false인데 만약 한곳에서만 로그인이 가능한데
// 2곳에서 로그인을 한 경우 true면 나중에 로그인하려는곳에서 막힘 false는 먼저 로그인한 곳 만료됨
세션 생성 전략: sessionCreationPolicy IF_REQUIRED (기본값) NEVER STATELESS // 진짜 session을 쓰지 않는다 ALWAYS
인증, 인가 에러 처리를 담당하는 필터
ExceptionTranslationFilter -> FilterSecurityInterceptor // 14번필터가 15번필터를 크게 감싼 형태
AuthenticationException -> AuthenticationEntrypoint // 로그인한 유저만 접근가능한 곳에 그냥 접근했을 때
AccessDeniedException -> AccessDeniedHandler // 인증은 했는데 AdminRole만 접근가능한곳에 UserRole로 접근한 경우
http
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
log.info(username + " is denied to access " + httpServletRequest.getRequestURI()); //서버단에 로그도 남겨주고
httpServletResponse.sendRedirect("/access-denied");
}
});
++ controller 와 html도 추가해줘야한다
HTTP 리소스 시큐리티 처리를 담당하는 필터. AccessDecisionManager를 사용하여 인가를 처리한다.
http
.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**","/signup").permitAll()
.mvcMatchers("/admin", "/dashBoard").hasRole("ADMIN")
.mvcMatchers("/User").hasRole("USER")
.anyRequest().authenticated() // 기타등등은 인증을 하면 접근할 수 있다
.expressionHandler(securityExpressionHandler());
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Authentication과 Authorization참조
<div th:if="${#authorization.expr('isAuthenticated()')}">
<h2 th:text="${#authentication.name}"></h2>
<a href="/logout" th:href="@{/logout}">Logout</a>
</div>
<div th:unless="${#authorization.expr('isAuthenticated()')}">
<a href="/login" th:href="@{/login}">Login</a>
</div>
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;
}
public Account getAccount() {
return account;
}
}
메타어노테이션으로 + expression을 통해 익명 Authentication인 경우 (“anonymousUser”)에는 null 아닌 경우에는 account 필드를 사용한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentUser {
}
스프링 시큐리티 http vs web config https://stackoverflow.com/questions/56388865/spring-security-configuration-httpsecurity-vs-websecurity
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/login", "/register", "/api/public/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/login", "/register", "/api/public/**").permitAll()
.anyRequest().authenticated();
}
configure(WebSecurity web) Endpoint used in this method ignores the spring security filters, security features (secure headers, csrf protection etc) are also ignored and no security context will be set and can not protect endpoints for Cross-Site Scripting, XSS attacks, content-sniffing.
configure(HttpSecurity http) Endpoint used in this method ignores the authentication for endpoints used in antMatchers and other security features will be in effect such as secure headers, CSRF protection, etc.
websecurity에서 ignoring은 말 그대로 모든 필터들을 무시 : 즉 시큐리티 필터체인을 아예 거치지않고 무시
httpsecurity는 인가에 대해 무시 but 나머지 특성(secure headers, CSRF protection 등)들은 유지
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**")
.antMatchers("/publics/**"); // 그렇기 때문에 만약 여기서 publics에 대해 ignoring을 한다면 필터체인을 아예 거치지않기때문에
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/publics/**").hasRole("USER") // no effect - 당연히 여기서 설정할 필요조차 없으며 효과가 없다.
.anyRequest().authenticated();
}
package buravel.buravel.infra;
import buravel.buravel.infra.jwt.JwtAuthenticationFilter;
import buravel.buravel.infra.jwt.JwtAuthorizationFilter;
import buravel.buravel.modules.account.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AccountRepository accountRepository;
private final CorsConfig corsConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().configurationSource(corsConfigurationSource())
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.formLogin().disable()
.httpBasic().disable();
http
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), accountRepository))
.authorizeRequests()
.mvcMatchers("/signUp", "/login", "/", "/tempPassword", "/findUsername","/index/search").permitAll()
.mvcMatchers(HttpMethod.GET, "/plans","/plans/{id}").permitAll()
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
.anyRequest().authenticated();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addExposedHeader("Authorization");
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
public void configure(WebSecurity web) throws Exception
{
web.ignoring()
.mvcMatchers("/favicon.ico","/resources/**","/error")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
}
cors에서 문제가 되는 경우 기록 : jwt토큰을 헤더에 담아줬는데 리액트를 사용하는 프론트부분에서 개발자도구로는 토큰을 확인할 수 있었지만 콘솔에서는 뜨지 않았다. 그래서 preflight를 위해 .antMatchers(HttpMethod.OPTIONS,"/**").permitAll()을 설정했고 config.addExposedHeader("Authorization");를 통해 authorization 헤더를 노출할 수 있도록 설정해서 해결했다.
login / logout 시 405 에러가 나는 경우 login / logout은 시큐리티의 default 경로
아마 custom url로 login 혹은 logout을 사용한다면
Request method 'GET' not supported을 만날 수 있을 것
그럴 땐 url변경 나는 logOut으로 사용한 경험이 있다
``
-Http Basic은 헤더에 어떤 정보가 들어올 지 알고있어서 위험하다 https쓸때만 사용해야한다.