SpringBoot Sequrity + JWT 구현
취업 전부터 실무까지 요즘 개발자라면 모두 사용하는 JWT는 구조 자체를 몰랐을 때 나에게 매우 어려운 과제였습니다.
인터넷에 어떤 블로그를 봐도 끝까지 완성 시킬 수 없었고, 회사 내에서 JWT를 자주 사용하다 보니 익숙해진 구조를 이제는 글로 작성해 보려고 합니다. 나의 깃허브에 들어가면 Database 정보만 제대로 입력하면 실행할 수 있도록 구성해 왔으니 참고 부탁드립니다!
JWT 란?
JWT(JSON Web Token)은 JSON 객체 인증에 필요한 정보들을 담은 키값들을 서명한 토큰입니다.
JWT로 인증 (Authentication)과 권한(Authorization)의 방식을 구현할 수 있습니다.
Security와 JWT간의 필요한 토큰 생성 구조
코드를 작성하기 전 이러한 과정이필요하다는 걸 알고 진행하면 부족한 코드일지라도 흐름을 알기 때문에 더 쉽게 진행하실 수 있습니다.
- Gradle에 의존성 추가
- UserDetails interface 를 implements(상속)한 CustomUserDetails 생성
- UserDetailService의 loadByUsername 구현
- JWT 토큰 발급 클래스를 생성하고 Bean으로 등록 (@Component)
- JWT관련 Filter 생성
- JWT관련 Filter의 예외처리 클래스 생성
- SecurityConfig생성
- 회원가입 API 생성
- 로그인 API 생성
- refreshToken 재발급 API 생성
- 내 정보 API 생성
Gradle에 의존성 추가
다음과 같이 Gradle에 의존성을 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-security'
//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5
'implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
UserDetails 생성
저는 유저의 PK를 사용하여 로직을 진행할것이기 때문에 id를 추가했습니다.
GrantedAuthority는 유저의 권한을 지정합니다.
기본 유저는 ROLE_USER를 사용할것이므로 SimpleGrantedAuthority 객체에 권한을 넣어 생성해줍니다.
나머지 Override 부분들은 원하시는대로 사용하셔도 됩니다.
loadByUsername에 사용할 getUsername에만 return 값을 id로 지정해주었습니다.
@Data
@Builder
public class MemberDetails implements UserDetails {
private Long id;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
HashSet<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return id.toString();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailService 생성
저는 JPA를 사용하여 유저에 대한 값을 PK를 불러와 인증 객체인 MemberDetails에 id를 넣어 값을 전달했습니다.
추가되는 값이 있으면 MemberDetails를 수정하여 더 많은 값을 넣을 수 있습니다.
이 과정을 거치면 Security의 인증 객체인 UserDetails의 기본 설정이 완료되었습니다.
@Service
@RequiredArgsConstructor
public class MemberDetailService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
try {
Member member = memberRepository.findById(Long.parseLong(id)).orElseThrow(() -> new UsernameNotFoundException(id));
return MemberDetails.builder()
.id(member.getId())
.build();
}catch (NullPointerException e){
throw new UsernameNotFoundException(null);
}
}
JWT 토큰 관련 클래스 생성
먼저 JWT관련 YML을 구성하여 각 토큰의 만료 시간을 구성해줍니다.
해당 클래스의 생성자를 생성하여 필요한 값들을 넣어줍니다.
내가 발급한 키 값으로 accessToken과 refreshToken에 대한 만료 시간을 각각 넣어주는 토큰 발급 메서드를 생성합니다.
JWT 구조에만 맞게 구성하면 되니 꼭 아래 코드를 따라하실 필요는 없습니다.
값을 추가하거나 Claims를 넣고 싶으면 생성하셔서 넣으셔도 됩니다.
getAuthentication 에는 내가 발급한 토큰으로 유저의 인증 객체를 생성합니다.
@Compoent 어노테이션으로 생성한 클래스를 빈으로 등록해줍니다.
@Component
public class JwtTokenProvider {
private Key key;
@Value("${jwt.access.expire-second}")
private Long accessTokenExpireSecond;
@Value("${jwt.refresh.expire-second}")
private Long refreshTokenExpireSecond;
private final MemberDetailService memberDetailService;
public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey,
MemberDetailService memberDetailService) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
this.memberDetailService = memberDetailService;
}
public String createJwt(Long memberId) {
Instant issuedDt = Instant.now().truncatedTo(ChronoUnit.SECONDS);
Instant expireDt = issuedDt.plus(accessTokenExpireSecond, ChronoUnit.SECONDS);
return Jwts.builder()
.setSubject(memberId.toString())
.setIssuedAt(Date.from(issuedDt))
.setExpiration(Date.from(expireDt))
.signWith(key)
.compact();
}
public String createRefreshJwt(String token) {
Claims claims = getClaims(token);
Instant issuedDt = Instant.now().truncatedTo(ChronoUnit.SECONDS);
Instant expireDt = issuedDt.plus(refreshTokenExpireSecond, ChronoUnit.SECONDS);
return Jwts.builder()
.setSubject(claims.getSubject())
.setIssuedAt(Date.from(issuedDt))
.setExpiration(Date.from(expireDt))
.signWith(key)
.compact();
}
public boolean isExpiredToken(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
Date expireDt = claims.getBody().getExpiration();
Date nowDt = new Date();
return expireDt.before(nowDt);
} catch (Exception e) {
return true;
}
}
public boolean validateToken(String token) throws BadRequestException {
try {
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
Date expireDt = claims.getBody().getExpiration();
Date nowDt = new Date();
if (expireDt.after(nowDt)) {
return true;
}
return false;
} catch (Exception e) {
throw new BadRequestException("토큰이 유효하지 않습니다.");
}
}
public Authentication getAuthentication(String jwt) throws BadRequestException {
String memberId = getClaims(jwt).getSubject();
if (key == null) {
throw new BadRequestException("등록되지 않은 토큰입니다.");
}
HashSet<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(memberDetailService.loadUserByUsername(memberId), jwt, authorities);
}
public Claims getClaims(String token) {
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody();
}
여기까지 완료했다면 Spring Security + JWT의 구성 세팅은 완료가 되었습니다.
이어서 다음 글에는 Swagger를 구성하고, 각 API를 만들어 JWT가 제대로 작동해 보는지 테스트해보는 시간을 갖도록 하겠습니다.
'Spring' 카테고리의 다른 글
[JPA] JPA 개념과 기본 설정 가이드 (0) | 2024.01.29 |
---|---|
[Spring] SpringBoot JWT 로그인 구현 가이드 (1) | 2024.01.26 |
[Spring] SpringBoot Swagger 연동 (3.x ver) (0) | 2024.01.10 |
[Spring] Spring Security는 무엇일까? (0) | 2024.01.04 |
[Spring] 스프링 부트 멀티 모듈 프로젝트 구성하기 (gradle, yml 설정, 3.x ver) (0) | 2024.01.02 |