<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.1</version>
</dependency>
server:
port: 8080
jwt:
header: token
issuer: prgrms
client-secret: EENY5W0eegTf1naQB2eDeyCLl5kRS2b8xa5c4qLdS0hmVjtbvo8tOyhPMcAmtPuQ
expiry-seconds: 60
header : jwt token 이 입력되서 들어오는 header 의미
issuer : 발행자 정보
client-secret : 토큰의 위변조방지에 사용되는 64byte secret 키
expiry-seconds : 토큰 발행 후 1분 후 만료됨을 의미
사용하지 않는 필터 disable 및 session 사용안함 표시
@Configuration
@EnableWebSecurity
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/user/me").hasAnyRole("USER", "ADMIN")
.anyRequest().permitAll()
.and()
/**
* formLogin, csrf, headers, http-basic, rememberMe, logout filter 비활성화
*/
.formLogin() // 현재 필요 없다
.disable()
.csrf() // 현재 페이지 기반 서비스가 아니므로 사용X
.disable()
.headers() // 필요 없다
.disable()
.httpBasic()
.disable()
.rememberMe()
.disable()
.logout() // 현재 사용 안함
.disable()
/**
* Session 사용하지 않음
*/
.sessionManagement() // session 을 사용하지 않음을 명시
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
/**
* 예외처리 핸들러
*/
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
/**
* JwtSecurityContextRepository 설정
*/
.securityContext()
.securityContextRepository(securityContextRepository())
.and()
/**
* jwtAuthenticationFilter 추가
*/
//.addFilterAfter(jwtAuthenticationFilter(), SecurityContextPersistenceFilter.class)
;
}
}
package com.prgrms.devcourse.configures;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component // 자동 스캔되게 설정
@ConfigurationProperties(prefix = "jwt") // jwt prefix 로 yaml 파일에서 바인딩
public class JwtConfigure {
private String header;
private String issuer;
private String clientSecret;
private int expirySeconds;
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public String getIssuer() {
return issuer;
}
public void setIssuer(String issuer) {
this.issuer = issuer;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public int getExpirySeconds() {
return expirySeconds;
}
public void setExpirySeconds(int expirySeconds) {
this.expirySeconds = expirySeconds;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("header", header)
.append("issuer", issuer)
.append("clientSecret", clientSecret)
.append("expirySeconds", expirySeconds)
.toString();
}
}
package com.prgrms.devcourse.jwt;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public final class Jwt {
private final String issuer; // 앞에 3개의 필드 가져온다.
private final String clientSecret; // 앞에 3개의 필드 가져온다.
private final int expirySeconds; // 앞에 3개의 필드 가져온다.
private final Algorithm algorithm; // 위변조 체크에 사용되는 알고리즘
private final JWTVerifier jwtVerifier; // jwt 검증을 위한 필드
public Jwt(String issuer, String clientSecret, int expirySeconds) {
this.issuer = issuer;
this.clientSecret = clientSecret;
this.expirySeconds = expirySeconds;
this.algorithm = Algorithm.HMAC512(clientSecret); // 64 byte 클라이언트 시크릿이 필요하다.
this.jwtVerifier = com.auth0.jwt.JWT.require(algorithm) // Jwt 클래스와 maven 에 추가한 JWT 를 혼동하지 않기 위해 풀 패키지 작성
.withIssuer(issuer)
.build();
}
// 2. 토큰을 만드는 메서드(필요한 정보-Claims 를 받는다)
public String sign(Claims claims) {
Date now = new Date(); // Date을 쓰는 이유는 JWT 에서 날짜를 DateTime 으로 받기 때문이다. 보통 LocalDateTime 을 쓰는게 좋지만 쓰지않는 이유이다
JWTCreator.Builder builder = com.auth0.jwt.JWT.create();
builder.withIssuer(issuer);
builder.withIssuedAt(now);
if (expirySeconds > 0) {
builder.withExpiresAt(new Date(now.getTime() + expirySeconds * 1_000L));
// 현재 시간 + 주어진 expire 시간지나면 토큰 만료
}
builder.withClaim("username", claims.username);
builder.withArrayClaim("roles", claims.roles);
return builder.sign(algorithm); // 서명데이터까지 주어진 알고리즘으로 생성하여 최종적으로 토큰 생성
}
// 3. 토큰이 주어졌을때 토큰을 decode 해서 Claims 로 리턴하는 메서드
public Claims verify(String token) throws JWTVerificationException {
return new Claims(jwtVerifier.verify(token)); // verigy 에서 위변조 검사 후 토큰 만료시간 발행주체 id 등을 검증한 후 decodedJWT 반환
}
public String getIssuer() {
return issuer;
}
public String getClientSecret() {
return clientSecret;
}
public int getExpirySeconds() {
return expirySeconds;
}
public Algorithm getAlgorithm() {
return algorithm;
}
public JWTVerifier getJwtVerifier() {
return jwtVerifier;
}
// 1. jwt 토큰을 만들거나 검증할때 필요한 데이터를 전달하기 위해 필요한 클래스
static public class Claims {
String username; // 유저이름
String[] roles; // 퀀한 목록
Date iat; // 토큰의 발행 일자
Date exp; // 토큰의 만료 일자
private Claims() {/*no-op*/}
// DecodedJWT 를 통해 Claims 객체 초기화
Claims(DecodedJWT decodedJWT) {
Claim username = decodedJWT.getClaim("username"); // 유저이름 가져오기
if (!username.isNull())
this.username = username.asString();
Claim roles = decodedJWT.getClaim("roles"); // roles 가져오기
if (!roles.isNull()) {
this.roles = roles.asArray(String.class);
}
this.iat = decodedJWT.getIssuedAt();
this.exp = decodedJWT.getExpiresAt();
}
// username 과 roles 를 이용한 팩터리 메서드
public static Claims from(String username, String[] roles) {
Claims claims = new Claims();
claims.username = username;
claims.roles = roles;
return claims;
}
// map 을 리턴하는 메서드
public Map<String, Object> asMap() {
Map<String, Object> map = new HashMap<>();
map.put("username", username);
map.put("roles", roles);
map.put("iat", iat()); // Date 보다는 long 타입으로 변환
map.put("exp", exp()); // Date 보다는 long 타입으로 변환
return map;
}
long iat() {
return iat != null ? iat.getTime() : -1;
}
long exp() {
return exp != null ? exp.getTime() : -1;
}
void eraseIat() {
iat = null;
}
void eraseExp() {
exp = null;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("username", username)
.append("roles", Arrays.toString(roles))
.append("iat", iat)
.append("exp", exp)
.toString();
}
}
}
@Configuration
@EnableWebSecurity
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter{
private final JwtConfigure jwtConfigure;
public WebSecurityConfigure(JwtConfigure jwtConfigure) {
this.jwtConfigure = jwtConfigure;
}
@Bean
public Jwt jwt() {
return new Jwt( // jwtConfigure를 통해 JWT 객체를 만들어 빈으로 등록
jwtConfigure.getIssuer(),
jwtConfigure.getClientSecret(),
jwtConfigure.getExpirySeconds()
);
}
package com.prgrms.devcourse.user;
import com.prgrms.devcourse.jwt.Jwt;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class UserRestController {
private final Jwt jwt;
private final UserService userService;
public UserRestController(Jwt jwt, UserService userService) {
this.jwt = jwt;
this.userService = userService;
}
/**
* 보호받는 엔드포인트 - ROLE_USER 또는 ROLE_ADMIN 권한 필요함
* @return 사용자명
*/
@GetMapping(path = "/user/me")
public String me() {
return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
/**
* 주어진 사용자의 JWT 토큰을 출력함
* @param username 사용자명
* @return JWT 토큰
*/
@GetMapping(path = "/user/{username}/token")
public String getToken(@PathVariable String username) {
UserDetails userDetails = userService.loadUserByUsername(username);
String[] roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toArray(String[]::new);
return jwt.sign(Jwt.Claims.from(userDetails.getUsername(), roles));
}
/**
* 주어진 JWT 토큰 디코딩 결과를 출력함
* @param token JWT 토큰
* @return JWT 디코드 결과
*/
@GetMapping(path = "/user/token/verify")
public Map<String, Object> verify(@RequestHeader("token") String token) {
return jwt.verify(token).asMap();
}
}
jwt 인코딩 디코딩 사이트 : https://jwt.io/