스프링 시큐리티 및 JWT를 사용하기 위한 의존성을 pom.xml에 추가시켜준다.
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
UserDetails를 상속하는 엔티티 클래스인 User 엔티티 클래스 생성
getAuthorities()
계정이 가진 권한 목록을 리턴
getPassword()
계정의 비밀번호를 리턴
getUsername()
계정의 이름(아이디)을 리턴
isAccountNonExpired()
계정이 만료되지 않았는 지 리턴
isAccountNonLocked()
계정이 잠기지 않았는 지 리턴
isCredentialNonExpired()
비밀번호가 만료되지 않았는 지 리턴
isEnabled()
계정이 활성화됐는 지 리턴
public class User implements UserDetails {
@Column(nullable = false, unique = true)
private String uid; // 회원 ID (JWT 토큰 내 정보)
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // Json 결과로 출력하지 않을 데이터에 대해 해당 어노테이션 설정 값 추가
@Column(nullable = false)
private String password;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
//security 에서 사용하는 회원 구분 id
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public String getUsername() {
return this.uid;
}
/**
* 계정이 만료되었는지 체크
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 계정이 잠겼는지 체크
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 계정의 패스워드가 만료되었는지 체크
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 계정이 사용가능한지 체크
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isEnabled() {
return true;
}
}
UserRepository에서는 ID 값을 토큰 생성 정보로 사용하기 위해 User를 받아오는 메서드를 생성해준다.
public interface UserRepository extends JpaRepository<User,Long> {
User getByUid(String uid);
}
Spring Security의 UserDetailsService 인터페이스를 상속받아 구현한다.
username을 갖고 UserDetails 객체를 반환하는 메소드를 갖고 있다.
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final Logger LOGGER = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username){
LOGGER.info("[loadUserByUsername] loadUserByUsername 수행, username : {}", username);
return userRepository.getByUid(username);
}
}
JWT 토큰을 생성하는 데 필요한 정보를 UserDetails에서 가져와서 해당 정보를 갖고 토큰을 생성할 JwtTokenProvider를 생성한다.
@Value("${springboot.jwt.secret}")
private String secretKey = "secretKey";
private final long tokenValidMillisecond = 1000L * 60 * 60;
application.properties에서 설정한 값을 가져오거나 가져오지 못했다면 기본 값인 secretKey
를 가져오게 된다.
init() 메소드에서는 secretKey를 Base64 형식으로 인코딩한다.
@PostConstruct
protected void init(){
LOGGER.info("[init] JwtTokenProvider 내 secretKey 초기화 시작");
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
LOGGER.info("[init] JwtTokenProvider 내 secretKey 초기화 완료");
}
public String createToken(String userUid, List<String> roles){
LOGGER.info("[createToken] 토큰 생성 시작");
Claims claims = Jwts.claims().setSubject(userUid);
claims.put("roles", roles);
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime()+ tokenValidMillisecond))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
LOGGER.info("[createToken] 토큰 생성 완료");
return token;
}
JWT 토큰의 내용 부분에 값을 넣기 위해 Claims 객체를 생성
생성한 Claims 객체에 sub 속성에 값을 추가하기 위해 userUid 값을 넣은 setSubject 메소드를 사용
Jwts.builder()를 사용해 token을 생성한다.
필터에서 인증이 성공했을 때 SecurityContextHodler에 저장할 Authentication을 생성하는 역할
Authentication : Spring Security에서 한 유저의 인증 정보를 가지고 있는 객체
public Authentication getAuthentication(String token){
LOGGER.info("[getAuthentication] 토큰 인증 정보 조회 시작");
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUsername(token));
LOGGER.info("[getAuthentication] 토큰 인증 정보 조회 완료, UserDetails UserName : {}", userDetails.getUsername());
return new UsernamePasswordAuthenticationToken(userDetails, "",userDetails.getAuthorities());
}
초기화를 위해 UserDetails 객체를 가져온 후 UsernamePasswordAuthenticationToken 객체를 생성한다. 이를 위해 사용되는 Username값은 또 메서드를 만들어준다.
Jwts.parser()를 통해 secretKey를 설정하고 클레임을 추출해서 토큰을 생성할 때 넣었던 sub 값을 추출한다.
private String getUsername(String token) {
LOGGER.info("[getUsername] 토큰 기반 회원 구별 정보 추출");
String info = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
LOGGER.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info : {}", info);
return info;
}
HttpServletRequest를 파라미터로 받아 헤더 값으로 전달된 X-AUTH-TOKEN 값을 가져와 리턴하는 메소드
클라이언트가 헤더를 통해 애플리케이션 서버로 JWT 토큰 값을 전달해야한다.
public String resolveToken(HttpServletRequest request){
LOGGER.info("[resolveToken] HTTP 헤더에서 Token 값 추출");
return request.getHeader("X_AUTH_TOKEN");
}
해당 토큰의 유효기간을 검사하는 메소드
// 토큰 유효기간 검사
public boolean validateToken(String token){
LOGGER.info("[validateToken] 토큰 유효 체크 시작");
try{
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e){
LOGGER.info("[validateToken] 토큰 유효 체크 예외 발생");
return false;
}
}