// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
key = 256bit 난수;
//Header 부분 설정
Map<String, Object> headers = new HashMap<>();
// 토큰의 유형
headers.put("typ", "JWT");
// 서명 알고리즘
headers.put("alg", "HS256");
//payload 부분 설정
Map<String, Object> payloads = new HashMap<>();
// 저장할 데이터 설정(AES암호화)
payloads.put("userKey", aes256.encoding(userKey));
payloads.put("name", aes256.encoding(name));
Long expiredTime = 1000 * 60L * 3L; // 토큰 유효 시간 (3분)
Date ext = new Date(); // 토큰 만료 시간
// 현재 시간 + 유효 시간
ext.setTime(ext.getTime() + expiredTime);
// 토큰 Builder
String jwt = Jwts.builder()
.setHeader(headers) // Headers 설정
.setClaims(payloads) // Claims 설정
.setSubject("user") // 토큰 용도(제목)
.setExpiration(ext) // 토큰 만료 시간 설정
.signWith(SignatureAlgorithm.HS256, key.getBytes()) // HS256과 Key로 Sign
.compact(); // 토큰 생성
Claims claims = Jwts.parser()
.setSigningKey(key.getBytes("UTF-8")) // Set Key
.parseClaimsJws(jwt) // 파싱 및 검증, 실패 시 에러
.getBody();
String userKey = claims.get("userKey", String.class);
String name = claims.get("name", String.class);
String hp = claims.get("hp", String.class);
Spring Security 5.7.0 부터는 더이상 WebSecurityConfigurerAdapter를 확장해서 사용하지 않고 Bean을 주입하는 방식으로 사용
(role의 경우 별도로 필드를 추가하거나 테이블을 추가해도 되지만 테스트용이기 때문에 따로 만들지 않았음)
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
public final User getUser() {
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
// return user.getRoles().stream().map(o -> new SimpleGrantedAuthority(
// o.getName()
// )).collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPw();
}
@Override
public String getUsername() {
return user.getUserKey();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Service
@RequiredArgsConstructor
public class JpaUserDetailsService implements UserDetailsService {
private final UserRepository repository;
@Override
public UserDetails loadUserByUsername(String userKey) throws UsernameNotFoundException {
User user = repository.findByUserKey(userKey).orElseThrow(
() -> new UsernameNotFoundException("Invalid authentication!")
);
return new CustomUserDetails(user);
}
}
@RequiredArgsConstructor
@Component
public class JwtProvider {
private final ApplicationSetting setting;
private final AES256 aes256;
private final UserRepository repository;
private byte[] secretKey;
// 만료시간 : 1시간
private final long exp = 1000L * 60 * 60;
private final JpaUserDetailsService userDetailsService;
@PostConstruct
protected void init() {
secretKey = setting.getJwt_key().getBytes(StandardCharsets.UTF_8);
}
// 토큰 생성
public String createToken(String userKey) throws Exception {
User user = repository.findByUserKey(userKey).get();
Claims claims = Jwts.claims().setSubject(aes256.encoding(userKey));
claims.put("name", aes256.encoding(user.getName()));
claims.put("pw", user.getPw());
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + exp))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
// 권한정보 획득
// Spring Security 인증과정에서 권한확인을 위한 기능
public Authentication getAuthentication(String token) throws Exception {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getAccount(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에 담겨있는 UserKey(제목) 획득
// aes256 인코딩하여 삽입했기 때문에 디코딩 필수
public String getAccount(String token) throws Exception {
return aes256.decoding(
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject()
);
}
// 토큰에 담겨있는 데이터 획득
public UserResponse getBody(String token) throws Exception {
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey) // Set Key
.parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
.getBody();
String name = claims.get("name", String.class);
String hp = claims.get("hp", String.class);
return UserResponse.of(new HashMap<String, String>(){{
put("userKey", aes256.decoding(claims.getSubject()));
put("name", aes256.decoding(name));
put("hp", aes256.decoding(hp));
}});
} catch (SignatureException e){ // 기존 서명을 확인하지 못했을 경우
throw new SignatureException("ERROR_SIGN_JWT");
} catch (ExpiredJwtException e) { // 토큰이 만료되었을 경우
throw new JwtException("ERROR_EXP_JWT");
}
}
// Authorization Header를 통해 인증을 한다.
public String resolveToken(HttpServletRequest request) {
return request.getHeader("Authorization");
}
// 토큰 검증
public boolean validateToken(String token) {
try {
// Bearer 검증
if (!token.substring(0, "BEARER ".length()).equalsIgnoreCase("BEARER ")) {
return false;
} else {
token = token.split(" ")[1].trim();
}
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
// 만료되었을 시 false
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
public JwtAuthenticationFilter(JwtProvider jwtProvider) {
this.jwtProvider = jwtProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 헤더의 토큰값을 가져옴
String token = jwtProvider.resolveToken(request);
// 토큰의 유효성 검사를 통해 boolean값을 가져옴
if (token != null && jwtProvider.validateToken(token)) {
// check access token
token = token.split(" ")[1].trim();
Authentication auth = null;
try {
// 권한을 가져옴
auth = jwtProvider.getAuthentication(token);
System.out.println(auth);
} catch (Exception e) {
throw new RuntimeException(e);
}
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
private final JwtProvider jwtProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ID, Password 문자열을 Base64로 인코딩하여 전달하는 구조
.httpBasic().disable()
// 쿠키 기반이 아닌 JWT 기반이므로 사용하지 않음
.csrf().disable()
// CORS 설정
.cors(c -> {
CorsConfigurationSource source = request -> {
// Cors 허용 패턴
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(
Arrays.asList("*")
);
config.setAllowedMethods(
Arrays.asList("*")
);
return config;
};
c.configurationSource(source);
}
)
// Spring Security 세션 정책 : 세션을 생성 및 사용하지 않음
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 조건별로 요청 허용/제한 설정
.authorizeRequests()
// 회원가입과 로그인은 모두 승인
.antMatchers("/register", "/login").permitAll()
// /admin으로 시작하는 요청은 ADMIN 권한이 있는 유저에게만 허용
.antMatchers("/admin/**").hasRole("ADMIN")
// /user 로 시작하는 요청은 USER 권한이 있는 유저에게만 허용
.antMatchers("/user/**").hasRole("USER")
// denyAll: 접근을 전부 제한
// permitAll: 접근을 전부 허용
.anyRequest().denyAll()
.and()
// JWT 인증 필터 적용
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class)
// 에러 핸들링
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 권한 문제가 발생했을 때 이 부분을 호출한다.
response.setStatus(403);
response.setCharacterEncoding("utf-8");
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write("권한이 없는 사용자입니다.");
}
})
// AuthenticationEntryPoint
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 인증문제가 발생했을 때 이 부분을 호출한다.
response.setStatus(401);
response.setCharacterEncoding("utf-8");
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write("인증되지 않은 사용자입니다.");
}
});
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
[JAVA] jjwt library 사용방법 - JWT(Java Web Token)-밤둘레
JWT payload (claims, body) 부 암호화 및 복호화 방법-호형