JWT 0.12.x 버전 사용
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
application.properties
spring.jwt.secret="암호화 키"
초기 인가 작업
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//csrf disable
http
.csrf((auth) -> auth.disable());
//From 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());
//http basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());
//세션 설정
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Session 방식은 session이 고정되기 때문에 csrf를 활성화 해야 하지만, jwt 방식은 session이 유지되기 않고, stateless하게 작동하기 때문에 csrf를 비활성화 해도 됨.
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
Session을 stateless하게 설정
UsernamePasswordAuthenticationFilter의 경우, formLogin을 활성화 하면, 유저를 검증 해주는데,
(UsernamePasswordAuthenticationFilter가 호출한 AuthenticationManager를 통해 진행하며 DB에서 조회한 데이터를 UserDetailsService를 통해 받음)
jwt방식의 경우 formLogin을 비활성화 하기 때문에 직접 구현해야 한다.
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//클라이언트 요청에서 username, password 추출
String username = obtainUsername(request);
String password = obtainPassword(request);
//스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
//token에 담은 검증을 위한 AuthenticationManager로 전달
return authenticationManager.authenticate(authToken);
}
//로그인 성공시 실행하는 메소드 (여기서 JWT를 발급하면 됨)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
}
//로그인 실패시 실행하는 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
}
}
SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// AuthenticationConfiguration 주입받기
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);
...
}
// AuthenticationManager Bean 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
@Component
public class JWTUtil {
private final SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}")String secret) {
secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
public String createJwt(String username, String role, Long expiredMs) {
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
JWT 토큰을 발급할 클래스 생성
Payload에 저장될 정보
username, role, 생성일, 만료일
SecurityConfig.java
public class SecurityConfig {
...
private final JWTUtil jwtUtil;
// JWTUtil 의존성 주입
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);
...
}
LoginFilter.java
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
...
private final JWTUtil jwtUtil;
...
//로그인 성공시 실행하는 메소드 (여기서 JWT를 발급하면 됨)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();
String username = customUserDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
String token = jwtUtil.createJwt(username, role, 60*60*10L);
response.addHeader("Authorization", "Bearer " + token);
}
//로그인 실패시 실행하는 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
response.setStatus(401);
}
}
토큰 검증 성공 시, 실패 시 메소드 구현
HTTP 인증 방식은 RFC 7235 정의에 따라
Authentication: Bearer TokenString
형태를 가져야 함
JWTFilter.java
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//request에서 Authorization 헤더를 찾음
String authorization= request.getHeader("Authorization");
//Authorization 헤더 검증
if (authorization == null || !authorization.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
//Bearer 부분 제거 후 순수 토큰만 획득
String token = authorization.split(" ")[1];
//토큰 소멸 시간 검증
if (jwtUtil.isExpired(token)) {
System.out.println("token expired");
filterChain.doFilter(request, response);
return;
}
//토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
//user Entity를 생성하여 값 set
User user = new User();
user.setUsername(username);
user.setPassword("temppassword");
user.setRole(role);
//UserDetails에 회원 정보 객체 담기
CustomUserDetails customUserDetails = new CustomUserDetails(user);
//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
JWT 토큰 검증을 위한 필터
SecurityConfig.java
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
해당 필터 추가
'Web > Spring Security' 카테고리의 다른 글
JWT 방식 OAuth2.0 클라이언트 동작 원리 (0) | 2025.02.26 |
---|---|
Spring Security 6.x.x JWT 보안 요소 추가 (0) | 2025.02.20 |
Spring Security JWT 인증 인가 순서 (0) | 2025.02.14 |
Spring Security 6.x.x 세션 기반 인증 인가 (0) | 2025.02.13 |
Thymeleaf + Spring Security 사용 시 principal 변수 접근 (0) | 2023.04.17 |