티스토리 뷰

https://digda.tistory.com/38

 

[Spring Security + AOP] 로그를 찍어보자 - 로그인 인증 (1)

https://digda.tistory.com/36 Spring Security 흐름 Spring Security Stream 스프링 시큐리티의 제일 중요한 포인트는 인증(Authentication) 과 허가 (Authorization)이다. 인증 - '누구인지 증명하는 과정' 인가(허가) - '권한

digda.tistory.com

해당 예제의 github : https://github.com/digda5624/spring_security_study

 

GitHub - digda5624/spring_security_study: 서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발

서버 로깅 작업을 위한 스프링 시큐리티 예제 + 로깅 시스템 개발. Contribute to digda5624/spring_security_study development by creating an account on GitHub.

github.com


인증에 대한 성공을 마쳤으므로 JWT 를 발급해보겠습니다.

저 같은 경우에는 Spring Security Context 객체를 통해서 JWT 를 발급했는데요, Security Context 의 경우 Session Login 의 핵심 역할을 하게 되므로 잘 알아 두어야 한다고 생각합니다.

 

잠깐 Security Context Holder 의 내용을 살펴 봅시다.

 

SecurityContextHolder 는 말 그대로 Context 를 저장하는 저장소 개념의 역할을 합니다.

내부에는 Authentication 정보를 포함하고 있다고 생각해보 무방해 보입니다.

 

Security Context Holder 는 이 인증 객체에 대한 정보를 저장할 수 있는 정책이 여러가지가 존재하게 됩니다.

기본 정책은 Thread Local 입니다.

정책 내용
MODE_THREADLOCAL 스레드당 Context 객체를 할당. default
MODE_INHERITABLETHREADLOCAL 메인 스레드와 자식 스레드에 관하여 동일한 Context 유지
MODE_GLOBAL Memory 에 단 하나의 Context 만을 가지고 있음

예제에서는 기본적으로 스레드 로컬 전략을 사용하고 있고, JWT의 경우에 StateLess 한 성질을 목표로 만들었기 때문에 별도로 SecurityContext 에 인증 객체를 넣을 필요는 없습니다. (Login api 에 한해서)

 

현재 핵심은 인증 완료된 객체를 가지고 JWT 를 만들어 클라이언트에게 발급하는 것 입니다.

 

그렇다면 JWT 란 무엇일까요?

JWT 란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token 입니다.

JWT 는 Header, PayLoad, Signature의 구조로 각 부분은 Base64 로 인코딩 되어 표현 됩니다.

 

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

토큰의 헤더는 typ 와 alg 두 가지 정보로 구성이 되며 alg 의 경우 Signature 를 해싱하기 위한 알고리즘을 지정하는 것입니다.

 

토큰의 페이로드에는 토큰에서 사용할 정보의 조각들인 클레임(Claim) 이 담겨 있습니다. 클레임은 총 3가지로 나누어지며, Json(Key/Value) 형태로 다수의 정보를 넣을 수 있습니다.

 

1. Registered Claim - 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터

2. Public Claim - 사용자 정의 클레임, 공개용 정보를 위해 사용

3. Private Claim - 사용자 정의 클레임, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장

 

토큰의 서명은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드입니다. 서명은 위에서 만든 헤더와 페이로드의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성합니다.

 

자 이제 LoginSuccess Handler 에서 JWT 를 발급하는 로직을 작성해 봅시다.

손쉬운 JWT 작성을 위해 gradle 에 라이브러리를 추가합니다.

// JWT 의존성 추가
implementation group: 'com.auth0', name: 'java-jwt', version: '3.18.3'

JWT 와 관련된 설정 변수인 secretKey, 만료시간들의 경우에 application.yml 을 통해 따로 뺄 수 있지만 예제의 경우 static 변수를 통해 지정해 주었습니다.

 

@Slf4j
@Component
@RequiredArgsConstructor
public final class JwtUtils {

    private static final String secretKey = "digda's secretKey";
    private static final int jwtExpirationInMs = 1800;

    public static String createAccessToken(Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        return accessToken(user);
    }

    private static String accessToken(User user){
        Map<String, Object> payload = createClaims(user);

        return JWT.create()
                .withSubject("Digda_Test_token")
                .withExpiresAt(new Date(System.currentTimeMillis() + jwtExpirationInMs * 1000L))
                .withPayload(Map.of("userInfo", payload))
                .sign(Algorithm.HMAC512(secretKey));
    }

    private static Map<String, Object> createClaims(User user){
        Map<String, Object> claims = new HashMap<>();

        claims.put("id", user.getId());
        claims.put("name", user.getName());
        claims.put("role", user.getRole().toString());

        return claims;
    }

}

JWT 발급 유틸 클래스를 작성했으므로 LoginSuccessHandler 를 작성해 보겠습니다.

 

public class JwtLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    /**
     * 들어온 Authentication 기반으로 토큰 설정하기
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

        // Authentication 기반으로 토큰생성
        System.out.println("request = " + request);
        String accessToken = JwtUtils.createAccessToken(authentication);
        response.setHeader("Access-Token", accessToken);
        response.getWriter().println("login success");
    }
    
}

마지막으로 spring security 에 대한 설정 파일 작성을 통해서 인증에 대한 포스팅을 마쳐보겠습니다.

 

@Configuration
@EnableWebSecurity
@Import(UserDetailsServiceImpl.class)
public class JwtSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
        http.csrf().disable()
                // jwt 토큰을 사용하므로 세션 저장소에 Context를 저장할 필요없다.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 스프링 시큐리티에서 제공하는 로그인 폼을 사용하지 않을 것이므로 disable
                .formLogin().disable()
                .addFilterBefore(jwtAuthenticationFilter(authenticationManager), LogoutFilter.class)
                .authenticationManager(authenticationManager)
                // 일단 현재에서는 Test를 위해서 모든 요청들에 대해 인가 검사를 하지 않게 적용했다.
                .authorizeRequests()
                .anyRequest()
                .permitAll();

        return http.build();
    }

    /**
     * jwtAuthenticationFilter Bean 등록
     */
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(AuthenticationManager authenticationManager){
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager);
        filter.setAuthenticationSuccessHandler(new JwtLoginSuccessHandler());
        return filter;
    }

    /**
     * AuthenticationManager 등록
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){
        return new ProviderManager(authenticationProvider);
    }

    @Bean
    public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService){
        return new JwtAuthenticationProvider(userDetailsService);
    }
    
}

HttpSecurity.build() 를 통해서 스프링 시큐리티의 필터 체인을 빌드하고 이를 빈으로 등록합니다.

추가적으로 스프링 시큐리티 체인의 경우 우선순위가 사용자 정의 필터보다 선순위를 가지게 되므로 주의가 필요합니다.

댓글
05-20 14:04
Total
Today
Yesterday
링크