spring security+jwt 회원가입, 로그인 #3
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)