ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • spring security+jwt 회원가입, 로그인 #3
    SOLUX-완숙이 2022. 1. 17. 15:07

    2022.01.12 - [SOLUX-완숙이] - spring security+jwt 회원가입, 로그인 #2

     

    spring security+jwt 회원가입, 로그인 #2

    2022.01.12 - [SOLUX-완숙이] - spring security+jwt 회원가입, 로그인 #1 spring security+jwt 회원가입, 로그인 #1 security 적용 전 회원가입 코드 1. Member 클래스 package solux.wansuki.OurNeighbor_BE.dom..

    leeeehhjj.tistory.com


    JWT 를 활용해서 AccessToken과 RefreshToken을 발급하고, 프론트에서 헤더에 access token을 붙여 보내면 이를 검증하여 권한에 따라 접근을 허용해주는 코드이다.

     

    1. MemberService 클래스

    @Transactional
        public TokenInfoResponseDto login(LoginDto loginDto) {
            if (memberRepository.findByLoginId(loginDto.getLoginId()).orElse(null) == null) {
                return null;
            }
    
            UsernamePasswordAuthenticationToken authenticationToken = loginDto.toAuthentication();
    
            Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    
            TokenInfoResponseDto tokenInfo = jwtTokenProvider.generateToken(authentication);
    
            return tokenInfo;
        }

    인증이 완료된 정보가 authentication 변수에 담기고 이를 jwtTokenProvider의 generateToken 메소드로 넘겨서 accessToken을 생성한다.

     

    2. application properties 파일

    #Jwt
    jwt.secret=자신이 원하는 값 입력

     JwtTokenProvider

    import io.jsonwebtoken.*;
    import io.jsonwebtoken.io.Decoders;
    import io.jsonwebtoken.security.Keys;
    import io.jsonwebtoken.security.SecurityException;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    import solux.wansuki.OurNeighbor_BE.dto.Member.TokenInfoResponseDto;
    
    import java.security.Key;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.Date;
    import java.util.stream.Collectors;
    
    @Slf4j
    @Component
    public class JwtTokenProvider {
    
        private static final String AUTHORITIES_KEY = "auth";
        private static final String BEARER_TYPE = "Bearer";
        private static final long ACCESS_TOKEN_EXPIRE_TIME = 30*60*1000L; //30분
        private static final long REFRESH_TOKEN_EXPIRE_TIME = 30 * 24 * 60 * 60 * 1000L; //30일
    
        private final Key key;
    
        public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
            byte[] keyBytes = Decoders.BASE64.decode(secretKey);
            this.key = Keys.hmacShaKeyFor(keyBytes);
        }
    
        public TokenInfoResponseDto generateToken(Authentication authentication) {
            //권한 가져오기
            String authorities = authentication.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.joining(","));
    
            long now = (new Date()).getTime();
    
            //AccessToken 생성
            Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
            String accessToken = Jwts.builder()
                    .setSubject(authentication.getName())
                    .claim(AUTHORITIES_KEY, authorities)
                    .setExpiration(accessTokenExpiresIn)
                    .signWith(key, SignatureAlgorithm.HS256)
                    .compact();
    
            //RefreshToken 생성
            String refreshToken = Jwts.builder()
                    .setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
                    .signWith(key, SignatureAlgorithm.HS256)
                    .compact();
    
            return TokenInfoResponseDto.builder()
                    .grantType(BEARER_TYPE)
                    .accessToken(accessToken)
                    .refreshToken(refreshToken)
                    .refreshTokenExpirationTime(REFRESH_TOKEN_EXPIRE_TIME)
                    .build();
        }
    
    }

    - accessToken, refreshToken을 생성해주는 클래스이다.

    - @Value("${jwt.secret}") : application.properties 파일에서 jwt.secret 값을 찾아서 secretKey 변수에넣어준다.

    - service에서 가져온 authentication에서 권한을 가져온 후 claim으로 넣어준다. 이 정보를 통해 spring security가 회원의 접근 권한을 확인한다.

    * accessToken은 탈취 시 토큰의 유효시간이 끝날때까지 자유롭게 인증이 가능하므로 accessToken의 유효시간은 짧게, refreshToken의 유효시간은 그보다 길게 설정한다. refreshToken은 회원 정보를 담고 있지 않아 탈취 당해도 위험하지 않다.

     

    3. WebSecurityConfig

    @RequiredArgsConstructor
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final JwtTokenProvider jwtTokenProvider;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .httpBasic().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/login","/signup","/schedules").permitAll()
                    .antMatchers("/**").hasAnyRole("ADMIN","USER")
                    .and()
                    .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
    }
    • .httpBasic().disable() : rest api 이므로 basic auth 인증을 사용하지 않는다
    • .csrf().disable() : rest api 이므로 csrf 보안을 사용하지 않는다.
    • .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) : JWT를 사용하기 때문에 세션을 사용하지 않는다
    • .antMatchers().permitAll() : 모든 요청을 허가한다
    • .antMatchers().hasRole("USER") : USER 권한이 있어야 요청가능
    • .antMatchers().hasRole("ADMIN") : ADMIN 권한이 있어야 요청가능
    • .addFilterBefore(new JwtAUthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) : Jwt 인증을 위하여 직접 구현한 JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 실행하겠다는 설정

    4. JwtTokenProvider

    ...
    //토큰 정보 검증
        public boolean validateToken(String token) {
            try {
                Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
                return true;
            } catch (SecurityException| MalformedJwtException e) {
                log.info("Invalid JWT token", e);
            } catch (ExpiredJwtException e) {
                log.info("Expired jwt token", e);
            } catch (UnsupportedJwtException e) {
                log.info("Unsupported jwt token", e);
            } catch (IllegalArgumentException e) {
                log.info("JWT claims string is empty", e);
            }
            return false;
        }
       
       //jwt 토큰을 복호화해 그 안의 정보를 꺼내는 메소드
        public Authentication getAuthentication(String accessToken) {
            //토큰 복호화
            Claims claims = parseClaims(accessToken);
    
            if (claims.get(AUTHORITIES_KEY) == null) {
                throw new RuntimeException("권한 정보가 없는 토큰");
            }
            //클레임에서 권한 정보 가져오기
            Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
            //userDetails 객체 만들어 Authentication 리턴
            UserDetails principal = new User(claims.getSubject(), "",authorities);
            return new UsernamePasswordAuthenticationToken(principal, "", authorities);
        }
        
         private Claims parseClaims(String accessToken) {
            try {
                return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
            }catch (ExpiredJwtException e) {
                return e.getClaims();
            }
        }

    JwtTokenProvider 클래스에 @Slf4j 어노테이션을 쓰는데 이 어노테이션을 통해 log.info를 활용하여 간단하게 log를 찍을 수 있다.

     

    JwtAuthenticationFilter

    import lombok.RequiredArgsConstructor;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.GenericFilterBean;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    
    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends GenericFilterBean {
    
        private static final String AUTHORIZATION_HEADER = "Authorization";
        private static final String BEARER_TYPE = "Bearer";
    
        private final JwtTokenProvider jwtTokenProvider;
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String token = resolveToken((HttpServletRequest) request);
    
            if (token != null && jwtTokenProvider.validateToken(token)) {
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            chain.doFilter(request, response);
        }
    
        private String resolveToken(HttpServletRequest request) {
            String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_TYPE)) {
                return bearerToken.substring(7);  //"Bearer "을 잘라냄
            }
            return null;
        }
    }

    - JWT 토큰을 Header에 담아 API 요청이 왔을 때 해당 토큰을 검사하고, 토큰에서 인증 정보를 가져오기 위해 생성하는 필터

    - resolveToken을 통해 authorization 이라는 이름의 header를 가져온다. `Bearer ${accessToken}` 형태로 들어오므로 "Bearer "을 잘라준다.

    - jwtTokenProvider의 validateToken 메소드를 통해 토큰을 검증한 후 getAuthentication 메소드를 통해 그 안의 권한 정보를 가져온다.

    이후 chain.doFilter(request, response); 을 통해 chain이 동작 완료되고 api 요청에 대한 응답을 한다.


    참고

    spring security + JWT 로그인 기능 파헤치기 - 1 (tistory.com)

     

    spring security + JWT 로그인 기능 파헤치기 - 1

    로그인 기능은 거의 대부분의 애플리케이션에서 기본적으로 사용됩니다. 추가로 요즘은 웹이 아닌 모바일에서도 사용 가능하다는 장점과 Stateless 한 서버 구현을 위해 JWT를 사용하는 경우를 많

    wildeveloperetrain.tistory.com

    SPRING SECURITY + JWT 회원가입, 로그인 기능 구현 (tistory.com)

     

     

     

     

     

Designed by Tistory.