티스토리 뷰
Spring Security의 기본 동작은 표준 웹 애플리케이션을 사용하기에 쉽다. 쿠기 기반 인증 및 세션을 사용한다. 또한 CSRF 토큰을 자동으로 처리한다.
반면에 외부 서비스 또는 SPA/모바일 응용 프로그램과 함께 사용되는 REST API만 빌드하는 경우에는 전체 세션이 필요하지 않을 수 있다. 여기에 디지털 서명 토큰인 JWT(JSON Web Token)가 있다. 필요한 모든 정보를 토큰에 저장할 수 있으므로 서버가 세션이 없을 수 있다. 서버가 사용자에게 권한을 부여할 수 있도록 모든 HTTP 요청에 JWT를 첨부해야 한다. 토큰을 보내는 방법에 대한 몇 가지 옵션이 있다. 예를 들어 URL 매개 변수로 또는 Bearer 스키마를 사용하는 HTTP 인증 헤더를 사용한다.
Authorization: Bearer <token string>
JWT는 마침표 (.)를 구분자로 세 부분으로 나누어져 있으며 JOSE 헤더, JWT Claim Set, Signature라고 한다.
Header - 일반적으로 토큰 유형 및 해싱 알고리즘을 포함한다.
Payload - 일반적으로 사용자 및 토큰 발급 대상에 대한 데이터를 포함한다.
Signature - 메시지가 변경되지 않았는지 확인하는 데 사용된다.
토큰 (Base64로 인코딩된 JSON 객체)
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA
public final class SecurityConstants {
// JWT token defaults
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String TOKEN_TYPE = "JWT";
private SecurityConstants() {
throw new IllegalStateException("Cannot create instance of static util class");
}
}
@Service
public class JwtService implements IJwtService {
private static final Logger logger = LoggerFactory.getLogger(JwtService.class);
@Autowired
HttpServletRequest request;
@Value("${jwt.salt}")
private String salt;
@Override
public <T> String createJWT(String key, T data, String subject) throws UnsupportedEncodingException {
String jwt = Jwts.builder()
.setHeaderParam("typ", SecurityConstants.TOKEN_TYPE)
.setHeaderParam("regDt", System.currentTimeMillis())
.setSubject(subject)
.claim(key, data)
.setExpiration(new Date(System.currentTimeMillis() + 864000000))
.signWith(SignatureAlgorithm.HS512, generateKey())
.compact();
return jwt;
}
@SuppressWarnings("unused")
@Override
public boolean isUsable(String jwt) {
try {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(generateKey())
.parseClaimsJws(jwt);
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token: {}", e.getMessage());
return null;
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token: {}", e.getMessage());
return null;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
return null;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
return null;
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
return null;
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException: {}", e.getMessage());
return null;
}
return true;
}
@SuppressWarnings("unchecked")
@Override
public Map<String, Object> get(String key, String jwt) {
Jws<Claims> claims = null;
try {
claims = Jwts.parser()
.setSigningKey(salt.getBytes("UTF-8"))
.parseClaimsJws(jwt);
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token: {}", e.getMessage());
return null;
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token: {}", e.getMessage());
return null;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
return null;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
return null;
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
return null;
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException: {}", e.getMessage());
return null;
}
Map<String, Object> map = (Map<String, Object>) claims.getBody().get(key);
return map;
}
/**
* 암호화 키 생성
* @return
* @throws UnsupportedEncodingException
*/
private byte[] generateKey() throws UnsupportedEncodingException {
byte[] key = null;
try {
key = salt.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException: {}", e.getMessage());
throw new UnsupportedEncodingException("UnsupportedEncodingException: " + e.getMessage());
}
return key;
}
}
1) ExpiredJwtException : JWT를 생성할 때 지정한 유효기간 초과했을 경우
2) UnsupportedJwtException : 예상하는 형식과 일치하지 않는 특정 형식이나 구성의 JWT일 경우
3) MalformedJwtException : JWT가 올바르게 구성되지 않았을 경우
4) SignatureException : JWT의 기존 서명을 확인하지 못했을 경우
5) IllegalArgumentException : JWT claims이 비어있는 경우
// JWT 생성 key, 데이터, subject은 상황에 맞게 정의 하여 생성
String jwtToken = jwtService.createJWT("user", jwtDto, "userInfo");
// JWT 유효토큰 확인 (true, false 반환)
String jwtToken = request.getParameter(SecurityConstants.TOKEN_HEADER);
jwtService.isUsable(jwtToken);
// JWT 토큰정보 확인
String jwtToken = request.getParameter(SecurityConstants.TOKEN_HEADER);
Map<String, Object> jwtMap = jwtService.get("user", jwtToken);
JWT 주의점
JWT Claim Set은 암호화를 하지 않는다. 서명 없이도 누구나 열어볼 수 있기 때문에 여기에는 보안이 중요한 데이터를 넣으면 안 된다. base64로 인코딩해서 사용하다 보면 이 부분을 간과하기 쉬운데 필요한 최소한의 정보만 JWT Claim Set에 담아야 한다.
인코딩 특성상 JWT Claim Set의 내용이 많아지면 토큰의 길이도 길어진다. 불필요한 정보까지 많은 정보를 담으면 안 된다. 토큰을 강제로 만료시킬 방법이 없다. 서버가 토큰의 상태를 가지고 있지 않고 토큰 발급 시 해당 토큰이 유효한 조건이 결정되므로 클라이언트가 로그아웃해도 토큰 자체가 만료되는 것은 아니다. 이때 토큰을 탈취한다면 해당 토큰을 만료시간까지는 유효하게 된다
'프로그래밍 > Back end' 카테고리의 다른 글
[Back end] Spring Boot jar에서 war로 변경 (0) | 2020.12.30 |
---|---|
[Back end] Spring Boot Mybatis 설정 (0) | 2020.12.10 |
[Back end] Java Find Min and Max from List (0) | 2020.09.03 |
[Back end] Mybatis multiple selectKey (0) | 2020.09.03 |
[Back end] Lombok 개념 및 설치 (0) | 2020.06.30 |
- 경력관리
- 회고
- Maven
- sort algorithm
- 성능분석
- 오라클 내장 함수
- SQL
- React
- effective java
- 개발환경
- 자바
- 이직
- Collection
- javascript
- 오라클
- 제주도 3박4일 일정
- 프로그래머
- Java
- spring
- 제주도 여행
- 리액트 16
- Linux 명령어
- 리눅스 명령어
- 정렬 알고리즘
- Eclipse
- Tomcat
- 소프트웨어공학
- 프로그래머스
- 자바스크립트
- 리액트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |