Spring Security JWT 생성 및 인증

바그다드·2023년 4월 20일
0

Spring Security

목록 보기
17/17
  • jwt를 이용해 개인정보에 접근하면 전자서명을 통해 접근이 가능하도록 구현해보자

JwtAuthorizationFilter 생성

// 시큐리티가 filter를 가지고 있는데 그 필터중에 BasicAuthenticationFilter라는 것이 있음
// 권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어 있음
// 만약에 권한이나 인증이 필요한 주소가 아니라면 이 필터를 사용하지 않음

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {


    public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    // 인증이나 권한이 필요한 주소 요청이 있을 때 해당 필터를 타게 됨
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        super.doFilterInternal(request, response, chain);
        System.out.println("인증이나 권한이 필요한 주소 요청 됨");

        String jwtHeader = request.getHeader("Authorization");
        System.out.println("jwtHeader = " + jwtHeader);
    }
}
  • 이제 이 필터를 등록해주자

SecurityConfig 수정

public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
            http
                    .addFilter(corsConfig.corsFilter())
                    .addFilter(new JwtAuthenticationFilter(authenticationManager))
                    // 추가
                    .addFilter(new JwtAuthorizationFilter(authenticationManager));
        }
    }
  • 이제 포스트맨으로 login을 시도하고, 권한이 필요한 페이지인 '/api/v1/user/'로 접근해보자

    username과 password가 일치했다면 응답으로 authorization에 토큰값이 넘어왔을 것이다. 이것을 이용해 권한이 필요한 페이지에 요청을 보내보자.

    아직 별다른 후속 기능을 해두지 않아 403에러가 발생하지만 콘솔을 확인해보면

    토큰 값이 제대로 넘어오는 것을 확인할 수 있다!!
    * 참고로 Authorization필드에는 한글을 적을수 없다.
    이제 이것을 이용해 후처리를 해주자

JwtAuthorizationFilter 수정

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private UserRepository userRepository;


    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
        super(authenticationManager);
        this.userRepository = userRepository;
    }

    // 인증이나 권한이 필요한 주소 요청이 있을 때 해당 필터를 타게 됨
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        super.doFilterInternal(request, response, chain);
        System.out.println("인증이나 권한이 필요한 주소 요청 됨");

        String jwtHeader = request.getHeader("Authorization");
        System.out.println("jwtHeader = " + jwtHeader);
		
        //*******여기서부터 추가하면 됨***********
        // header에 토큰이 있는지 확인
        if (jwtHeader == null || !jwtHeader.startsWith("Bearer")) {
            chain.doFilter(request, response);
            return;
        }

        // jwt를 검증해서 정상적인 사용자인지 확인
        String jwtToken = request.getHeader("Authorization").replace("Bearer ", "");

        // 암호화를 할 때 HMAC512("pem")이라고 넣었으므로 여기서도 pem이라는 단어를 이용해서 복호화 함
        String username =
                JWT.require(Algorithm.HMAC512("pem")).build().verify(jwtToken).getClaim("username").asString();

        // 서명이 정상적으로 되었다는 뜻
        if (username != null) {
            User userEntity = userRepository.findByUsername(username);

            PrincipalDetails principalDetails = new PrincipalDetails(userEntity);

            // Jwt 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
            Authentication authentication =
                    new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());

            // 강제로 시큐리티 세션에 접근하여 Authentication 객체를 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);

            chain.doFilter(request, response);
        }


    }
}
  1. 헤더에 들어있는 jwt를 복호화 하고, 여기서 username을 가져온다.
  2. db에서 username과 일치한 사용자 정보를 찾아 가져오고, 이것을 이용해 PrincipalDetails(UserDetails상속)를 생성한다.
  3. new UsernamePasswordAuthenticationToken()를 이용해 인자로 PrincipalDetails, 암호(여기서는 토큰을 이용한 전자서명 방식이므로 null을 넣었다), 권한 정보를 넘겨주고 Authentication을 생성한다.
  4. 시큐리티 세션에 접근하여 생성된 Authentication를 저장한다.
  5. 이후 필터로 다시 돌아간다.
  • Repository를 생성자에서 받아야 하므로 SecurityConfig에 추가해주자

SecurityConfig 수정

public class SecurityConfig{

    private CorsConfig corsConfig;
    // 추가
    private UserRepository userRepository;
	
    // 수정
    public SecurityConfig(CorsConfig corsConfig, UserRepository userRepository) {
        this.corsConfig = corsConfig;
        this.userRepository = userRepository;
    }
	
    // 중간 생략
    
        public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
            http
                    .addFilter(corsConfig.corsFilter())
                    .addFilter(new JwtAuthenticationFilter(authenticationManager))
                    // 수정
                    .addFilter(new JwtAuthorizationFilter(authenticationManager, userRepository));
        }
    }
  • 이제 다시 프로젝트를 실행하고 포스트맨으로 권한이 필요한 페이지에 요청을 보내보면

    아까와는 다르게 500에러가 뜬다!!!
    그럼 이제 테스트를 위한 주소를 생성해주자

RestApiController 수정

    // user, manager, admin 권한 접근 가능
    @GetMapping("/api/v1/user")
    public String user(Authentication authentication) {
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        System.out.println("principalDetails = " + principalDetails.getUsername());
        return "user";
    }

    // manager, admin 접근 가능
    @GetMapping("/api/v1/manager")
    public String manager() {
        return "manager";
    }

    // admin만 접근 가능
    @GetMapping("/api/v1/admin")
    public String admin() {
        return "admin";
    }
  • 이제 다시 포스트맨으로 요청을 보내보자

    그럼 null point exception이 발생하는데 원인은 Security session이 생성되지 않아 에러가 발생한 것이다.

에러 발생 원인

  • JwtAuthorizationFilter의 doFilterInternal에서 아래처럼 주석 처리한 부분을 주석 처리해주자. 그렇지 않으면
    super.doFilterInternal(request, response, chain); 여기서 한번
    chain.doFilter(request, response); 여기서 한번
    총 두번의 응답이 이뤄져 에러가 발생한다.
@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //아래 부분 주석!!
//        super.doFilterInternal(request, response, chain);
        System.out.println("인증이나 권한이 필요한 주소 요청 됨");

        ...... 중간 생략

            chain.doFilter(request, response);
        }


    }
}
  • 수정 후 다시 요청을 보내보면 정상적으로 응답이 오는 것을 확인할 수 있다!!!

JwtProperties 인터페이스 생성

  • 유지보수나 확장성을 생각해서 jwt를 생성할 때 쓰이는 값을들 따로 모아 인터페이스를 생성해주자
package com.pem.jwt.config.jwt; // 위치

public interface JwtProperties {
    String SECRET = "바그다드"; // 우리 서버만 알고 있는 비밀값
    int EXPIRATION_TIME = 60000 * 10;
    String TOKEN_PREFIX = "Bearer ";
    String HEADER_STRING = "Authorization";
}
  • 그리고 JwtAuthenticationFilter와 JwtAuthorizationFilter에서 이 값들을 사용해야 하는 부분을 수정해주자!

JwtAuthenticationFilter 수정

  • JwtProperties값을 사용해야 하는 부분을 수정해주자
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        System.out.println("successfulAuthentication이 실행됨 => 인증이 완료됨");


        PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();

        // RSA방식은 아니구 Hash암호방식
        // 이 부분 수정
        String jwtToken = JWT.create()
                .withSubject(principalDetails.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
                .withClaim("id", principalDetails.getUser().getId())
                .withClaim("username", principalDetails.getUser().getUsername())
                .sign(Algorithm.HMAC512(JwtProperties.SECRET));

        response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX+jwtToken);

        System.out.println("토큰 생성 후 헤더에 첨부 완료");
//        super.successfulAuthentication(request,response,chain,authResult);
    }

JwtAuthorizationFilter 수정

  • 마찬가지로 JwtProperties값을 사용하는 부분을 수정해주자
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //아래 부분 주석!!
//        super.doFilterInternal(request, response, chain);
        System.out.println("인증이나 권한이 필요한 주소 요청 됨");

        String jwtHeader = request.getHeader(JwtProperties.HEADER_STRING);
        System.out.println("jwtHeader = " + jwtHeader);

        // header에 토큰이 있는지 확인
        if (jwtHeader == null || !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }

        // jwt를 검증해서 정상적인 사용자인지 확인
        String jwtToken = request.getHeader(JwtProperties.HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, "");

        // 암호화를 할 때 HMAC512("pem")이라고 넣었으므로 여기서도 pem이라는 단어를 이용해서 복호화 함
        String username =
                JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(jwtToken).getClaim("username").asString();

        // 서명이 정상적으로 되었다는 뜻
        if (username != null) {
            System.out.println("username 정상");
            User userEntity = userRepository.findByUsername(username);

            PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
            System.out.println("principalDetails = " + principalDetails.getUsername() + " user name이 찍여야 함");

            // Jwt 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
            Authentication authentication =
                    new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());

            // 강제로 시큐리티 세션에 접근하여 Authentication 객체를 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);


        }

        chain.doFilter(request, response);
    }
  • 이것으로 필터를 등록하고, jwt를 생성해서 인증을 하는 기능을 구현해 보았다!!!
profile
꾸준히 하자!

0개의 댓글