[Dining-together] Spring Security (로그인 회원가입 구현)

Jifrozen·2021년 5월 25일
0

Dining-together

목록 보기
3/25

spring security

처음 사용했기 때문에 굉장히 힘들었다...
스프링에서 인증 및 권한 부여를 통해 리소스 사용을 쉽게 컨트롤 할 수 있는 spring security 를 제공한다.


전체적인 프로세스

  1. client가 어플리케이션에 요청을 보내면 , 서블릿 필터에 의해 시큐리티 필터로 시큐리티 작업이 위임되고 여러 시큐리티 필터 중에서 UsernamePasswordAuthenticationFilter(Username and Password Authentication 방식에서 사용하는 AuthenticationFilter)에서 인증을 처리한다. 시큐리티에서 제공해주는 폼을 사용하지 않기 때문에
    UsernamePasswordAuthenticationFilter 이전 필터인 사용자 정의 필터 AuthenticationFilter(JwtAuthenticationFilter)를 생성한다.

  2. AuthenticationFilter -> AuthenticationProvider -> 전달받은 인증 객체의 정보를 UserDetailService 에 넘겨준다.

  3. UserDetailService는 UserDetail 객체를 db 에 있는 사용자 정보와 일치한다면 AuthenticationProvider전달한다.

  4. AuthenticationProvider은 전달받은 UserDetails를 인증해 성공하면 ProviderManager에게 권한(Authorities)을 담은 검증된 인증 객체를 전달합니다. -> AuthenticationFilter

  5. AuthenticationFilter는 검증된 인증 객체를 SecurityContextHolder의 SecurityContext에 저장합니다.


WebSecurityConfig

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()	// security에서 기본으로 생성하는 login페이지 사용 안 함
                .csrf().disable()	// REST API 사용하기 때문에 csrf 사용 안 함
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)	// JWT인증사용하므로 세션 생성 안함
                .and()
                .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크 //인증절차 설정 진행
                .antMatchers("/*/login", "/*/signUp").permitAll() // 가입 및 인증 주소는 누구나 접근가능
                .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
                .and()
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

    }

@EnableWebSecurity

스프링 시큐리티 활성화

WebSecurityConfigureAdapter

스프리 시큐리티 설정 관련 클래스로 커스텀 설정 클래스가 이 클래스 상속받아 오버라이딩 설정을 하면 스프링 시큐리티에 반영된다.

antMatchers()

특정 url 에 대해서 어떻게 인증처리 할지 결정

permitAll()

인증과 상관없이 모두 통과시켜 사용할 수 있다.

addFilterBefore

UsernamePasswordAuthenticationFilter 이전에 필터를 등록하는 설정이다.

JwtAuthenticationFilter.java

: Jwt가 유효한 토큰인지 인증하기 위한 Filter이다.

public class JwtAuthenticationFilter extends GenericFilterBean {


    private JwtTokenProvider jwtTokenProvider;

    // Jwt Provier 주입
    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    // Request로 들어오는 Jwt Token의 유효성을 검증(jwtTokenProvider.validateToken)하는 filter를 filterChain에 등록합니다.
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
        if (token != null && jwtTokenProvider.validateToken(token)) {//토큰 검증
            Authentication auth = jwtTokenProvider.getAuthentication(token); //인증객체 생성
            SecurityContextHolder.getContext().setAuthentication(auth); // SecurityContextHolder에 인증 객체 저장
        }
        filterChain.doFilter(request, response);
    }
}

용자가 정의한 필터(JwtAuthenticationFilter)에서 인증 및 권한 작업을 진행할 것이기 때문에 AuthenticationManager를 사용하지 않고 JwtTokenProvider를 통해서 인증 후 SecurityContextHolder를 바로 사용했다.

JwtTokenProvider.java

: Jwt Token을 생성, 인증, 권한 부여, 유효성 검사, PK 추출 등의 다양한 기능을 제공하는 클래스

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

    private final Environment env;
    private final UserDetailsService userDetailsService;
    private String SECRET_KEY;

    private long EXPIRATION_TIME;


    @PostConstruct
    protected void init() {
        SECRET_KEY = Base64.getEncoder().encodeToString(env.getProperty("spring.jwt.secret").getBytes());
        EXPIRATION_TIME= Long.parseLong(env.getProperty("spring.jwt.expiration_time"));

    }

    // Jwt 토큰 생성
    public String createToken(String userPk, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(userPk);
        claims.put("roles", roles);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 데이터
                .setIssuedAt(now) // 토큰 발행일자
                .setExpiration(new Date(now.getTime() + EXPIRATION_TIME)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 암호화 알고리즘, secret값 세팅
                .compact();
    }

    // Jwt 토큰으로 인증 정보를 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // Jwt 토큰에서 회원 구별 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 Header에서 token 파싱 : "X-AUTH-TOKEN: jwt토큰"
    public String resolveToken(HttpServletRequest req) {
        return req.getHeader("X-AUTH-TOKEN");
    }

    // Jwt 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }


}

UserDetailsService.java

wtTokenProvider가 제공한 사용자 정보로 DB에서 알맞은 사용자 정보를 가져와 UserDetails 생성

    @Service
    @RequiredArgsConstructor
    public class CustomUserDetailService implements UserDetailsService {

        private final UserRepository userRepository;

        @Override
        public UserDetails loadUserByUsername(String email){
            return userRepository.findByEmail(email).orElseThrow(UserNotFoundException::new);
        }
    }

여기서 궁금증이 생겼다.

loadUserByUsername 의 역할은?

loadUserByUsername 라는 오버라이딩 메소드에서 Request에서 받은 로그인 데이터를 활용하여 로그인 작업을 해준다. 이때 loadUserByUsername 메소드는 인증된 결과를 가지고 UserDetails 인터페이스를 구현하는 인증대상객체를 리턴해준다.

근데
userRepository.findByEmail(email).orElseThrow(UserNotFoundException::new);
이렇게 이메일만을 일치하는지 살펴보고 비밀번호는 확인하는 코드가 없다....

찾아보니 스프링 시큐리티 내부에서 검사를 해준다고 한다.
기준이 되는 데이터는 User 객체에서 받은 데이터이다. 여기에 아이디와 패스워드가 들어있을 것이고 이는 DB에 저장된 값이므로 회원가입 당시에 입력된 데이터이고
DB 정보를 SpringSecurity에서 받아오고 난 후 사용자 입력데이터와 비교하는 것이다.
loadUserByUsername을 호출하여 인증을 마치고 내부적으로 SecurityUser 클래스와 Password를 passwordEncorder를 이용하여 처리할 수도 있도록 하는 서비스 로직이 존재한다.

DelegatingPasswordEncoder

기존 BCryptPasswordEncoder를 기반으로 단반향 암호화 인코더 PasswordEncoder를 사용해왔다.

  • 오래된 암호화 인코딩 방식을 사용하는 많은 애플리케이션이 존재한다.
  • 암호 저장에 대한 좋은 방법들은 다시 변경된다.
  • 스프링 시큐리티는 프레임 워크로서 자주 변화를 줄 수 없다.

이에 스프링 시큐리티는 위 세가지 문제를 모두 해결하는 DelegatingPasswordEncoder를 낸다.

Create Default DelegatingPasswordEncoder

public class MemberApplication {

    public static void main(String[] args) {
        SpringApplication.run(MemberApplication.class, args);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

참고문서
https://imbf.github.io/spring/2020/06/29/Spring-Security-with-JWT.html
https://sas-study.tistory.com/359?category=784778

0개의 댓글