출처 : https://coding-start.tistory.com/153
출처 : https://sjh836.tistory.com/165
출처 : https://blog.naver.com/fltltmxjs/80196103430
출처 : https://springsource.tistory.com/80
보안용어
1) 접근 주체(Principal) : 보호된 리소스에 접근하는 대상
2) 인증(Authentication) : 보호된 리소스에 접근한 대상에 대해 이 유저가 누구인지,
애플리케이션의 작업을 수행해도 되는 주체인지 확인하는 과정(ex. Form 기반 Login)
3) 인가(Authorize) : 해당 리소스에 대해 접근 가능한 권한을 가지고 있는지 확인하는 과정(After Authentication, 인증 이후)
4) 권한 : 어떠한 리소스에 대한 접근 제한, 모든 리소스는 접근 제어 권한이 걸려있다.
즉, 인가 과정에서 해당 리소스에 대한 제한된 최소한의 권한을 가졌는지 확인한다.
스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한,인가 등)을 담당하는 스프링 하위 프레임워크이다.
주로 서블릿 필터와 이들로 구성된 필터체인으로의 위임모델을 사용한다.
1) 사용자가 Form을 통해 로그인 정보를 입력하고 인증 요청을 보낸다.
2) AuthenticationFilter(사용할 구현체 UsernamePasswordAuthenticationFilter)가 HttpServletRequest에서 사용자가 보낸 아이디와 패스워드를 인터셉트한다. 프론트 단에서 유효성검사를 할 수도 있지만, 무엇보다 안전! 안전을 위해서 다시 한번 사용자가 보낸 아이디와 패스워드의 유효성 검사를 해줄 수 있다.(아이디 혹은 패스워드가 null인 경우 등) HttpServletRequest에서 꺼내온 사용자 아이디와 패스워드를 진짜 인증을 담당할 AuthenticationManager 인터페이스(구현체 - ProviderManager)에게 인증용 객체(UsernamePasswordAuthenticationToken)로 만들어줘서 위임한다.
3) AuthenticationFilter에게 인증용 객체(UsernamePasswordAuthenticationToken)을 전달받는다.
4) 실제 인증을 할 AuthenticationProvider에게 Authentication객체(UsernamePasswordAuthenticationToken)을 다시 전달한다.
5) DB에서 사용자 인증 정보를 가져올 UserDetailsService 객체에게 사용자 아이디를 넘겨주고 DB에서 인증에 사용할 사용자 정보(사용자 아이디, 암호화된 패스워드, 권한 등)를 UserDetails(인증용 객체와 도메인 객체를 분리하지 않기 위해서 실제 사용되는 도메인 객체에 UserDetails를 상속하기도 한다.)라는 객체로 전달 받는다.
6) AuthenticationProvider는 UserDetails 객체를 전달 받은 이후 실제 사용자의 입력정보와 UserDetails 객체를 가지고 인증을 시도한다.
8) 9) 10) 인증이 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행한다.(실패시 AuthenticationFailureHandler를 실행한다.)
1) SecurityContextPersistenceFilter : SecurityContextRepository에서 SecurityContext를 가져오거나 저장하는 역할을 한다. (SecurityContext는 밑에)
2) LogoutFilter : 설정된 로그아웃 URL로 오는 요청을 감시하며, 해당 유저를 로그아웃 처리
3) UsernamePassword)AuthenticationFilter : (아이디와 비밀번호를 사용하는 form 기반 인증) 설정된 로그인 URL로 오는 요청을 감시하며, 유저 인증 처리
- AuthenticationManager를 통한 인증 실행
- 인증 성공 시, 얻은 Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
- 인증 실패 시, AuthenticationFailureHandler 실행
4) DefaultLoginPageGeneratingFilter : 인증을 위한 로그인폼 URL을 감시한다.
5) BasicAuthenticationFilter : HTTP 기본 인증 헤더를 감시하여 처리한다.
6) RequestCacheAwareFilter : 로그인 성공 후, 원래 요청 정보를 재구성하기 위해 사용된다.
7) SecurityContextHolderAwareRequestFilter : HttpServletRequestWrapper를 상속한
SecurityContextHolderAwareRequestWapper 클래스로 HttpServletRequest 정보를 감싼다.
SecurityContextHolderAwareRequestWrapper 클래스는 필터 체인상의 다음 필터들에게 부가정보를 제공한다.
8) AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 인증토큰에 사용자가 익명 사용자로 나타난다.
9) SessionManagementFilter : 이 필터는 인증된 사용자와 관련된 모든 세션을 추적한다.
10) ExceptionTranslationFilter : 이 필터는 보호된 요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달하는 역할을 한다.
11) FilterSecurityInterceptor : 이 필터는 AccessDecisionManager 로 권한부여 처리를 위임함으로써 접근 제어 결정을 쉽게해준다.
Authentication
모든 접근 주체(=유저) 는 Authentication 를 생성한다. 이것은 SecurityContext 에 보관되고 사용된다. 즉 security의 세션들은 내부 메모리(SecurityContextHolder)에 쌓고 꺼내쓰는 것이다. 참고로 Authentication 인터페이스는 자주 쓰이니 알아둬야한다.
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // Authentication 저장소에 의해 인증된 사용자의 권한 목록
Object getCredentials(); // 주로 비밀번호
Object getDetails(); // 사용자 상세정보
Object getPrincipal(); // 주로 ID
boolean isAuthenticated(); //인증 여부
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
출처: https://sjh836.tistory.com/165 [빨간색코딩]
AuthenticationManager
유저의 요청 내에 담긴 Authentication를 AuthenticationManager 에 넘겨주고, AuthenticationManager 를 구현한 ProviderManager가 처리한다. 정확히는 ProviderManager 는
private List<AuthenticationProvider> providers;
로 여러 AuthenticationProvider를 가질 수 있는데, 이 친구들이 처리를 해서 Authentication 를 반환해준다. (실패하면 예외던짐)
- AbstractAuthenticationProcessingFilter : 웹 기반 인증요청에서 사용되는 컴포넌트로 POST 폼 데이터를 포함하는 요청을 처리한다. 사용자 비밀번호를 다른 필터로 전달하기 위해서 Authentication 객체를 생성하고 일부 프로퍼티를 설정한다.
- AuthenticationManager : 인증요청을 받고 Authentication을 채워준다.
- AuthenticationProvider : 실제 인증이 일어나고 만약 인증 성공시 Authentication 객체의 authenticated = true로 설정해준다.
비밀번호 인증과정
DaoAuthenticationProvider 는 UserDetailsService 타입 오브젝트로 위임한다. UserDetailsService 는 UserDetails 구현체를 리턴하는 역할을 한다. UserDetails 인터페이스는 이전에 설명한 Authentication 인터페이스와 상당히 유사하지만 서로 다른 목적을 가진 인터페이스이므로 헷갈리면 안된다.
- Authentication : 사용자 ID, 패스워드와 인증 요청 컨텍스트에 대한 정보를 가지고 있다. 인증 이후의 사용자 상세정보와 같은 UserDetails 타입 오브젝트를 포함할 수도 있다.
- UserDetails : 이름, 이메일, 전화번호와 같은 사용자 프로파일 정보를 저장하기 위한 용도로 사용한다.
인증 예외 클래스
인증과 관련된 모든 예외는 AuthenticationException 을 상속한다. AuthenticationException 은 개발자에게 상세한 디버깅 정보를 제공하기위한 두개의 멤버 필드를 가지고 있다.
- authentication : 인증 요청관련 Authentication 객체를 저장하고 있다.
- extraInformation : 인증 예외 관련 부가 정보를 저장한다. 예를 들어 UsernameNotFoundException 예외는 인증에 실패한 유저의 id 정보를 저장하고 있다.
자동으로 설정된 Spring Security 필터 체인의 마지막 서블릿 필터는 FilterSecurityInterceptor 이다.
이 필터는 해당 요청의 수락 여부를 결정한다. FilterSecurityInterceptor 가 실행되는 시점에는 이미 사용자가 인증되어 있을 것이므로 유효한 사용자인지도 알 수 있다.
Authentication 인터페이스에는 List<GrantedAuthority> getAuthorities() 라는 메소드가 있다는 것을 상기해 보자.
이 메소드는 사용자 아이디에 대한 권한 목록을 리턴한다.
권한처리시에 이 메소드가 제공하는 권한정보를 참조해서 해당 요청의 승인 여부를 결정하게 된다.
Access Decision Manager 라는 컴포넌트가 인증 확인을 처리한다.
AccessDecisionManager 인터페이스는 인증 확인을 위해 두 가지 메소드를 제공한다.
- supports : AccessDecisionManager 구현체는 현재 요청을 지원하는지의 여부를 판단하는
두개의 메소드를 제공한다.
하나는 java.lang.Class 타입을 파라미터로 받고 다른 하나는 ConfigAttribute 타입을 파라미터로 받는다.
- decide : 이 메소드는 요청 컨텍스트와 보안 설정을 참조하여 접근 승인여부를 결정한다.
이 메소드에는 리턴값이 없지만 접근 거부를 의미하는 예외를 던져 요청이 거부되었음을 알려준다.
AccessDecisionManager 는 표준 스프링 빈 바인딩과 레퍼런스로 완벽히 설정할 수 있다.
디폴트 AccessDecisionManager 구현체는 AccessDecisionVoter 와 Vote 취합기반 접근 승인 방식을 제공한다.
Voter 는 권한처리 과정에서 다음 중 하나 또는 전체를 평가한다.
- 보호된 리소스에 대한 요청 컨텍스트 (URL 을 접근하는 IP 주소)
- 사용자가 입력한 비밀번호
- 접근하려는 리소스
- 시스템에 설정된 파라미터와 리소스
AccessDecisionManager 는 요청된 리소스에 대한 access 어트리뷰트 설정을 Voter에게 전달하는 역할도 하므로 보터는 웹 URL 관련 access 어트리뷰트 설정 정보를 가지게 된다.
Voter는 사용할 수 있는 정보를 사용해서 사용자의 리소스에 대한 접근 허가 여부를 판단한다.
보터는 접근 허가 여부에 대해서 세 가지 중 한 가지로 결정하는데, 각 결정은 AccessDecisionVoter 인터페이스에 다음과 같이 상수로 정의되어 있다.
- Grant(ACCESS_GRANTED) : Voter 가 리소스에 대한 접근 권한을 허가하도록 권장한다.
- Deny(ACCESS_DENIED) : Voter 가 리소스에 대한 접근 권한을 거부하도록 권장한다.
- Abstain(ACCESS_ABSTAIN) : Voter 는 리소스에 대한 접근권한 결정을 보류한다. 이 결정 보류는 다음과 같은 경우에 발생할 수 있다.
1) Voter 가 접근권한 판단에 필요한 결정적인 정보를 가지고 있지 않은 경우
2) Voter 가 해당 타입의 요청에 대해 결정을 내릴 수 없는 경우
Java Config 기반 코드 설명
@EnableWebSecurity
// 어노테이션을 통해 스프링 시큐리티를 사용하겠다라고 선언
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{
// private UserDetailsService userDetailsService;
// private PasswordEncoder passwordEncoder;
private AuthenticationProvider authenticationProvider;
public SpringSecurityConfig(/*UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder,*/
AuthenticationProvider authenticationProvider) {
// this.userDetailsService = userDetailsService;
// this.passwordEncoder = passwordEncoder;
this.authenticationProvider = authenticationProvider;
}
/*
* 스프링 시큐리티가 사용자를 인증하는 방법이 담긴 객체.
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*
* AuthenticationProvider 구현체
*/
// 인증을 담당할 프로바이더 구현체를 설정하는 메소드이다.
auth.authenticationProvider(authenticationProvider);
// auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
/*
* 스프링 시큐리티 룰을 무시하게 하는 Url 규칙.
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/resources/**")
.antMatchers("/css/**")
.antMatchers("/vendor/**")
.antMatchers("/js/**")
.antMatchers("/favicon*/**")
.antMatchers("/img/**")
;
}
/*
* 스프링 시큐리티 룰.
* 권한, 설정, 특정 기능 결과에 대한 Handler 등록, Custom Filter 등록(ex. AuthenticationFilter 재정의)
* 그리고 예외 핸들러 등을 등록하는 메소드
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login*/**").permitAll()
.antMatchers("/logout/**").permitAll()
.antMatchers("/chatbot/**").permitAll()
.anyRequest().authenticated()
// 보호된 리소스 URI에 접근할 수 있는 권한을 설정해주는 설정
// AccessDecisionManager에 설정되는 access 정보이다.
// 이 설정들은 추후 FilterSecurityInterceptor에서 권한 인증에 사용될 정보들이다.
.and().logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler())
// 로그아웃 기능에 대한 설명이다.
// logoutUrl()을 통해 로그아웃 기능에 대한 RequestUri정보를 전달한다.
// 그리고 .logoutSuccessHandler()를 통해 로그아웃이 성공적으로 끝나면 수행될 핸들러를 등록해준다.
.and().csrf()
.disable()
// csrf().disable()을 통하여 csrf 보안 설정을 비활성화한다.(예제라서 생략)
.addFilter(jwtAuthenticationFilter())
// Form Login에 사용되는 custom AuthenticationFilter 구현체를 등록해준다.
.addFilter(jwtAuthorizationFilter())
// Header 인증에 사용되는 BasicAuthenticationFilter 구현체를 등록해준다.
// (JWT Token 기반 인증에 본격적으로 사용될 필터이다.)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(authenticationEntryPoint())
// 예외 핸들러를 등록해주는 설정이다.
// accessDeniedHandler()는 권한 체크에서 실패할 때 수행되는 핸들러를 등록하고,
// authenticationEntryPoint()는 현재 인증된 사용자가 없는데(SecurityContext에 인증사용자가 담기지 않음)
// 인증되지 않은 사용자가 보호된 리소스에 접근하였을 때, 수행되는 핸들러(EntryPoint)를 등록해준다.
// .and().sessionManagement()
// .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
/*
* SuccessHandler bean register
*/
// Form Login(AuthenticationFilter)에서 인증이 성공했을 때 수행될 핸들러이다.
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
CustomAuthenticationSuccessHandler successHandler = new CustomAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl("/index");
return successHandler;
}
/*
* FailureHandler bean register
*/
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
CustomAuthenticationFailureHandler failureHandler = new CustomAuthenticationFailureHandler();
failureHandler.setDefaultFailureUrl("/loginPage?error=error");
return failureHandler;
}
/*
* LogoutSuccessHandler bean register
*/
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
CustomLogoutSuccessHandler logoutSuccessHandler = new CustomLogoutSuccessHandler();
logoutSuccessHandler.setDefaultTargetUrl("/loginPage?logout=logout");
return logoutSuccessHandler;
}
/*
* AccessDeniedHandler bean register
*/
@Bean
public AccessDeniedHandler accessDeniedHandler() {
CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
accessDeniedHandler.setErrorPage("/error/403");
return accessDeniedHandler;
}
/*
* AuthenticationEntryPoint bean register
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new CustomAuthenticationEntryPoint("/loginPage?error=e");
}
/*
* Form Login시 걸리는 Filter bean register
*/
/*
AuthenticationFilter 설정이다.
우선 위에서 말했듯 AuthenticationFilter는 AuthenticationManager에게 위임한다했다.
그렇기 때문에 AuthenticationManager를 생성자로 전달한다.
그리고 setFilterProcessesUrl()을 통해 로그인 요청 URI를 정의해준다.
(스프링이 제공하므로 따로 컨트롤러 등록할 필요가 없다.
logout URI도 똑같이 스프링에서 제공한다.
하지만 SuccessHandler나 FailureHandler,EntryPoint는 컨트롤러에 등록된 URI로 설정해야한다.)
setUsernameParameter(),setPasswordParameter()를 통해 폼으로 넘어오는 사용자 아이디,패스워드 변수값을 설정한다.(<input>의 name속성이라고 생각하면된다.)
setAuthenticationSuccess,FailureHandler()를 통해 결과에 대해 수행할 핸들러를 등록한다.
*/
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/login");
jwtAuthenticationFilter.setUsernameParameter("username");
jwtAuthenticationFilter.setPasswordParameter("password");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
jwtAuthenticationFilter.afterPropertiesSet();
return jwtAuthenticationFilter;
}
/*
* Filter bean register
*/
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() throws Exception {
JwtAuthorizationFilter jwtAuthorizationFilter = new JwtAuthorizationFilter(authenticationManager());
return jwtAuthorizationFilter;
}
}
출처: https://coding-start.tistory.com/153 [코딩스타트]
/*
* SuccessHandler bean register
*/
// Form Login(AuthenticationFilter)에서 인증이 성공했을 때 수행될 핸들러이다.
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
CustomAuthenticationSuccessHandler successHandler = new CustomAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl("/index");
return successHandler;
}
// Simp
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler implements ExceptionProcessor{
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
log.debug("CustomAuthenticationSuccessHandler.onAuthenticationSuccess ::::");
/*
* 쿠키에 인증 토큰을 넣어준다.
*/
super.onAuthenticationSuccess(request, response, authentication);
}
@Override
public void makeExceptionResponse(HttpServletRequest request, HttpServletResponse response,
Exception exception) {
}
}
커스텀 클래스를 만들어본다.
CustomCustomAuthenticationSuccessHandler 은 ExceptionProcessor 구현체이면서 또한SavedRequestAwareAuthenticationSuccessHandler 상속을 받는다.
이 클래스는 AuthenticationSuccessHandler 메소드에서 활용이되며 로그인 성공시 index 페이지로 리다이렉트 하는 역할을 수행한다.
아래또한 비슷한 과정을 거친다.
// Form Login 실패시 수행되는 핸들러
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler implements ExceptionProcessor{
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
log.debug("CustomAuthenticationFailureHandler.onAuthenticationFailure ::::");
super.onAuthenticationFailure(request, response, exception);
}
@Override
public void makeExceptionResponse(HttpServletRequest request, HttpServletResponse response,
Exception exception) {
}
}
// 로그아웃에 성공했을 시 수행되는 핸들러
@Slf4j
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler{
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
log.debug("CustomLogoutSuccessHandler.onLogoutSuccess ::::");
super.onLogoutSuccess(request, response, authentication);
}
}
// 권한 체크 실퍂시 수행되는 핸들러
@Slf4j
public class CustomAccessDeniedHandler extends AccessDeniedHandlerImpl implements ExceptionProcessor{
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.debug("CustomAccessDeniedHandler.handle");
this.makeExceptionResponse(request, response, accessDeniedException);
}
@Override
public void makeExceptionResponse(HttpServletRequest request, HttpServletResponse response,
Exception exception) throws IOException {
log.debug("CustomAccessDeniedHandler.makeExceptionResponse :::: {}",exception.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, exception.getMessage());
}
}
// 인증된 사용자가 SecurityContext에 존재하지도 않고, 어떠한 인증되지 않은 익명의 사용자가 보호된 리소스에 접근하였을 때,
// 수행되는 EntryPoint 핸들러
@Slf4j
public class CustomAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint{
public CustomAuthenticationEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.debug("CustomAuthenticationEntryPoint.commence ::::");
super.commence(request, response, authException);
}
}
// Form Login시 걸리는 Filter
// UsernamePasswordAuthenticationFilter를 상속한 JwtAuthenticationFilter을 등록한다.
// HttpServletRequest에서 사용자가 Form으로 입력한 로그인 정보를 인터셉트해서
// AuthenticationManager에게 Authentication 객체를 넘겨준다.
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
private boolean postOnly = true;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
/*
* 해당 필터에서 인증 프로세스 이전에 요청에서 사용자 정보를 가져와서
* Authentication 객체를 인증 프로세스 객체에게 전달하는 역할
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
log.debug("JwtAuthentication.attemptAuthentication ::::");
/*
* POST로 넘어왔는지 체크
*/
if(postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if(StringUtils.isEmpty(username)) {
username = "";
}
if(StringUtils.isEmpty(password)) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
// Form Login에서 인증된 이후의 요청에 대해 Header 인증을 담당할 Filter이다.
// BasicAuthenticationFilter를 상속한 JwtAuthorizationFilter를 등록한다.
// 다음 포스팅에서 다룰 JWT 기반 인증에서 실제 JWT 토큰의 인증이 이루어질 필터 부분이다.
@Slf4j
public class JwtAuthorizationFilter extends BasicAuthenticationFilter{
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
// TODO Auto-generated constructor stub
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.debug("JwtAuthorizationFilter.doFilterInternal ::::");
/*
* 쿠키 인증 토큰을 검사한다.
* 만약 토큰 및 헤더에 대한 검사에 실패한다면,
* AuthenticationEntryPoint에 위임하거나 혹은 HttpResponse에 적절한
* 상태코드와 메시지를 담아서 리턴해준다.
*/
super.doFilterInternal(request, response, chain);
}
/*
* 성공시 처리 메소드
*/
@Override
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
Authentication authResult) throws IOException {
// TODO Auto-generated method stub
super.onSuccessfulAuthentication(request, response, authResult);
}
/*
* 실패시 처리 메소드
*/
@Override
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException {
// TODO Auto-generated method stub
super.onUnsuccessfulAuthentication(request, response, failed);
}
}
// AuthenticationProvider 구현체에서 인증에 사용할 사용자 인증정보를 DB에서 가져오는 역할을 하는 클래스이다.
@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService{
@Autowired private ChatbotUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("UserDetailsServiceImpl.loadUserByUsername :::: {}",username);
ChatbotUserDto userDto = userService.findByUsername(username);
if(ObjectUtils.isEmpty(userDto)) {
throw new UsernameNotFoundException("Invalid username");
}
userDto.setAuthorities(AuthoritiesUtils.createAuthorities(userDto));
return userDto;
}
}
// 실제 DB에는 비밀번호가 적절한 암호화 알고리즘으로 암호화되 저장되어있다.
// 폼에서 넘어오는 평문의 사용자 입력정보를 이용해 인증을 하려면 DB에 저장한 암호화 알고리즘 엔코더가 필요하다. 해당 역할을 하는 클래스이다
@Slf4j
@Component
public class ShaPasswordEncoder implements PasswordEncoder{
@Override
public String encode(CharSequence rawPassword) {
log.debug("ShaPasswordEncoder.encode :::: {}",rawPassword);
return Crypto.sha256(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
log.debug("ShaPasswordEncoder.matches :::: {} <-> {}",rawPassword,encodedPassword);
return Crypto.sha256(rawPassword.toString()).equals(encodedPassword);
}
}
/*
실제 인증이 일어나는 클래스이다.
AuthenticationManager(ProviderManager)는 실제로 많은 AuthenticationProvider 구현체들을 가질 수 있다고 이야기 했듯 저 AuthenticationProvider를 가지고
ProviderManager는 인증 로직을 태우게된다. AuthenticationProvider를 구현한 CustomAuthenticationProvider를 사용한다.
해당 클래스 내부적으로는 UserDetailsService에게 입력받은 사용자 아이디를 넘겨 DB에서 사용자 인증 정보를 받고 암호화된
패스워드를 비교하기 위하여 PasswordEncoder에게 사용자가 입력한 평문 패스워드를 전달해 암호화된 형태로 받아서
암호화<->암호화 형태로 비밀번호를 비교한다(평문<->평문 아님).
만약 인증이 완료되면 Authentication객체를 구현한 UsernamePasswordAuthenticationToken객체를 반환한다.
*/
@Slf4j
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{
private UserDetailsService userDetailsService;
private PasswordEncoder encoder;
public CustomAuthenticationProvider(UserDetailsService userDetailsService,PasswordEncoder encoder) {
this.userDetailsService = userDetailsService;
this.encoder = encoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.debug("CustomUserAuthenticationProvider.authenticate :::: {}",authentication.toString());
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken)authentication;
String userId = token.getName();
ChatbotUserDto user = null;
if(!StringUtils.isEmpty(userId)) {
user = (ChatbotUserDto) userDetailsService.loadUserByUsername(userId);
}
if(ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("Invalid username");
}
user.setUsername(user.getUsername());
user.setPassword(user.getPassword());
String password = user.getPassword();
if(!StringUtils.equals(password, encoder.encode(String.valueOf(token.getCredentials())))) {
throw new BadCredentialsException("Invalid password");
}
return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
log.debug("CustomUserAuthenticationProvider.supports ::::");
return UsernamePasswordAuthenticationToken
.class.equals(authentication);
}
}
Access
- hasRole(Role) : 해당 Role을 갖고있는 사용자 허용
- hasAnyRole(Role 1, Role2, ...) : 해당 Role 중에 1개이상 갖고있는 사용자 허용
- anonymous : 익명 사용자 허용
- authenticated : 인증된 사용자 허용
- permitAll : 모든 사용자 허용
- denyAll : 모든 사용자 허용
유용한 어노테이션
- @Secured : 각 요청경로에 따른 권한 설정은 위의 xml에서도 할 수 있지만, 메소드 레벨에서 어노테이션을 통해서도 가능하다. @EnableGlobalMethodSecurity(securedEnabled=true) 를 추가하면 된다.
- @Secured("ROLE_ADMIN"), @Secured({"ROLE_ADMIN","ROLE_USER"})
- 비인가 접근시 AccessDeniedExcptiopn 예외를 던짐
- @PreAuthorize : 위와 비슷하지만 spEl을 사용할 수 있다. @EnabledGlobalMethodSecurity(prePostEnabled=true)를 설정한다.
- @PreAuthorize("hasRole('ADMIN')")
- @PostAuthorize : 위와 동일
- @AuthenticationPrincipal : 컨트롤러단에서 세션의 정보들에 접근하고 싶을 때 파라미터에 선언해준다.
- public ModelAndView userInfo(@AuthenticationPrincipal User user)
- 이거 안쓰고 확인하려면 (User)
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
- 4.x부터는 org.springframework.security.core.annotation.AuthenticationPrincipal 을 import 해야한다.
- mvc : annotation-driven.mvc : argument-resolvers 에 bean으로 org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumnetRes 등록한다.
소스코드는 https://github.com/yeoseong/spring-security
추천)
http://tmondev.blog.me/220310743818
추천2)
https://sjh836.tistory.com/165
추천3)
https://okky.kr/article/382738