[BE] 스프링 시큐리티 - JWT와 필터

김주언·2022년 6월 16일
0

TODO LIST

목록 보기
10/18
post-thumbnail
  • 스프링 시큐리티
  • 서블릿 필터
  • JWT 인증 로직
  • 패스워드 암호화

~이전 게시물에서~
로그인 여부를 저장하지 않는 문제
→ 스프링 시큐리티와 JWT토큰 사용하여 해결하기

어떻게 구현하묘..?
제일 간단하게는..

  • 모든 API요청에 ID와 패스워드 , 또는 토큰 전송하기

그런데 이렇게 했다가 API가 100개라면..? 5개도 귀찮아서 하기시름
그래서 스프링 시큐리티를 쓴다..!

스프링 시큐리티 코드를 작성하고 해당 코드가 모든 API 수행하기 전에 실행되도록 해준다~~


JWT 생성

  1. 사용자 정보를 사용하여 헤더와 페이로드를 작성
  2. 작성된 {헤더}.{페이로드}를 전자서명하면 A라는 결과를 받는다
  3. {헤더}.{페이로드}.A를 Base64로 인코딩하여 토큰 리턴하기

JWT 디펜던시 추가

https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt

build.gradle의 dependencies에 추가해준다.

TokenProvider.java

JWT 생성해주는 라이브러리를 사용한다

package com.example.demo.security;

// import ...

@Slf4j
@Service
public class TokenProvider {
	// 시크릿 키 눈감고 키보드 두들기면 됨
    private static final String SECRET_KEY = "AS1SLsDFsdVLa5SD";
	
    // 사용자에게 할당될 토큰 생성해주는 create 메서드
    public String create(UserEntity userEntity) {
    	// 유효기간은 하루
        Date expDate = Date.from(
                Instant.now()
                        .plus(1, ChronoUnit.DAYS)
        );
        		// 토큰 생성
        return Jwts.builder()
        		// header 내용
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                // payload 내용
                .setSubject(userEntity.getId())	// 토큰 소유자
                .setIssuer("JUEON^-^v")			// 토큰 발행자
                .setIssuedAt(new Date())		// 토큰 발행일
                .setExpiration(expDate)			// 토큰 만료일
                .compact();
    }

    public String validateAndGetUserId(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();

        return  claims.getSubject();
    }
}
  • validateAndGetUserId
    parseClaimsJws가 Base64로 디코딩 및 파싱 수행한다.
  1. 헤더와 페이로드를 setSigningKey()의 매개변수로 넘어온 시크릿 키를 이용하여 서명한다.
  2. parseClaimsJws()의 매개변수로 전달된 token이 위조되지 않은 토큰이라면 Claims(페이로드)를 리턴한다.
  3. 페이로드 중 userId를 추출하기 위해(subject를 추출하기 위해) getBody() 호출

이제 로그인 시 TokenProvider를 사용하여 토큰을 생성한 뒤 UserDTO에 반환하면 된다.

UserController.java

  1. TokenProvider 의존 주입
  2. authenticate 메서드에서 토큰 생성
// 의존주입
@Autowired
    private TokenProvider tokenProvider;

// ...

@PostMapping("/signin")
    public ResponseEntity<?> authenticate(@RequestBody UserDTO userDTO) {
        UserEntity user = userService.getByCredentials(
                userDTO.getEmail(), userDTO.getPassword());
        if (user != null) {
        	// 토큰 생성
            final String token = tokenProvider.create(user);
            final UserDTO responseUserDTO = UserDTO.builder()
                    .email(user.getEmail())
                    .id(user.getId())
                    // 응답으로 토큰 전송
                    .token(token)
                    .build();
            // 생략
    }

토큰 생성 완.
로그인 시 생성한 토큰 리턴 완.

이제 API마다 인증하는 거 구현하면 된다... 🫠


스프링 시큐리티

  1. 사용자에게서 받은 토큰을 Base64로 디코딩
  2. 디코딩하면 결과는 {헤더}.{페이로드}.A가 된다
  3. {헤더}.{페이로드}를 secret키로 전자서명 해주면 B라는 결과를 얻는다
  4. A=B이면 서명이 일치하는 것 → 검증완료

위의 순서가 API 실행될 때마다 수행되어야 사용자 인증이 완료되는 것이다.
이렇게 API 실행마다 토큰인증을 해주려면 컨트롤러의 메서드 첫 부분마다 인증코드를 작성해야한다.
→ 서블릿 필터를 사용하여 해결한다


서블릿 필터

서블릿 실행 전에 실행되는 클래스
스프링의 서블릿 이름은 디스패처 서블릿인데, 서블릿 필터는 디스패처 서블릿이 실행되기 전 항상 실행된다. 스프링 시큐리티는 일종의 서블릿 필터의 집합이다.

서블릿 필터는 HttpFilterFilter를 상속하는 클래스이다. 이를 상속해서 doFilter() 메서드를 오버라이딩하면 된다.

오버라이딩하여 구현을 끝낸 후에는 서블릿 컨테이너가 필터클래스를 사용하도록 web.xml같은 설정 파일에 경로를 지정 해줘야 한다. 일일히 하면 ㄱ ㅐ고생이다.. 스프링 시큐리티가 해준다^^!

스프링 서블릿 필터

스프링 시큐리티를 추가하면 스프링 시큐리티가 FilterChainProxy라는 필터를 서블릿 필터에 추가한다. 이 필터 클래스는 내부적으로 필터를 실행시키는데 이 내부의 필터들이 스프링이 관리하는 스프링 빈 필터이다.

스프링 시큐리티를 사용하면 HttpFilterFilter 대신 OncePerRequestFilter를 상속하고, web.xml 대신 WebSecurityConfigurerAdapter 클래스를 상속하여 필터를 설정한다.

구현

dependency 추가

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.6.8'

AuthenticationFilter.java

한 요청 당 한번씩만 인증하면 되기 때문에 OncePerRequestFilter를 상속하는 클래스를 작성해줄 것이다.

  1. parseBearerToken() : 요청의 헤더에서 Bearer 토큰 추출하기
  2. TokenProvider를 이용하여 토큰 인증 작업 수행 후 UsernamePasswordAuthenticationToken 생성
  3. SecurityContextHolder의 createEmptyContext() 메서드를 사용하여 SecurityContext 생성
  4. UsernamePasswordAuthenticationToken에 사용자의 인증 정보(authenticationToken)를 저장하고 SecurityContext에 사용자 등록
  5. 생성한 컨텍스트에 인증정보를 전달하고 SecurityContext에 SecurityContextHolder의 등록해준다.
💡 SecurityContextHolder는 ThreadLocal에 저장되기 때문에 스레드마다 하나의 컨텍스트 관리가 가능하며, 
같은 스레드 내에 있다면 어디서든 접근이 가능하다.
<package com.example.demo.security;
// import ...

@Slf4j
@Component
public class AuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
        	
            // request에서 토큰 파싱하기

            String token = parseToken(request);
				
            // 토큰이 존재할 경우 유효성 검사하기
           // equalsIgnoreCase 대소문자 구분하지 않고 문자열을 검사한다.
            if (token != null && !token.equalsIgnoreCase("null")) {
            	// 토큰 인증과정을 거쳐서 유저의 id 추출
                String userId = tokenProvider.validateAndGetUserId(token);
                
                // 인증된 토큰을 SecurityContext에 등록해줘야 인증된 사용자를 인식한다.
                AbstractAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userId, null, AuthorityUtils.NO_AUTHORITIES);
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                securityContext.setAuthentication(authenticationToken);
                SecurityContextHolder.setContext(securityContext);
            }
        } catch (Exception ex) {
            logger.error(ex);
        }
        filterChain.doFilter(request, response);
    }


	// HTTP requset의 헤더부분을 파싱하여 Bearer 토큰을 리턴하는 메서드
    private String parseToken(HttpServletRequest request) {
        String tokenFromReq = request.getHeader("Authorization");

        if (StringUtils.hasText(tokenFromReq) && tokenFromReq.startsWith("Bearer ")) {
            return tokenFromReq.substring(7);

        }
        return null;

    }

}

스프링 시큐리티 Configuration

서블릿 필터를 사용하려면

  1. 서블릿 필터를 구현하고
  2. 서블릿 컨테이너에 해당 서블릿 필터를 사용하는 설정 추가
    • 스프링 시큐리티에 구현한 AuthenticationFilter 클래스를 사용하도록 설정해줘야한다.

WebSecurityConfig

package com.example.demo.config;

// import ...

@Slf4j
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@Configuration
public class WebSecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors() // WebMvcConfig에서 이미 설정했으므로 기본 cors 설정.
                .and()
                .csrf()// csrf는 현재 사용하지 않으므로 disable
                .disable()
                .httpBasic()// token을 사용하므로 basic 인증 disable
                .disable()
                .sessionManagement()  // session 기반이 아님을 선언
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests() // /와 /auth/** 경로는 인증 안해도 됨.
                .antMatchers("/", "/auth/**").permitAll()
                .anyRequest() // /와 /auth/**이외의 모든 경로는 인증 해야됨.
                .authenticated();

        // filter 등록.
        // 매 리퀘스트마다
        // CorsFilter 실행한 후에
        // jwtAuthenticationFilter 실행한다.
        http.addFilterAfter(
                jwtAuthenticationFilter,
                CorsFilter.class
        );
        return http.build();
    }

}
profile
학생 점심을 좀 차리시길 바랍니다

0개의 댓글