ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringBoot - JWT ( JSON WEB TOKEN ) - 코드편
    Spring-Boot/etc. 2022. 5. 9. 15:03

    HS512 알고리즘을 사용할 것이기 때문에 secret key는 512bit, 즉 64byte 이상을 사용해야 합니다.

    터미널에서 secret key를 base64로 인코딩하여 secret 항목에 채워넣습니다.

    $ echo ‘silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret’|base64

    application.yml

    jwt:
      header: Authorization
      secret: 4oCYc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXTigJkK
      token-validity-in-seconds: 86400

    ( 저는 그냥 하드 코딩했습니다. 하지만 yml 파일에서 관리하는게 좋습니다. )


    Filter방식으로 JWT 토큰 활용하기

    1. WebSecurityConfig

         - corsFilter는 cors방지용 필터

         - JwtAuthenticationFilter : 로그인 성공시 토큰발급해주는 필터

         - JwtAuthorizationFilter : 해당 토큰 보안여부 판단해주는 필터

                                          ( /joinMember와 /readBoard 이외에는 이 필터가 판단)

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
        	http
            	.csrf().disable()   // jwt토큰 방식을 쓰기에 필요한 설정
    
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)        // 세션을 사용하지 않기에 stateless
    
                    .and()
                    .addFilter(corsFilter)      // @CrossOrigin(인증X), 시큐리티 필터에 등록 인증(O)
                    .formLogin().disable()      // 폼로그인 사용X
                    .httpBasic().disable()      // Bearer 방식을 쓸거기 때문
                    .addFilter(new JwtAuthenticationFilter(authenticationManager()))   // AuthenticationManager
                    .addFilter(new JwtAuthorizationFilter(authenticationManager(),memberMapper))
                    .authorizeRequests()
                    .antMatchers("/joinMember","/readBoard").permitAll()                       //login페이지
                    .anyRequest().authenticated();
     	}
     }

    2. CorsConfig

    @Configuration
    public class CorsConfig {
    
        @Bean
        public CorsFilter corsFilter() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(true);       // 내 서버가 응답할 때 json을 JS에서 처리할 수 있게 설정
            config.addAllowedOrigin("*");           // 모든 ip에 응답을 허용
            config.addAllowedHeader("*");           // 모든 header에 응답 허용
            config.addAllowedMethod("*");           // 모든 post,get,put,delete,patch 요청허용
            source.registerCorsConfiguration("/**",config);
            return new CorsFilter(source);
        }
    
    }

    3. JwtAuthenticationFilter

    // 스프링 시큐리티에서 UsernamePasswordAuthenticationFilter 가 있음
    // /login 요청해서 username, password 전송하면 (post)
    // UsernamePasswordAuthenticationFilter 동작함
    
    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
        private final AuthenticationManager authenticationManager;
    
        // /login요청을 하면 로그인 시도를 위해서 실행되는 함수
       @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
    
    
           // 1. username,password 받아서
           try {
    
               // json데이터를 parsing해줌
               ObjectMapper om = new ObjectMapper();
               MemberVO mem = om.readValue(request.getInputStream(), MemberVO.class);
               System.out.println("request요청 값들 :"+mem);
    
               UsernamePasswordAuthenticationToken authenticationToken =
                       new UsernamePasswordAuthenticationToken(mem.getM_id(),mem.getM_pw());
    
               // PrincipalDetailsService의 loadUserByUsername() 함수가 실행된 후 정상이면 authentication이 리턴
               // DB에 있는 username과 password가 일치한다.
               // pw는 스프링이 알아서 처리 해줌
               Authentication authentication =
                       authenticationManager.authenticate(authenticationToken);
    
               // => 로그인이 되었다는 뜻
               PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
                System.out.println("로그인 완료 여부: "+principalDetails.getUsername());
               // authentication 객체가 session영역에 저장을 해야하고 그 방법이 return 해주면 됨
               // 리턴의 이유는 권한 관리를 security가 대신 해주기 떄문에 편하려고 하는 거임
               // 굳이 JWT토큰을 사용하면서 세션을 만들 이유가 없음. 근데 단지 권한 처리때문에 session넣어줌
                return  authentication;
           } catch (IOException e) {
               e.printStackTrace();
               System.out.println(111111);
           }
           // 2. 정상인지 로그인 시도를 해보는 것, authenticationManager로 로그인 시도를 하면!!
           //    PrincipalDetailsService가 호출 -> loadUserByUsername이 자동으로 실행
    
           // 3. PrincipalDetails를 세션에 담고 ( 권한 관리를 위해서 )
    
           // 4. JWT토큰을 만들어서 응답해주면 됨.
           return null;
        }
        
        // attemptAuthentication 실행 후 인증이 정상적으로 되었으면 successfulAuthentication 함수 실행
        // JWT 토큰을 만들어서 request요청한 사용자에게 JWT토큰을 response해주면 됨
        @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("cos토큰")
                    .withExpiresAt(new Date(System.currentTimeMillis()+(60000*10000)))
                    .withClaim("username",principalDetails.getUsername())
                    .withClaim("m_seq", Integer.toString(principalDetails.getMem().getM_seq()))
                    .sign(Algorithm.HMAC512("cos"));
    
           response.addHeader("Authorization","Bearer "+jwtToken);
        }
    }

    - token의 규칙은 { "Authorization" : "Bearer 토큰명" } 이다. 

      ( 안지키면 token 판별을 못하더라구요.... )

    - attemptAuthentication의 authenticationManager.authenticate가 실행 시 PrincipalDetailService의

      loadUserByUsername이 실행되므로 다음 파일로 보겠다.

     

    4. PrincipalDetailsService

    @Service
    @RequiredArgsConstructor
    public class PrincipalDetailsService implements UserDetailsService {
    
        private final MemberMapper memberMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            System.out.println("loadUserByUsername 로그인요청");
            MemberVO user = memberMapper.selectId(username);
            return new PrincipalDetails(user);
        }
    }

    - loadUserByUsername의 반환변수가 UserDetails이므로 PrincipalDetails 파일도 첨부

     

    + PrincipalDetails

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class PrincipalDetails implements UserDetails {
    
        private MemberVO mem;
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    return  mem.getM_role();
                }
            });
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return mem.getM_pw();
        }
    
        @Override
        public String getUsername() {
            return mem.getM_id();
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }

    5. JwtAuthorizationFilter 

    - 토큰 인증해주는 필터

    // 시큐리티가 filter가지고 있는 그 필터중에 BasicAuthenticationFilter 라는 것이 있음
    // 권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어있음
    // 만약에 권한이 인증이 필요한 주소가 아니라면 이 필터를 안탄다.
    public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
        private MemberMapper memberMapper;
    
        public JwtAuthorizationFilter(AuthenticationManager authenticationManager, MemberMapper memberMapper) {
            super(authenticationManager);
            this.memberMapper = memberMapper;
        }
    
        // 인증이나 권한이 필요한 주소요청이 있을 때 해당 필터를 타게 됨
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            System.out.println("인증이나 권한이 필요한 주소 요청이됨");
    
            String jwtHeader = request.getHeader("Authorization");
            System.out.println("jwtHeader : " + jwtHeader);
    
            // header가 있는 확인
            if (jwtHeader == null || jwtHeader.trim().isEmpty() || !jwtHeader.startsWith("Bearer")) {
                chain.doFilter(request, response);
            }else {
    
                // JWT 토큰을 검증을 해서 정상적인 사용자인지 확인
                String jwtToken = request.getHeader("Authorization").replace("Bearer ", "");
    
                String username =
                        JWT.require(Algorithm.HMAC512("cos")).build().verify(jwtToken).getClaim("username").asString();
    
                // 서명이 정상적으로 됨
                if (username != null) {
                    MemberVO mem = memberMapper.selectId(username);
    
                    PrincipalDetails principalDetails = new PrincipalDetails(mem);
                    // Jwt 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
                    Authentication authentication =
                            new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
    
                    // 강제로 시큐리티의 세션에 접근하여 Authentication 객체를 저장.
                    SecurityContextHolder.getContext().setAuthentication(authentication);
    
                    chain.doFilter(request, response);
                }
    
            }
        }
    
    
    }

     

    + Controller에서 토큰 복호화해서 쓰기

    @PostMapping("/")
    public void 토큰복화화(@RequestHeader("Authorization") String jwtToken){
    	// 토큰 복호화 과정
            String token = jwtToken.substring(7);
            System.out.println("post에서 받은 토큰 : "+token);
            String m_seq =
                    JWT.require(Algorithm.HMAC512("cos")).build().verify(token).getClaim("m_seq").asString();
            String username =
                    JWT.require(Algorithm.HMAC512("cos")).build().verify(token).getClaim("username").asString();
            System.out.println("post user : "+m_seq+ " / " + username);
    }

     


    마무리

    Spring-Security와 JWT토큰 적용시키기위해 4일동안 공부하고 코드쓰고... 쉽지 않았다...

    @Override때문에 기존의 스프링 라이브러리 읽는 것도 연습해야겠고 자동적으로 실행하는 순서때문에 많이 애먹은거같다. 나중에는 편하다고 느껴지겠지만 지금은 이 자동적으로 실행하는거 때문에 많은 시간을 썼다.

    정리하자면 

     

    1. 로그인과정 + 토큰 발급

    WebScurityConfig.java

    -> JwtAuthenticationFilter.java( extends UsernamePasswordAuthenticationFilter ) 의

             attemptAuthentication ( @Override )에서 로그인을 처리해주는데 해당 함수에서

    -> PrincipalDetailsService( implements UserDetailsService )의 loadUserByUsername( @Override )을 실행하고

    -> PrincipalDetails( implements UserDetails )를 반환한다.

    -> 로그인을 성공하면 attemptAuthentication( @Override ) 실행 후 바로

               successfulAuthentication( @Override )을 실행해서 토큰을 발급해준다.

    2.  토큰 인증

    WebScurityConfig.java

    -> WebScurityConfig.java에서 권한이나 인증이 필요한 특정 주소가 요청이 온다면 

        JwtAuthorizaionFilter( extends BasicAuthenticationFilter )의

                                   doFilterInternal( @Override )이 실행되고 토큰 인증을 하게 된다.

     

     

    implements ,extends, @Override 의 중요성을 많이 깨닫고 간다...

    다음은 JWT를 인터셉터로 한번해보거나 JWT토큰 코드를 좀 더 리펙토링해봐야겠다.

     

    github : https://github.com/PHyeonMIN/boardDemo

     

    ( config만 보시면됩니다. config파일과 같은 라인에있는 jwt는 실험용이었습니다..ㅠ )

    댓글

Designed by Tistory.