본문 바로가기

Java/Spring

[Spring] 간단한 SNS 만들기 #2

What to do?

인증기능

JWT를 사용한 인증기능 구현


UserEntity

 

  • UserDetail을 implement하고, 아래의 메써드들을 오버라이딩
@Entity
public class UserEntity implements UserDetails {

   ...
   
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Set.of(new SimpleGrantedAuthority(role.name()));
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

(지난번 포스팅과 코드는 동일)


application.yaml 파일

 

  • JWT 생성에 필요한 비밀키를 환경변수로 등록

DEV_JWT_SECRET_KEY라는 이름으로 환경변수 추가

 

  • JWT 생성에 필요한 설정 추가
    • secret-key : 암호화에 필요한 비밀키
    • duration : JWT 유효기간 (단위 : 1/1000초)
jwt:
  secret-key: ${DEV_JWT_SECRET_KEY}
  duration: 6048000000 # 7일

application.yaml


JWT Filter

 

  • doFiterInternal : 인증기능 처리
    1. 헤더 체크
      • null이 아닌지
      • Bearer XXXX 와 같은 문자열이 맞는지
    2. Claims 추출
    3. 유효기간 체크
    4. 유효한 토큰인 경우  SecurityContextHolder에 인증정보 담음
@Slf4j
@RequiredArgsConstructor
public class JwtUtil extends OncePerRequestFilter {
    private final UserService userService;
    private final String secretKey;

    /**
     * JWT 생성
     * @param username 인코딩할 문자열에 유저명 사용
     * @param secretKey 보안키 값
     * @param duration 토큰 유효시간 (Milli Second)
     * @return jwt 토큰값
     */
    public static String generateToken(String username, String secretKey, long duration){
        Claims claims = Jwts.claims();
        claims.put("username", username);
        long currentTimeMillis = System.currentTimeMillis();
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(currentTimeMillis))
                .setExpiration(new Date(currentTimeMillis+duration))
                .signWith(
                        Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)),
                        SignatureAlgorithm.HS256
                ).compact();
    }

    /**
     * JWT에서 claims 추출
     * @param jwt 토큰
     * @param secretKey 비밀키
     * @return Claims
     */
    public static Claims extractClaimsFromJwt(String jwt, String secretKey){
        Key signingKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
        return Jwts.parserBuilder()
                .setSigningKey(signingKey)
                .build()
                .parseClaimsJws(jwt)
                .getBody();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 헤더가 null이 아닌지 체크
        final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (header == null) {
            log.error("Header is null...");
            filterChain.doFilter(request, response);
            return;
        // JWT토큰이 Bearer로 시작하는 문자열이 맞는지 확인
        } else if (!header.startsWith("Bearer ")) {
            log.error("Authorization Header does not start with Bearer...");
            filterChain.doFilter(request, response);
            return;
        }

        // 인증토큰에서 JWT 분리
        final String jwt = header.split(" ")[1].trim();
        Claims claims = extractClaimsFromJwt(jwt, secretKey);
        
        // 토큰 유효기간이 지났는지 체크
        if (claims.getExpiration().before(new Date())){
            log.error("Token is expired...");
            filterChain.doFilter(request, response);
            return;
        }

        try {
            // JWT에서 유저명 추출
            String usernameFromJwt = claims.get("username", String.class);
            UserEntity user = userService.findByUsernameOrElseThrow(usernameFromJwt);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    user, null,
                    user.getAuthorities()
            );
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);

        } catch (RuntimeException e) {
            log.error(e.getMessage());
        } finally {
            filterChain.doFilter(request, response);
        }
    }
}

Configuration

 

  • Static 파일 경로에 대한 요청 허용
  • 회원가입, 로그인페이지로 POST요청 허용
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final UserService userService;
    @Value("${jwt.secret-key}") private String secretKey;
    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http
    ) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth
                        // Static(html, css, js, favicon...) 허용
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                        .permitAll()
                        .requestMatchers(
                                HttpMethod.POST,
                                "/api/*/user/register", "/api/*/user/login")
                        .permitAll()
                        .requestMatchers("/api/**")
                        .authenticated()
                        .anyRequest().permitAll()

                )
                // JWT 필터
                .addFilterBefore(new JwtUtil(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()
                .formLogin(withDefaults())
                .logout(logout -> logout.logoutSuccessUrl("/"))
                // csrf 풀기
                .csrf().disable()
                .build();
    }
}

 


PasswordEncoder

 

  • 패스워드 인코더를 Bean으로 등록해주어야 함
@Configuration
public class CustomPasswordEncoder {
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

왜인지는 모르겠지만... SecurityConfig에 넣으면 Circular Import 때문에 에러가 나서 따로 파일을 분리함

 

'Java > Spring' 카테고리의 다른 글