경희대학교 컴퓨터공학부 하계 리턴 백엔드(스프링부트) 스터디 8주차 - 트랙장 최현영
8주차를 끝으로 리턴 하계 스프링 부트 트랙을 마치도록 하겠다.
@Override
public voiddoFilter(ServletRequest request, ServletResponse response, FilterChain chain)throwsIOException, ServletException {
log.info("현재, Custom Filter 통과중"); //선처리
**chain.doFilter(request, response); //추가
//후처리(즉, 비지니스 후 돌아왔을 때 처리됨)**
}
1부의 시큐리티 과정 복습
의존성 추가
//build.gradle
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
...
}
사용자의 정보를 담는 UserDetails 인터페이스 구현
1부에서 UserDetailsService()의 loadUserByUsername 메서드를 통해 사용자의 정보가 담긴 UserDetails 객체를 반환한다고 하였다. 우리는 이를 재정의 하여 사용할 것이다.
또한, Authentication에 포함될 권한을 부여해야하기에, Role 열거형을 생성하여 USER와 ADMIN으로 권한을 분리할 것이다.
package com.rebe.returnstudy.Entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
@ToString
@Getter
@AllArgsConstructor
public enum Role {
ROLE_USER("ROLE_USER"), ROLE_ADMIN("ROLE_ADMIN");
private final String role;
}
여기서 중요한 점이 ROLE_이라는 접두사를 붙여주어야 인식한다.
UserDetails를 상속받는 Member 엔티티 구현
또한, 회원의 비밀번호 역시 저장해야하기에 password 애트리뷰트를 추가해준다.
package com.rebe.returnstudy.Entity;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Member implements UserDetails {
@Id
@GeneratedValue
private Long id;
....
**@Column(nullable = false)
private String password;
@Column(nullable = false)
private String role;**
....
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//계정이 가지고 있는 권한 목록을 반환
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(this.getRole()));
return authorities;
}
@Override
public String getPassword() {
//계정의 비밀번호 반환
return this.password;
}
@Override
public String getUsername() {
//계정의 아이디를 반환
return this.studentId;
}
@Override
public boolean isAccountNonExpired() {
//계정이 만료되었는지 리턴
//false가 기본값인데, true는 만료가 되지 않음을 의미
return true;
}
@Override
public boolean isAccountNonLocked() {
//계정이 잠겼는지 리턴
//false가 기본값인데, true는 안잠겼다는 것을 의미
return true;
}
@Override
public boolean isCredentialsNonExpired() {
//비밀번호가 만료되었는지 리턴
//false가 기본값인데, true는 만료가 안되었다는 것을 의미
return true;
}
@Override
public boolean isEnabled() {
//계정이 현재 활성화 상태인지를 리턴
//false가 기본값인데, true는 현재 활성화 된 계정을 의미
return true;
}
}
UserDetailsService 구현
@Service //서비스 레이어의 클래스에 사용하는 컴포넌트 시리즈 어노테이션
@RequiredArgsConstructor
@Slf4j
public class MemberService implements UserDetailsService {
...
@Override
public UserDetails loadUserByUsername(String username) {
log.info("사용자 아이디 : " + username);
return memberRepository.findByStudentId(username)
.orElseThrow(() -> new UsernameNotFoundException("유저를 찾을 수 없음"));
}
}
JwtService 구현
Jwt를 발급하는 서비스 클래스를 구현해보자
우선, application.yml에서 고유한 시크릿 키를 생성하자.
//application.yml
jwt:
secret: "Return"
c. 다음으로 JwtService 클래스를 생성하자
package com.rebe.returnstudy.Service;
import com.rebe.returnstudy.Entity.Role;
import io.jsonwebtoken.*;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
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.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.Optional;
@Service
@Slf4j
@RequiredArgsConstructor
public class JwtService {
//1
@Value("${jwt.secret}")
private String secret_key;
private final long ACCESS_TOKEN_VALIDITY_TIME = 604800 * 1000L; //JWT 유효기간 1주
private final String HEADER_KEY = "Authorization";
//2
@PostConstruct
protected void init() {
log.info("secret_key Base64 인코딩시작");
log.info("Original Secret_Key : " + secret_key);
this.secret_key = Base64.getEncoder()
.encodeToString(secret_key.getBytes(StandardCharsets.UTF_8));
log.info("Encoded Base64 Secret_Key : " + secret_key);
log.info("secretKey 초기화 완료");
}
//3
public Optional<String> extractAccessToken(HttpServletRequest request) throws IOException, ServletException {
return Optional.ofNullable(request.getHeader(HEADER_KEY));
}
//4
public String extractStudentId(String jwt) {
return Jwts.parser().setSigningKey(secret_key).parseClaimsJws(jwt).getBody().getSubject();
}
//5
public Long getExpireTime(String token) {
Date expirationDate = Jwts.parser().setSigningKey(secret_key)
.parseClaimsJws(token).getBody().getExpiration();
long now = new Date().getTime();
return ((expirationDate.getTime() - now) % 1000) + 1;
}
//5
public String generateToken(String studentId, String role) {
Claims claims = Jwts.claims().setSubject(studentId);
claims.put("role", role);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + ACCESS_TOKEN_VALIDITY_TIME))
.signWith(SignatureAlgorithm.HS256, secret_key)
.compact(); //토큰생성
}
//6
public Authentication getAuthentication(UserDetails userDetails) {
log.info("토큰 인증 정보 조회 시작");
log.info("UserDetails UserName : {}",
userDetails.getUsername());
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(),
userDetails.getAuthorities());
}
//7
public boolean validateToken(String token) { //토큰 발리데이션
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token);
log.info("토큰 유효 체크 완료");
return !claims.getBody().getExpiration().before(new Date());
} catch (SignatureException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.");
} catch (Exception e) {
log.info("JWT 토큰이 이상합니다.");
}
return false;
}
}
1번
2번
3번
4번
5번
6번
인증이 성공하였다면, 인증 객체를 생성하는 메서드이다.
7주차에서 살펴본 Authentication 내부에 인증 주체[ID], 비밀번호, 권한이 들어간다고 하였다.
7번