-
spring security+jwt 회원가입, 로그인 #3SOLUX-완숙이 2022. 1. 17. 15:07
2022.01.12 - [SOLUX-완숙이] - spring security+jwt 회원가입, 로그인 #2
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 회원가입, 로그인 기능 구현 (tistory.com)
'SOLUX-완숙이' 카테고리의 다른 글
spring boot 파일, 이미지 업로드(multipartfile) (0) 2022.01.25 spring security+jwt 회원가입, 로그인 #4 (0) 2022.01.23 JWT(Json Web Token) (0) 2022.01.17 jwt signWith deprecated 오류 (0) 2022.01.13 spring security+jwt 회원가입, 로그인 #2 (0) 2022.01.12