개인 포트폴리오/TodoList

TodoList 프로젝트 - Spring Security, JWT 설정

roalwh 2023. 10. 19. 15:23

Spring Security 설정

  • Spring Security를 활용한 인증 및 인가

  • JWT(JSON(JavaScript Object Notation) Web Token)를 이용한 로그인 및 회원 가입 처리

  • WebMvcConfig, WebSecurityConfig 를 이용한 SecurityFilterChain 설정

1. JWT(JSON Web Token) 생성 및 인증 처리

  • 유저 정보를 받아서 JWT 를 생성해준다.

  • 생성된 토큰값으로 사용자 인증 진행

1-1 인증 토큰 생성

@Service
public class TokenProvider {
    private static final String SECRET_KEY = "무작위 난수";
    byte[] keyBytes = SECRET_KEY.getBytes();
    Key key = Keys.hmacShaKeyFor(keyBytes);

    public String create(UserEntity userEntity) {
        // 기한 시작으로부터 1일로 설정
        Date expiryDate = Date.from(
                Instant.now().plus(1, ChronoUnit.DAYS));

        return Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS512)
                .setSubject(userEntity.getId())
                .setIssuer("demo app")
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .compact();
    }

    public String validateAndGetUserId(String token) {
        // parseClaimsJws 메서드가 Base 64로 디코딩 및 파싱.
        // 즉, 헤더와 페이로드를 setSigningKey로 넘어온 시크릿을 이용해 서명 후, token의 서명과 비교.
        // 위조되지 않았다면 페이로드(Claims) 리턴, 위조라면 예외를 날림
        // userId가 필요하므로 getBody를 부른다.
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key).build()
                .parseClaimsJws(token).getBody();
        return claims.getSubject();
    }

}

1-2 인증 필터 설정

  • OncePerRequestFilte 클래스를 상속받아서 설정함

  • OncePerRequestFilte는 한 요청당 반드시 한번만 실행되는 필터

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            // 요청에서 토큰 가져오기
            String token = parseBearerToken(request);
            log.info("Filter is running....");

            // 토큰 검사, JWT이므로 인가 서버에 요청하지 않고도 검증가능
            if (token != null && !token.equalsIgnoreCase("null")) {
                // userId 가져오기, 위조된경우 예외처리됨(tokenProvider.java 파일 참고)
                String userId = tokenProvider.validateAndGetUserId(token);
                log.info("Authenticated user ID : " + userId);

                // 인증완료; SecurityContextHolder에 등록되야 인증된 사용자로 처리함
                AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        // userId-> 인증된 사용자의 정보, 문자열이 아니어도 아무거나 등록가능, 보통 UserDetails 오브젝트로 처리하나 책에서는 생략한다고함,
                        // AuthenticationPrincipal
                        userId,
                        null,
                        AuthorityUtils.NO_AUTHORITIES);

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                securityContext.setAuthentication(authentication);
                SecurityContextHolder.setContext(securityContext);
            }
        } catch (Exception ex) {
            logger.error("Coule not set user authentication in security context", ex);
        }
        filterChain.doFilter(request, response);
    }

    private String parseBearerToken(HttpServletRequest request) {
        // Http 요청의 헤더를 파싱하여 Bearer 토큰을 리턴시킨다.
        String bearerToken = request.getHeader("Authorization");
            // hasText(bearerToken) 널같은 값체크, startsWith("Bearer ") 시작을 Bearer로 시작하는지
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            // substring(7)  bearerToken 토큰의 앞 7자리 리턴
            return bearerToken.substring(7);
        }
        return null;
    }

}

2. Spring Security 설정

@EnableWebSecurity
@Configuration
@Slf4j
// WebSecurityConfigurerAdapter 더이상 사용 안됨
public class WebSecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jdJwtAuthenticationFilter;

    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
        // https://docs.spring.io/spring-security/reference/servlet/configuration/java.html
        // https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html

        // WebMvcConfig에서 이미 설정했으므로 기본 cors 설정.
        http.cors((cors) -> cors.disable());
        // csrf는 현재 사용하지 않으므로 disable
        http.csrf((csrf) -> csrf.disable());
        // token을 사용하므로 basic 인증 disable
        http.httpBasic((basic) -> basic.disable());
        // session 기반이 아님을 선언
        http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        // /와 /auth/** 경로는 인증 안해도 됨.
        http.authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/", "/auth/**", "/error", "/login", "/todo").permitAll()
                // /와 /auth/**이외의 모든 경로는 인증 해야됨.
                .anyRequest().authenticated());
        http.addFilterAfter(
                jdJwtAuthenticationFilter,
                CorsFilter.class);

        return http.build();
    }

}