○ 로그인 아키텍쳐
JWT 관련 필터들을 구현하면서 자세하게 공부할 필요가 있다고 생각한게
적다보면 이게 무슨 역할을 하는건지, 어떤 원리로 동작하는건지 이해하기 어려웠다.
우선은 로그인 필터를 구현했다. SecurityConfig에서 .formLogin을 disable()했기 때문에
UsernamePasswordAuthenticationFilter가 동작하지 않는다고 한다.
○ formLogin을 비활성화?
- 문득 소스 코드를 작성하다가 생각이 든건 JWT 토큰 인증 방식에서는 꼭 formLogin을 비활성화 해야할까?
토큰 로그인 방식은 stateless 방식이기 때문에 세션을 사용하지 않는다. 따라서 세션을 사용하는 formLogin을 비활성화 하는 것이 일반적이다. 하지만 반드시 비활성화 할 필요는 없다고 한다.
○ formLogin과 JWT 혼용 가능
- 웹 어플리케이션에서 formLogin을 사용하여 인증 후 JWT 토큰을 발급하는 형식으로 특정 요구사항에 따라 formLogin과 JWT 인증을 동시에 사용할 수도 있다.
○ formLogin과 JWT는 각각 어떤 상황에서 사용하는지?
세션 인증 사례
- 전통적인 서버 기반 웹 애플리케이션.
- 인증 상태가 필요한 관리 시스템(예: ERP, CMS 등).
토큰 인증 사례
- RESTful API 또는 SPA(Single Page Application) 기반 웹 애플리케이션.
- 모바일 앱, IoT 기기 등 상태를 저장하지 않는 환경.
이 외에도 React나 Vue.js같은 클라이언트 앱 환경에서는 JWT 토큰 기반 인증 방식을 주로 사용하고 전통적인 HTML 웹 어플리케이션 환경에서는 세션 기반 인증 방식을 사용하는 것이 호환성이 편하다.
이번에 진행하는 J-PLAN의 경우 HTML의 웹 환경이지만 다양한 인증 방식에 대한 기본기를 향상 시키기 위해서 JWT, OAuth2 방식 모두 접목할 예정이다.
○ SecurityConfig 구현
@Configuration
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtUtil jwtUtil;
public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JwtUtil jwtUtil){
this.authenticationConfiguration = authenticationConfiguration;
this.jwtUtil = jwtUtil;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf(auth -> auth.disable());
http
.formLogin((auth) -> auth.disable());
http
.httpBasic((auth) -> auth.disable());
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login","/","/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
);
http
.addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class);
// UsernamePasswordAuthenticationFilter는 form-login 방식에서 사용되기 때문에 대체하기 위함
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);
// 세션 설정
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
○ 로그인 필터 구현
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
public LoginFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil){
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response
) throws AuthenticationException{
// 클라이언트 요청에서 username, password 추출
String username = obtainUsername(request);
String password = obtainPassword(request);
System.out.println(username);
// SpringSecurity에서 username과 password를 검증하기 위해서는 토큰에 담아야함
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
// 토큰에 담은 검증을 위한 AuthenticationManager로 전달
return authenticationManager.authenticate(authToken);
}
@Override
protected void successfulAuthentication(
HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication
) throws IOException, ServletException {
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*10000L);
response.addHeader("Authorization", "Bearer "+token);
}
@Override
protected void unsuccessfulAuthentication(
HttpServletRequest request, HttpServletResponse response, AuthenticationException failed
) throws IOException, ServletException {
System.out.println("failed");
response.setStatus(401);
}
}
○ JwtUtil 구현
@Component
public class JwtUtil {
private Key key;
public JwtUtil(@Value("${jwt.secret}") String secret) {
byte[] byteSecretKey = Decoders.BASE64.decode(secret);
key = Keys.hmacShaKeyFor(byteSecretKey);
}
public String getUsername(String token){
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody().get("username", String.class);
}
public String getRole(String token){
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody().get("role",String.class);
}
public Boolean isExpired(String token){
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody().getExpiration().before(new Date());
}
public String createJwt(String username, String role, Long expiredMs){
// claim을 설정하는 부분.
Claims claims = Jwts.claims(); // 빈 claim을 생성한다
// 빈 claim에 user 정보에 대한 내용 삽입
claims.put("username", username);
claims.put("role",role);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis()+expiredMs))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
}
○ JWT 필터 구현
package JwtTest.jwt;
import JwtTest.dto.CustomUserDetails;
import JwtTest.entity.JwtUser;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtFilter(JwtUtil jwtUtil){
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain
) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
if(authorization == null || !authorization.startsWith("Bearer ")){
System.out.println("token null");
filterChain.doFilter(request,response);
return;
}
String token = authorization.split(" ")[1];
try {
jwtUtil.isExpired(token);
} catch (ExpiredJwtException e){
System.out.println("token expired");
filterChain.doFilter(request,response);
return ;
}
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
JwtUser user = new JwtUser();
user.setUsername(username);
user.setPassword("1234");
user.setRole(role);
CustomUserDetails customUserDetails = new CustomUserDetails(user);
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request,response);
}
}
'Java > SpringBoot' 카테고리의 다른 글
JWT 로그인 테스트-3 (0) | 2025.01.21 |
---|---|
JWT 로그인 테스트-2 (0) | 2025.01.19 |
JWT 로그인 테스트-1 (0) | 2025.01.19 |