728x90
반응형
#. 시작
로그인과 관련된 내용인 Jwt를 사용한 인증 방식을 구현한 예제 코드를 정리해보겠다.
#. 예제 소스
1. 필터 기능 관련(JwtFilter.java)
// JwtFilter.java
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final TokenProvider jwtProvider;
private final UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String token = request.getHeader("Authorization");
String username = null;
// Bearer token 검증 후 user name 조회
if(token != null && !token.isEmpty()) {
String jwtToken = token.substring(7);
username = jwtProvider.getUsernameFromToken(jwtToken);
}
// token 검증 완료 후 SecurityContextHolder 내 인증 정보가 없는 경우 저장
if(username != null && !username.isEmpty() && SecurityContextHolder.getContext().getAuthentication() == null) {
// Spring Security Context Holder 인증 정보 set
SecurityContextHolder.getContext().setAuthentication(getUserAuth(username));
}
filterChain.doFilter(request,response);
}
/**
* token의 사용자 idx를 이용하여 사용자 정보 조회하고, UsernamePasswordAuthenticationToken 생성
*
* @param username 사용자 idx
* @return 사용자 UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getUserAuth(String username) {
var userInfo = userService.getUserById(username);
return new UsernamePasswordAuthenticationToken(userInfo.getId(),
userInfo.getPassword(),
Collections.singleton(new SimpleGrantedAuthority(userInfo.getRole()))
);
}
}
- Request Header에 발급받은 Jwt token이 있는지 확인하는 부분이다.
2. Spring Security 설정 관련(SecurityConfig.java)
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Component
public class SecurityConfig{
private final JwtFilter jwtFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception{
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector);
// white list (Spring Security 체크 제외 목록)
MvcRequestMatcher[] permitAllWhiteList = {
mvc.pattern("/login"),
mvc.pattern("/register"),
mvc.pattern("/token-refresh"),
mvc.pattern("/favicon.ico"),
mvc.pattern("/error"),
mvc.pattern("/**"),
};
http
.csrf(AbstractHttpConfigurer::disable) // CSRF
.headers((headerConfig)->
headerConfig.frameOptions(frameOptionsConfig ->
frameOptionsConfig.disable())
)
.authorizeHttpRequests((authorizeRequests)->
authorizeRequests
.requestMatchers(permitAllWhiteList).permitAll()
//.requestMatchers("/api/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
)
//.with(new JwtSecurityConfig(tokenProvider), JwtSecurityConfig::build)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
// before filter
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling((exceptionConfig) ->
exceptionConfig.authenticationEntryPoint(unauthorizedEntryPoint).accessDeniedHandler(accessDeniedHandler)
);
// session management
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 미사용
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.addAllowedOrigin("http://localhost:3000");
corsConfig.setAllowCredentials(true);
corsConfig.addAllowedHeader("*");
corsConfig.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return source;
}
private final AuthenticationEntryPoint unauthorizedEntryPoint =
(request, response, authException) -> {
ErrorResponse fail = new ErrorResponse(HttpStatus.UNAUTHORIZED, "Spring security unauthorized...");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
String json = new ObjectMapper().writeValueAsString(fail);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.write(json);
writer.flush();
};
private final AccessDeniedHandler accessDeniedHandler =
(request, response, accessDeniedException) -> {
ErrorResponse fail = new ErrorResponse(HttpStatus.FORBIDDEN, "Spring security forbidden...");
response.setStatus(HttpStatus.FORBIDDEN.value());
String json = new ObjectMapper().writeValueAsString(fail);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.write(json);
writer.flush();
};
@Getter
@RequiredArgsConstructor
public class ErrorResponse {
private final HttpStatus status;
private final String message;
}
}
- filterChain 메소드를 보면 Spring Security의 formlogin과 csrf, logout 기능을 사용하지 않고 addFilterBefore에 구현한 jwt 관련 Filter 기능을 적용했다.
3. 토큰 관련(TokenProvider.java)
// TokenProvider.java
@Component
@RequiredArgsConstructor
@Slf4j
public class TokenProvider {
// jwt 만료 시간 1시간
private static final long JWT_TOKEN_VALID = (long) 1000 * 60 * 30;
@Value("${jwt.secret}")
private String secret;
private SecretKey key;
@PostConstruct
public void init() {
key = Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* token Username 조회
*
* @param token JWT
* @return token Username
*/
public String getUsernameFromToken(final String token) {
return getClaimFromToken(token, Claims::getId);
}
/**
* token 사용자 속성 정보 조회
*
* @param token JWT
* @param claimsResolver Get Function With Target Claim
* @param <T> Target Claim
* @return 사용자 속성 정보
*/
public <T> T getClaimFromToken(final String token, final Function<Claims, T> claimsResolver) {
// token 유효성 검증
if(Boolean.FALSE.equals(validateToken(token)))
return null;
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* token 사용자 모든 속성 정보 조회
*
* @param token JWT
* @return All Claims
*/
private Claims getAllClaimsFromToken(final String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 토큰 만료 일자 조회
*
* @param token JWT
* @return 만료 일자
*/
public Date getExpirationDateFromToken(final String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* access token 생성
*
* @param id token 생성 id
* @return access token
*/
public String generateAccessToken(final String id) {
return generateAccessToken(id, new HashMap<>());
}
/**
* access token 생성
*
* @param id token 생성 id
* @return access token
*/
public String generateAccessToken(final long id) {
return generateAccessToken(String.valueOf(id), new HashMap<>());
}
/**
* access token 생성
*
* @param id token 생성 id
* @param claims token 생성 claims
* @return access token
*/
public String generateAccessToken(final String id, final Map<String, Object> claims) {
return doGenerateAccessToken(id, claims);
}
/**
* JWT access token 생성
*
* @param id token 생성 id
* @param claims token 생성 claims
* @return access token
*/
private String doGenerateAccessToken(final String id, final Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setId(id)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALID)) // 30분
.signWith(key)
.compact();
}
/**
* refresh token 생성
*
* @param id token 생성 id
* @return refresh token
*/
public String generateRefreshToken(final String id) {
return doGenerateRefreshToken(id);
}
/**
* refresh token 생성
*
* @param id token 생성 id
* @return refresh token
*/
public String generateRefreshToken(final long id) {
return doGenerateRefreshToken(String.valueOf(id));
}
/**
* refresh token 생성
*
* @param id token 생성 id
* @return refresh token
*/
private String doGenerateRefreshToken(final String id) {
return Jwts.builder()
.setId(id)
.setExpiration(new Date(System.currentTimeMillis() + (JWT_TOKEN_VALID * 2) * 24)) // 24시간
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(key)
.compact();
}
/**
* token 검증
*
* @param token JWT
* @return token 검증 결과
*/
public Boolean validateToken(final String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (SecurityException e) {
log.warn("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.warn("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
log.warn("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.warn("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
- access token 과 refresh token을 발행하고 관리하는 로직이다.
#. 마무리
인증과 관련하여 많이 쓰이는 jwt 관련 예제를 살펴보았다. 다음에는 이 예제를 이용해서 로그인 및 접속한 사용자와 관련된 기능을 구현해보도록 하겠다.
#. 참고
- 아래 깃허브의 소스를 참고하여 해당 예제를 작성하였다!
728x90
반응형
'Spring' 카테고리의 다른 글
[Spring Boot] JPA Entity에 현재 시간 적용 방법 (0) | 2024.12.29 |
---|---|
[Spring Boot] JAR 빌드 후 실행 시 초기화면 404 오류(index.html 관련) (0) | 2024.11.28 |
[Java]자바 공부 시작! (0) | 2019.01.05 |