코딩마을방범대
JWT - (2) Java에서 JWT 사용하기 본문
728x90
의존성 추가
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
간단한 토큰 생성과 검증
토큰 생성
KEY값은 난수 생성 사이트 에서 생성하여 사용할 수 있음
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(); // 토큰 생성
토큰 검증
get을 통해 데이터 가져오기
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를 이용한 JWT
build.gradle
dependencies{
...
implementation 'org.springframework.boot:spring-boot-starter-security'
User Entity의 경우 UserDetails 를 통합 구현할 필요 없이 별도의 클래스를 생성하여 구현할 수 있음
(role의 경우 별도로 필드를 추가하거나 테이블을 추가해도 되지만 테스트용이기 때문에 따로 만들지 않았음)
Spring Security 5.7.0 부터는 더이상 WebSecurityConfigurerAdapter를 확장해서 사용하지 않고 Bean을 주입하는 방식으로 사용
CustomUserDetails
getAuthorities 메소드
- 해당 user의 role 리스트를 가져오는 부분인데
따로 필드나 테이블을 생성하지 않았기 때문에 임의로 USER 권한을 부여함
( 권한 설정 시 앞에 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;
}
}
JpaUserDetailsService
UserDetails를 설정해주는 클래스로 리턴
@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);
}
}
JwtProvider
JWT의 생성, 검증 등을 구현하는 클래스
@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;
}
}
}
JwtAuthenticationFilter
토큰의 유효성을 검증하는 클래스
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);
}
}
SecurityConfig
Spring Security의 설정 클래스
cors
- addAllowedOrigin() : 허용할 URL
- addAllowedHeader() : 허용할 Header
- addAllowedMethod() : 허용할 Http Method
@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();
}
}
포스트맨의 경우 Header - Bearer Token에 유효한 토큰값을 실어 보내야 오류가 발생하지 않음
참고사이트
[JAVA] jjwt library 사용방법 - JWT(Java Web Token)-밤둘레
JWT payload (claims, body) 부 암호화 및 복호화 방법-호형
728x90
'💡 백엔드 > Java' 카테고리의 다른 글
SpringBoot의 QueryDSL (0) | 2023.05.28 |
---|---|
SpringBoot의 @Converter (0) | 2023.05.28 |
비대칭키를 이용한 인증서 생성 - (3) 실무 코드 정리 (0) | 2023.05.27 |
Query Method의 언더바 (0) | 2023.05.27 |
Spring Security 기초 (0) | 2023.05.27 |