이번엔 Security 설정과 Token에 대해 설정하려고 합니다.
먼저 패스워드 암호화를 적용하기 위한 Config 파일을 만들었습니다.
PasswordEncoderConfig.java
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
다음 순서에 따라 작성된 Security 설정입니다.
- SecurityFilterChain filterChain(HttpSecurity http) 메서드:
- 이 메서드는 SecurityFilterChain을 빈으로 등록하고, 필터 체인을 구성하는 역할을 합니다.
- http.headers().frameOptions().sameOrigin():
- X-Frame-Options 헤더를 설정하여, 웹 애플리케이션이 iframe 내에서 보호되도록 합니다. sameOrigin() 옵션은 같은 출처(Origin)에서만 iframe으로 해당 페이지를 렌더링할 수 있도록 제한합니다.
- formLogin().disable():
- 기본 로그인 폼 기능을 비활성화합니다. 이것은 커스텀 로그인 방법을 사용하고 있으므로, Spring Security의 기본 로그인 폼을 사용하지 않겠다는 의미입니다.
- csrf().disable():
- CSRF(Cross-Site Request Forgery) 공격 방지를 비활성화합니다. 이 설정은 테스트 목적 등에서는 비활성화할 수 있지만, 보안상 중요한 애플리케이션에서는 신중하게 고려해야 합니다.
- cors().configurationSource(configurationSource()):
- CORS (Cross-Origin Resource Sharing) 구성을 설정합니다. configurationSource() 메서드를 통해 CORS 구성 정보를 지정합니다.
- httpBasic().disable():
- HTTP Basic 인증을 비활성화합니다.
- sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS):
- 세션 관리를 구성합니다. STATELESS로 설정하면, 애플리케이션은 세션을 생성하지 않고, 상태를 유지하지 않는 상태로 작동합니다. 이는 RESTful API와 같이 상태를 저장하지 않는 애플리케이션에서 사용됩니다.
- authorizeRequests():
- URL 경로에 대한 인가 규칙을 설정합니다. 이 코드에서는 "/api/v1/**" 경로에 대한 요청은 모두 허용하도록 설정되어 있습니다.
- exceptionHandling():
- 예외 처리를 구성합니다. accessDeniedHandler 및 authenticationEntryPoint를 설정합니다. 이것은 권한이 없는 경우와 인증되지 않은 요청에 대한 처리 방법을 정의합니다.
- http.addFilterBefore(new JwtAuthFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class):
- JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가합니다. 이 필터는 JWT 토큰을 검증하고 사용자를 인증하는 역할을 합니다.
- return http.build():
- 설정이 완료된 http 객체를 반환하고, 이를 SecurityFilterChain 빈으로 등록합니다.
- AuthenticationManage 빈을 생성합니다.
- CORS(Cross-Origin Resource Sharing)구성을 설정하는 빈을 생성합니다.
SecuityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
private final CustomAccessDeniedHandler accessDeniedHandler;
private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
private final CustomOAuth2UserService customOAuth2UserService;
private final JwtProvider jwtProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers().frameOptions().sameOrigin()
.and()
.formLogin().disable()
.csrf().disable()
.cors().configurationSource(configurationSource())
.and()
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/v1/**").permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
http.addFilterBefore(new JwtAuthFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of());
configuration.setAllowCredentials(true); // 토큰 주고 받을 때
configuration.addAllowedHeader("*");
configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PATCH", "PUT", "DELETE", "OPTIONS"));
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
JWT를 사용하여 사용자의 인증 및 권한 부여를 처리하는 필터를 작성 했습니다.
JwtAuthFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
@Value("${jwt.secret}")
private String secretKey;
private static final String TOKEN_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// request Header에서 accessToken을 가져온다.
String accessToken = resolveToken(request);
// 토큰 검사 생략 (모두 허용 URL인 경우 토큰 검사 통과)
if (!StringUtils.hasText(accessToken)) {
doFilter(request, response, filterChain);
return;
}
// AccessToken을 검증하고 만료 되었을 경우 예외 발생
if (!jwtProvider.verifyToken(accessToken)) {
throw new JwtException(ErrorCode.ACCESS_TOKEN_EXPIRATION);
}
// AccessToken 값이 있고, 유효한 경우에만 진행
if (jwtProvider.verifyToken(accessToken)) {
// AccessToken 내부 payload에 있는 email로 user를 찾는다 -> 없다면 정상 토큰이 아님 (오류 반환)
Authentication authentication = jwtProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
// AccessToken 값만 추출
public String resolveToken(HttpServletRequest httpServletRequest) {
String bearerToken = httpServletRequest.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) {
return bearerToken.substring(TOKEN_PREFIX.length());
}
return null;
}
}
Jwt 토큰을 위한 JwtPovider를 작성했습니다.
JwtProvider.java
@Service
@RequiredArgsConstructor
public class JwtProvider implements InitializingBean {
private final UserDetailsService userDetailsService;
@Value("${jwt.secret}")
private String secret;
private Key signingKey;
private static final String ROLE_KEY = "role";
// signingKey 생성
@Override
public void afterPropertiesSet() throws Exception {
byte[] secretKeyByte = secret.getBytes();
signingKey = Keys.hmacShaKeyFor(secretKeyByte);
}
public String generateAccessToken(String email, String role) {
long tokenPeriod = 1000L * 60L * 30L; // 30분
// 새로운 클레임 객체를 생성, 이메일과 역할 (권한) 설정
Claims claims = Jwts.claims().setSubject(email);
claims.put(ROLE_KEY, role);
Date now = new Date();
return Jwts.builder()
// Payload를 구성하는 속성들 정의
.setClaims(claims)
// 발행 일자
.setIssuedAt(now)
// 토큰 만료 일시
.setExpiration(new Date(now.getTime() + tokenPeriod))
// 저장된 서명 알고리즘과 비밀 키로 토큰 서명
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
public String generateRefreshToken() {
long refreshPeriod = 1000L * 60L * 60L * 24L * 14L; // 2주
Date now = new Date();
return Jwts.builder()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + refreshPeriod))
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
// 유효 토큰 확인
public Boolean verifyToken(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(signingKey) // 비밀 키 설정하여 파싱
.build().parseClaimsJws(token); // 주어진 토큰 파싱하여 claims 객체 빼오기
// 토큰 만료 기간과 현재 시간 비교
return claims.getBody()
.getExpiration()
.after(new Date()); // 만료 시간이 현재 시간 이후인지 확인하여 유효성 결과를 반환
} catch (Exception e) {
return false;
}
}
// 권한 생성
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(getEmail(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 이메일 추출
public String getEmail(String token) {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build().parseClaimsJws(token)
.getBody().getSubject();
}
// 토큰에서 권한 추출
public String getTokenRole (String token) {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build().parseClaimsJws(token)
.getBody().get(ROLE_KEY, String.class);
}
// AccessToken 만료 시간이 5분 보다 적게 남았는지 확인
public boolean getExpiration(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(signingKey) // 비밀 키 설정하여 파싱
.build().parseClaimsJws(token); // 주어진 토큰 파싱하여 claims 객체 빼오기
Date expiration = claims.getBody().getExpiration();
Date now = new Date();
long timeDiff = expiration.getTime() - now.getTime();
int fiveMinutes = 1000 * 60 * 5; // 5분
return timeDiff < fiveMinutes;
} catch (Exception e) {
return false;
}
}
}
여기까지가 자체 로그인 마무리 입니다. 다음 글에서는 소셜 로그인에 대한 코드를 올리도록 하겠습니다.
[Spring Boot] 회원 가입 & 로그인(3)
로그인을 하기 위해 JWT 토큰을 사용하기로 결정 했습니다. 그래서 그전에 JWT 토큰이 무엇인지 어떻게 사용되는 것인지 먼저 간단하게 알아보려고 합니다. JWT(JSON Web Token)란 인증에 필요한 정보
classruntime.tistory.com
[Spring Boot]회원 가입 & 로그인(2)
이번엔 도메인 설정 이후 컨트롤러와 서비스를 만들어 보도록 하겠습니다. 먼저 반환 형식을 맞추려고 합니다. Response { "status" : "success", "message" : "회원 가입 성공", "data" : { "userId" : 1 } } 위와 같
classruntime.tistory.com
[Spring Boot]회원 가입 & 로그인 (1)
기본 회원 가입 및 로그인을 구현하면서 소셜 로그인도 같이 병합해서 사용해야겠다는 생각이 들어 만들어 봤습니다. ERD는 쇼핑몰을 생각하면서 만들어 봤습니다. 먼저 기본 회원 가입 ERD 입니
classruntime.tistory.com
GitHub - Llimy1/Auth_Spring
Contribute to Llimy1/Auth_Spring development by creating an account on GitHub.
github.com
'Backend > Spring' 카테고리의 다른 글
[Spring Boot] 회원 가입 & 로그인(5) (0) | 2023.10.04 |
---|---|
[Spring Boot] 회원 가입 & 로그인(3) (0) | 2023.10.04 |
[Spring Boot]회원 가입 & 로그인(2) (0) | 2023.09.26 |
[Spring Boot]회원 가입 & 로그인 (1) (0) | 2023.09.26 |