일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Docker
- JUnit5
- 우아한테크코스
- 우아한세미나
- 코드리뷰
- AOP
- Level2
- yml
- mock
- 세션
- JPA
- 스프링 부트
- 우테코
- Spring Batch
- MSA
- 서블릿
- HTTP
- 스프링부트
- 프리코스
- CircuitBreaker
- 프로그래머스
- 의존성
- 자바
- 트랜잭션
- AWS
- REDIS
- 미션
- 백준
- Paging
- 레벨2
- Today
- Total
늘
[Spring Security] Jwt Token & Session + refreshToken 본문
Jwt와 Session방식의 차이에 대해서 공부했다. 우선 우리가 사용하는 스프링 부트는 Session기반으로 작동되어 있다고 한다. 그래서 평소 자연스럽게 이용했던 세션을 통한 인증 관리가 아닌 이번에는 JWT를 이용하여 회원 인증과 권한을 관리해보려고 한다..!
그전에 잠깐 Session&Cookie 기반 인증방식과 Token(jwt) 기반 인증 방식의 차이를 간단하게 설명해 보겠다.
- Session & Cookie
- 유저가 로그인하고 세션이 서버 메모리 상에 저장된다. (서버 단에서 메모리 차지가 클 수 있다.)
- session Id를 기준으로 정보를 전달한다.
- 브라우저에 쿠키로 session Id를 저장 -> 쿠키로 보내진다.
- JWT(Json Web Token) 방식
- Header, Payload, Signature로 구성된다.
- Header는 Signature를 해싱하기 위한 alg 정보와 type이 담겨있음
- Payload는 실제 사용될 정보에 대한 내용이 담겨있음
- registered claim, public claim, private claim으로 나뉨
- Signature는 토큰의 유효성 검증을 위한 문자열이 담겨있음
- ex) HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 형태로 담김
- 유저 로그인 -> Token 발급 -> 클라이언트는 발급된 Token 저장
- 클라이언트는 요청 시 저장된 Token을 header에 포함시켜 전송 -> 서버 확인
- 인증서버가 따로 없으므로 시스템 수평 확장에 유리
- payload의 정보가 커지면 트래픽에 크기 증가, 데이터 설계 고려할 필요가 있다.
- Header, Payload, Signature로 구성된다.
JWT
- 알고리즘 방식은 대칭키인 HMAC 알고리즘을 사용한 방법과 공개키/개인키 암호화인 RSA or ECDSA 알고리즘 2가지로 나뉜다.
- 무결성과 인가를 위해서 사용한다.
- base64UrlEncode(header)+"."+ base64UrlEncode(payload)+"."+signature 형태
우선 jwt를 쓰기 위해선 build.gradle에 라이브러리를 추가해야 한다.
implementation 'io.jsonwebtoken:jjwt:0.9.1'
JWT를 쓰려면 Spring Security에서 기본적으로 지원하는 Session 설정을 해제해야 한다. 또한 API 서버로 사용할 거기 때문에 CSRF 보안도 필요 없어서 해제한다.
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.httpBasic().disable()//기본 로그인 페이지 사용x
.csrf().disable() //REST API 사용하기 때문에
.authorizeRequests().antMatchers("/authenticate", "/api/member","/members/new").permitAll()
.antMatchers("/uesr/**").hasRole("USER") //USER, ADMIN 접근 가능
.antMatchers("/admin/**").hasRole("ADMIN") //ADMIN만 접근 가능
.anyRequest().authenticated() //나머지 요청들은 권한이 있어야만 접근 가능
.and()
.formLogin()
.loginPage("/members/login") //로그인 페이지
.defaultSuccessUrl("/board/list") //로그인 성공 후
.and()
.logout()
.logoutSuccessUrl("/members/login")//로그아웃 성공
.and()
.exceptionHandling()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //jwt사용을 위해 session해제
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
package board.configuration;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenProvider {
@Value 어노테이션을 이용하여 .yml파일에 key를 저장해두는게 더 좋다.
private static final String secret = "secretKey!!!";
// 토큰 유효 기간
public static final long JWT_TOKEN_VALIDITY = 60 * 60 * 24 * 1000L; //하루
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getId);
}
// 토큰에서 회원 정보 추출
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token).getBody();// JWT payload 에 저장되는 정보단위
return claimsResolver.apply(claims);
}
private Boolean isTokenExpired(String token) {
final Date expiration = getClaimFromToken(token, Claims::getExpiration);
return expiration.before(new Date());
}
//JWT 토큰 생성1
public String generateToken(String id) {
return doGenerateToken(id, new HashMap<>());
}
//JWT 토큰 생성2
private String doGenerateToken(String id, Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setId(id)
.setIssuedAt(new Date(System.currentTimeMillis())) // 토큰 발행 시간 정보
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY)) // set Expire Time
.signWith(SignatureAlgorithm.HS512, secret)// 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
}
여기서 key값을 yml에 저장하고 @Value로 키를 가져올 땐, static을 넣으면 인식을 안 한다...!(덕분에 삽질 엄~청 했네요 ㅎㅎ)
package board.Service;
import board.Domain.Entity.UserEntity;
import board.Domain.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
@Service
@Transactional
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
}
}
코드들 마다 설명을 위해 주석을 달아두었다. jwt관련 다른 블로그들도 정리를 잘해두어서 참고하면 좋을 것 같다.
내 코드만의 특징이 있었는데, JwtDetailsService를 구현할때 @Lazy를 추가하여 생성자 주입을 이용했다. 그 이유는 빈의 순환 참조가 있어서였는데 이 부분에 대해서는 더욱 공부해봐야 할 것 같다..!
CustomeUserDetailService에서 이메일을 통해서 유저를 찾고 없으면 사용자를 찾을 수 없다고 알린다.
userDetail을 implements 할 때, 생성되는 함수들 또한 알아보자!
RefreshToken
https://github.com/Gyuchool/Spring-Jwt
Reference
'백앤드 개발일지 > 스프링부트' 카테고리의 다른 글
[JPA] 양방향 매핑 OneToOne Lazy 이슈 (0) | 2021.07.19 |
---|---|
스프링 프레임워크 (0) | 2021.07.02 |
JPA, Hibernate, Spring Data JPA (0) | 2021.06.27 |
스프링 시큐리티 + _csrf설정 (0) | 2021.03.28 |
상속 논리 모델 -> DB 물리 모델링 (0) | 2021.03.10 |