Spring Security 구현 정리

2022. 5. 24. 15:29 Spring Framework/Spring security

스프링 시큐리티는 "인증"과 "권한"이 있다.

  • Authentication(인증) : '이것' 이라고 주장하는 주체(user)가 '이것'이 맞는지  확인하는 것
    • 코드에서 Authentication : 인증 과정에 사용되는 핵심 객체
      • ID/PASSWORD 등 여러 방식으로 인증에 필요한 값이 전달되는 하나의 인터페이스

#인증방식의 종류 
1) credential 기반 인증 : 사용자명과 비밀번호를 이용한 방식 
2) 이중 인증 (twofactor 인증) : 사용자가 입력한 개인 정보를 인증 후 추가인증(OTP) 하는 방법 
3) 하드웨어 인증 : 자동차 키 

Spring Security는 credential 기반의 인증을 취한다.

 

 

로그인 과정

AuthenticationFilter 필터 리스트

 

  • WebAsyncManagerIntegrationFilter
    • SpringSecurityContextHolder는 ThreadLocal기반(하나의 쓰레드에서 SecurityContext 공유하는 방식)으로 동작하는데, 비동기(Async)와 관련된 기능을 쓸 때에도 SecurityContext를 사용할 수 있도록 만들어주는 필터
  • SecurityContextPersistenceFilter
    • SecurityContext가 없으면 만들어주는 필터
    • SecurityContext는 Authentication 객체를 보관하는 보관 인터페이스다. (구현체가 있어야겠지)
  • HeaderWriterFilter
    • 응답(Response)에 Security와 관련된 헤더 값을 설정해주는 필터
  • CsrfFilter
    • CSRF 공격을 방어하는 필터
  • LogoutFilter
    • 로그아웃 요청을 처리하는 필터
    • DefaultLogoutPageGeneratingFilter가 로그아웃 기본 페이지를 생성함
  • UsernamePasswordAuthenticationFilter → username, password를 쓰는 form기반 인증을 처리하는 필터.
    • AuthenticationManager를 통한 인증 실행
    • 성공하면, Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
    • 실패하면 AuthenticationFailureHandler 실행
  • RequestCacheAwareFilter
    • 인증 후, 원래 Request 정보로 재구성하는 필터
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
    • 이 필터에 올 때까지 앞에서 사용자 정보가 인증되지 않았다면, 이 요청은 익명의 사용자가 보낸 것으로 판단하고 처리한다. (Authentication 객체를 새로 생성함(AnonymousAuthenticationToken))
  • SessionManagementFilter
    • 세션 변조 공격 방지 (SessionId를 계속 다르게 변경해서 클라이언트에 내려준다)
    • 유효하지 않은 세션으로 접근했을 때 URL 핸들링
    • 하나의 세션 아이디로 접속하는 최대 세션 수(동시 접속) 설정
    • 세션 생성 전략 설정
  • ExceptionTranslationFilter
    • 앞선 필터 처리 과정에서 인증 예외(AuthenticationException) 또는 인가 예외(AccessDeniedException)가 발생한 경우, 해당 예외를 캐치하여 처리하는 필터 (모든 예외를 다 이 필터에서 처리하는 건 아님)
  • FilterSecurityInterceptor
    • 인가(Authorization)를 결정하는 AccessDecisionManager에게 접근 권한이 있는지 확인하고 처리하는 필터



#springSecurityFilterChain 이라는 필터가 가지고있는 필터중 하나로 "UsernamePasswordAuthenticationFilter"

라는 필터가 있는데,  이 필터는 일반적으로 아이디,패스워드를 이용한 인증을 담당한다.

즉, 요청에 대한 인증은 UsernamePasswordAuthenticationFilter가 담당한다.


아래는 UsernamePasswordAuthenticationFilter 클래스다. (인텔리제이에서 ctrl+n 으로 검색할 수 있다)

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	private boolean postOnly = true;

	// ~ Constructors
	// ===================================================================================================

	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}

	// ~ Methods
	// ========================================================================================================

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

->public Authentication attemptAuthentication 메소드를 살펴보자. username과 password를 값으로 얻어오고 그 얻어온 값으로 usernamePasswordAuthenticationToken(Authentication)을 생성한다.

그 다음에 참조하고 있던 AuthenticationManager(구현체인 ProviderManager)에게 인증을 진행하도록 위임한다.



☞AuthenticationManager, ProviderManager, AuthenticationProvider

-AuthenticationManager (interface)

 : Authentication 객체를 받아 인증하고 인증되었다면 인증된 Authentication 객체를 돌려주는 메서드를 구현하도록 하는 인터페이스다.

이 메서드를 통해 인증되면 isAuthenticated(boolean)값을 TRUE로 바꿔준다.

 

-ProvicderManager (class)

: AuthenticationManager의 구현체로 스프링에서 인증을 담당하는 클래스로 볼 수 있다.

(스프링 시큐리티가 생성하고 등록하고 관리하는 스프링 빈이므로  직접 구현할 필요X)

 

 

-AuthenticationProvider

:  인증과정이 이 메서드를 통해서 진행된다.

 

supports(Class<?>):boolean :  앞에서 보내준 Authentication 객체를 이 AuthenticationProvider가 인증 가능한 클래스인지 확인한다.

 

UsernamePasswordAuthenticationToken이 ProviderManager에 도착한다면  ProviderManager는 자기가 갖고 있는 AuthenticationProvider 목록을 순회하면서 '너가 이거 해결해줄 수 있어?' 하고 물어보고(supports()) 해결 가능하다고 TRUE를 리턴해주는 AuthenticationProvider에게 authenticate() 메서드를 실행한다.


	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

위 코드를 보고 정리하자면,  UsernamePasswordAuthenticationFilter가 요청을 가로채서 UsernamePasswordAuthenticationToken(Authentication) 객체를 AuthenticationManager 인터페이스를 구현한 providerManager 에게 넘겨준다. 

 

위 코드에 for each문 처럼 ProviderManager는 여러 AuthenticationProvider들을 순회하면 UsernamePasswordAuthenticationToken을 처리해줄 AuthenticationProvider를 찾는다. 

 

AuthenticationProvider는 인터페이스다. 따라서 이 인터페이스를 구현한 클래스를 만들어서 ProviderManager가 그 클래스의 객체에게 인증을 위임하도록 하면, 내가 만든 그 클래스가 인증 처리를 할 수 있게된다. 쉽게 말해서 AuthenticationProvider 인터페이스를 구현한 클래스를 만들어서 거기서 인증하면 끝난다.

 

 

 

AuthenticationProvider 구현하기

@Component
public class AuthProvider implements AuthenticationProvider {
    @Autowired
    UserDetailService userDetailsService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userId = authentication.getName(); 
        String userPassword = authentication.getCredentials().toString(); //userPassword

        UserLoginDto user = (UserLoginDto) userDetailsService.loadUserByUsername(userId); 

        if(!user.getPassword().equals(EncryptionUtils.encryptMD5(userPassword))) throw new BadCredentialsException("패스워드가 일치하지 않습니다");
        return new UserToken(userId, userPassword, null, user);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
    public class UserToken extends UsernamePasswordAuthenticationToken {
        private static final long serialVersionUID = 1L;

        @Getter
        @Setter
        UserLoginDto user;

        public UserToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, UserLoginDto user) {
            super(principal, credentials, user.getAuthorities());
            this.user = user;
            //principal : userId
            //credentials : password
        }
        @Override
        public Object getDetails() {
            return user;
        }
    }
}

 

이 클래스의 핵심은 Authentication 객체로부터 인증에 필요한 정보(username,password)를 받아오고,  userDetailsService 인터페이스를 구현한 객체(CustomUserDetailsService)로 부터 DB에 저장된 정보를 받아 온 후 password를 비교하고 인증이 완료되면 인증이 완료된 Authentication 객체를 리턴해주는 것이다.

이 코드에서는 UserToken이라는 클래스를 생성하여 UsernamePasswordAuthenticationToken을 extends해서 구현했다.

 

 

AuthenticationProvider 추가하기

위에서 AuthenticationProvider를 implements한  Authprovider 클래스가 바로 본인이 만든 인증시스템이 된다. 이 인증시스템을 ProviderManager가 알 수 있게 ProviderManager에 등록해줘야한다.

 

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    AuthProvider authenticationProvider;

    @Autowired
    AuthFailHandler failHandler;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/v2/api-docs",
                "/configuration/ui",
                "/swagger-resources/**",
                "/configuration/security",
                "/swagger-ui.html",
                "/webjars/**");

        String[] strings = new String[]{
                "/style/**", "/icons/**", "/image/**", "/images/**", "/js/**", "/font/**"
        };
        web.ignoring()
//                .antMatchers("/resources/**")
                .antMatchers(strings);

        web.httpFirewall(defaultHttpFirewall());
    }

    @Bean
    public HttpFirewall defaultHttpFirewall() {
        return new DefaultHttpFirewall();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
   http.authenticationProvider(authenticationProvider);
    }

}

 

 

자바 기반의 설정에서는 WebSecurityConfigurerAdapter를 상속받은 클래스에 
@EnableWebSecurity 어노테이션을 명시하는 것만으로도 springSecurityFilterChain가 자동으로 포함되어진다.
스프링 시큐리티는 사용자 정보를 UserDetails 구현체로 사용한다.

 

 

이런식으로 @Autowired를 이용하여   AuthenticationProvider 를 구현한 class를 스프링 빈(Bean)으로 등록하고 받아와서 설정에 넣으면 된다.

 


@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    AuthProvider authenticationProvider;

    @Autowired
    AuthFailHandler failHandler;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/v2/api-docs",
                "/configuration/ui",
                "/swagger-resources/**",
                "/configuration/security",
                "/swagger-ui.html",
                "/webjars/**");

        String[] strings = new String[]{
                "/style/**", "/icons/**", "/image/**", "/images/**", "/js/**", "/font/**"
        };
        web.ignoring()
//                .antMatchers("/resources/**")
                .antMatchers(strings);

        web.httpFirewall(defaultHttpFirewall());
    }

    @Bean
    public HttpFirewall defaultHttpFirewall() {
        return new DefaultHttpFirewall();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .antMatchers("/create/user").permitAll()
                .antMatchers("/check/**").permitAll()
                .antMatchers("/read/**").hasRole("USER")
                .antMatchers("/create/**").hasRole("USER")
                .antMatchers("/delete/**").hasRole("USER")
                .antMatchers("/update/**").hasRole("USER")
                //임시
                .antMatchers("/user_delete").permitAll()
                .antMatchers("/deleteCk").permitAll()
                .antMatchers("/delete/**").permitAll()
                .antMatchers("list/**").permitAll()
                //임시끝
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/home", true)
                .failureHandler(failHandler)
                .usernameParameter("userId")
                .passwordParameter("userPassword")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout_processing")
                .logoutSuccessUrl("/home")
                .invalidateHttpSession(true)
                .and().authorizeRequests().anyRequest().authenticated();

        http.authenticationProvider(authenticationProvider);
    }

}

 

 

.authorizeRequests().anyRequest().authenticated()  : 사용자 인증이 된 요청에 대해서만 요청을 허용한다.

httpBasic() : 사용자는 HTTP기반 인증으로 인증 할 수 있습니다.
.formLogin() : 사용자는 폼기반 로그인으로 인증 할 수 있다.   

.loginPage("/login") : 로그인페이지 url은  "/login" 이다.

->로그인페이지의 default 값은 post방식의 "/login" 이다.

defaultSuccessUrl("/home",true) : 로그인 성공시 "/home" 으로 이동한다.

.logoutUrl("/logout_processing") : "/logout_processing" 이 요청되면 로그아웃 한다.

logoutSuccessUrl("/home) 로그아웃 성공시 "/home"으로 이동한다.

invalidatehttpsession(true) : 로그아웃시  인증정보를 지우고 세션을 무효화시킨다는 설정이다.

failureHandler(failHandler) : 로그인 실패시  failHandler 

 

 

-> 스프링 시큐리티에 필요한 내용을 정의하는 configuration을 생성해야한다.
-> WebSecurityConfigurerAdapter 클래스를 상속받아서 configure 메서드를 재정의 해야한다.
-> @EnableWebSecurity, @EnableGlobalAuthentication와 같은 애노테이션을 사용하여 스프링 시큐리티 사용을 정의 해야 한다.
-> public void configure(WebSecurity web) throws Exception 메서드를 재정의하여 로그인 상관 없이 허용을 해줘야할 리소스 위치를 정의한다.
-> protected void configure(HttpSecurity http) throws Exception  메소드를 재정의하여 로그인 URL, 권한분리, logout URL 등등을 설정할 수 있다.

web.httpFirewall(defaultHttpFirewall());    
->더블슬래쉬 허용 (기본 정책은 URL에 더블 슬래쉬를 허용하지 않는다.)

 

protected void configure(HttpSecurity http) throws Exception 에서 사용되는  시큐리티 접근 메소드 종류

-> 

access(String)  : 주어진 SpEL 표현식의 평가 결과가 true이면 접근을 허용한다.

anonymous() : 익명의 사용자의 접근을 허용한다.

authenticated() : 인증된 사용자의 접근을 허용한다.

fullyAuthenticated() : 사용자가 완전히 인증되면 접근을 허용(기억되지 않음)

hasAnyAuthority(String ...) : 사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용

hasAnyRole(String) : 사용자가 주어진 권한이 있다면 접근을 허용

haslpAddress(String) : 주어진 IP로부터 요청이 왔다면 접근을 허용

hasRole(String) : 사용자가 주어진 역할이 있다면 접근을 허용

not() : 다른 접근 방식의 효과를 무효화

permitAll() : 무조건 접근을 허용함

denyAll() : 무조건 접근을 허용하지 않음

rememberMe() : 기억하기를 통해 인증된 사용자의 접근을 허용


학교 프로젝트에서 사용한 Spring security 예제

인증과정은 ?

1. /login  url의 form에 입력한 userId와 passworld를 Spring Security가 사용하는 유저의 객체로 변환시킴
2. 유저가 입력한 정보를 기반으로 DB에서 사용자 정보를 가져온다.
-> 이 과정은 UserDetails를 이용함, UserDetailService에서 userDetails형 메소드를 통해 UserDetails를 implements한
userLoginDto에 담아서 리턴해준다.(유저정보들을)
3. 유저가 입력한 정보와 DB에서 가져온 사용자 정보를 기반으로 인증 진행
4. 만약 인증에 성공했다면 유저에게 설정된 권한을 부여한다.

 

1. HTTPSecurity

package skhu.sof14.hotthink.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
import skhu.sof14.hotthink.service.AuthProvider;

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    AuthProvider authenticationProvider;

    @Autowired
    AuthFailHandler failHandler;

    @Autowired
    LogoutHandler logoutHandler;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/v2/api-docs",
                "/configuration/ui",
                "/swagger-resources/**",
                "/configuration/security",
                "/swagger-ui.html",
                "/webjars/**");

        String[] strings = new String[]{
                "/style/**", "/icons/**", "/image/**", "/images/**", "/js/**", "/font/**"
        };
        web.ignoring()
//                .antMatchers("/resources/**")
                .antMatchers(strings);

        web.httpFirewall(defaultHttpFirewall()); // 더블슬래쉬 허용
    }

    @Bean
    public HttpFirewall defaultHttpFirewall() {
        return new DefaultHttpFirewall();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .antMatchers("/create/user").permitAll()
                .antMatchers("/check/**").permitAll()
                .antMatchers("/read/**").hasRole("USER")
                .antMatchers("/create/**").hasRole("USER")
                .antMatchers("/delete/**").hasRole("USER")
                .antMatchers("/update/**").hasRole("USER")

                //임시
                .antMatchers("/user_delete").permitAll()
                .antMatchers("/deleteCk").permitAll()
                .antMatchers("/delete/**").permitAll()
                .antMatchers("list/**").permitAll()
                .antMatchers("/testCharge").permitAll()
                //임시끝
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/home", true)
                .failureHandler(failHandler)
                .usernameParameter("userId")
                .passwordParameter("userPassword")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout_processing")
                .logoutSuccessHandler(logoutHandler)
                .invalidateHttpSession(true);
//                .and().authorizeRequests().anyRequest().authenticated();

        http.authenticationProvider(authenticationProvider);
    }

}

 - WebSecurityConfigurerAdapter에서 configure(HttpSecurity http) 메소드를 제공함.

 -이 메소드에서 URL 및 로그인을 설정함으로써 Spring Security가 어떤 URL에 인증이 필요하고, 어떤 로그인 인증과정을 사용하는지 알 수 있게됨.

 -이 메소드를 통해서 자신만의 메커니즘을 구현할 수 있게됨.

 - Spring Security Filter Chain이 생성된다.

 ->@EnableWebSecurity 어노테이션을 사용하면 Spring Security Filter Chain이 자동으로 생성된다.

 -> 애플리케이션의 모든 URL에 인증할 수 있다.

 -> 기본 로그인 폼을 제공해주며, 폼 기반 인증을 통해 username과 password 인증을 할 수 있게 해준다.

 

 

2. AuthenticationProvider

package skhu.sof14.hotthink.service;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import skhu.sof14.hotthink.config.kafka.ConsumerConfiguration;
import skhu.sof14.hotthink.model.dto.user.UserLoginDto;
import skhu.sof14.hotthink.utils.EncryptionUtils;
import java.util.Collection;

// 이 클래스가  AuthenticationProvider 를 구현한 클래스이다.
//AuthenticationManager 는 Authentication authenticate(Authentication authentication) throws AuthenticationException 를 가지고있음.
@Component
public class AuthProvider implements AuthenticationProvider {
    @Autowired
    UserDetailService userDetailsService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userId = authentication.getName();
        String userPassword = authentication.getCredentials().toString(); //userPassword
        UserLoginDto user = (UserLoginDto) userDetailsService.loadUserByUsername(userId);
        if(!user.getPassword().equals(EncryptionUtils.encryptMD5(userPassword))) throw new BadCredentialsException("패스워드가 일치하지 않습니다");

        ConsumerConfiguration.startUserTopicConsumeContainer(user.getId());
        return new UserToken(userId, userPassword, null, user); //UserToken에 userId와 userPassword, user(loginDto , id,pw,status)
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }


    public class UserToken extends UsernamePasswordAuthenticationToken {
        private static final long serialVersionUID = 1L;

        @Getter
        @Setter
        UserLoginDto user;

        public UserToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, UserLoginDto user) {
            super(principal, credentials, user.getAuthorities());
            this.user = user;
            //principal : userId
            //credentials : password
        }
        @Override
        public Object getDetails() {
            return user;
        }
    }
}

- AuthenticationProvider를 구현한 클래스로, 실질적으로 인증을 처리하는 클래스이다.

- Authentication 메소드를 구현한 클래스이다.

- UserDetails와 userDetailService 그리고 PasswordEncoder를 이용해서 인증을 처리한다.

->ProviderManager는 여러개의 AuthenticationProvider를 가질 수 있다.
-> 여러개의 AuthenticationProvider를 가지고 있는 경우 각 순서대로 인증을 진행하거나 skip 한다.

 

3. UserDetailService

import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import skhu.sof14.hotthink.model.dto.user.UserLoginDto;
import skhu.sof14.hotthink.model.entity.User;
import skhu.sof14.hotthink.repository.UserRepository;

@Slf4j
@Service
public class UserDetailService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Autowired
    ModelMapper modelMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User entity = userRepository.findUserByUserId(username); // 유저 아이디를 찾음.
        if(entity == null) throw new UsernameNotFoundException("해당 아이디를 가진 유저를 찾을 수 없습니다");
        return modelMapper.map(entity, UserLoginDto.class); //UserLoginDto : userId,password,status를 보내줌.
    }

}

-사용자가 입력한 username을 기반으로 저장된 유저정보를 가져와서 UserDetails객체로 변환하여 리턴해주는 메소드

-이 프로젝트에서는 UserLoginDto class에 UserDetails를 implements 해서 구현하였다.

 

 

3-1. UserLoginDto

import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@ToString
public class UserLoginDto extends UserBase implements UserDetails{
    @Setter
    private String userId;
    @Setter
    private String userPassword;
    @Setter
    private boolean status;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {  //계정이 갖고있는 권한목록을 리턴한다.
        List<GrantedAuthority> auth = new ArrayList<>();
        auth.add(new SimpleGrantedAuthority("ROLE_USER"));
        return auth;
    }

    @Override
    public String getPassword() {
        return this.userPassword;
    } //계정의 패스워드를 리턴한다.

    @Override
    public String getUsername() {
        return this.userId;
    }  // 계정의 userId를 리턴한다.

    @Override
    public boolean isAccountNonExpired() {
        return this.status;
    } //계정의 만료여부를 리턴한다.(true: 만료되지 않음)

    @Override
    public boolean isAccountNonLocked() {
        return this.status;
    } //계정의 lock 여부(잠겨있는지) 를 리턴한다. (true: 잠겨있지 않음)

    @Override
    public boolean isCredentialsNonExpired() {
        return this.status;
    } // 계정의 패스워드 만료 여부를 리턴한다. (true: 만료되지 않음)

    @Override
    public boolean isEnabled() {
        return this.status;
    } //계정이 사용 가능한지 여부를 리턴한다. (true: 사용 가능)
}

-Spring Security에서는 사용자 객체가 필요하지만, 모든 서비스 마다 사용자 정의가 다르기 때문에 Spring Security 사용자 객체를 호환해주는 어댑터인 UserDetails가 필요.
-Spring Security를 사용하는 어플리케이션에서는 이 인터페이스를 구현해야함

-이 프로젝트에서는 권한을 사용하지 않았음.

 

출처 : https://1-7171771.tistory.com/80?category=885255