본문 바로가기
Programming/Spring Boot

[Spring Boot] Spring Security를 이용한 JWT CORS Filter 로그인 커스터마이징하기

by 공부합시다홍아 2024. 5. 30.
 

[Spring Boot] Spring Security를 이용한 JWT CORS필터체이닝

기본적인 JWT 토큰 발행방법 [Spring Boot] JWT HMAC 암호화[Spring Boot] JWT[Spring Boot] Spring Security스프링 시큐리티스프링 시큐리티는 스프링에 login을 처리하는 모듈이다.같은 시큐리티 기반이라도 코딩

hong-study.tistory.com

이전 포스팅에서는 Spring Security 환경에서 CORS Filter를 적용하는 것에 대해서 공부하였습니다.
이번 포스팅은 CORS Filter를 이용해 로그인 환경을 커스터마이징해보려고 합니다.


Security 설정 파일

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	//패스워드 인코딩
	@Bean
	public BCryptPasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		
		//1~6 생략....
		
		//7. authenticationManager얻기
		AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));		
		
		//9. 로그인진행시키기
		//10. 로그인처리하기
		//http.addFilterBefore( new CustomLoginFilter(authenticationManager),  UsernamePasswordAuthenticationFilter.class ); //로그인진행 필터 - 이렇게 해도됩니다.
		http.addFilter( new CustomLoginFilter(authenticationManager)); //로그인진행 필터

		
		return http.build();
	}
		
	//8. authenticationManager객체
	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}

}

CustomLoginFilter 클래스

public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {

	private AuthenticationManager authenticationManager;
	
	//2. authenticationManager를 시큐리티 설정에서 전달하기
	public CustomLoginFilter(AuthenticationManager authenticationManager) {
		this.authenticationManager = authenticationManager;
	}
	
	//login을 시도함
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		
		System.out.println("===========attemptAuthentication===========>");

		//1. /login요청으로 들어오는 username, userpassword의 폼 형식 로그인데이터를 얻는다.
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		
		System.out.println(username);
		System.out.println(password);
		
		//3. 로그인 실행시키기 - 시큐리티 로그인 기능이 사용하는 전용토큰에 아이디, 비밀번호를 담습니다.
		UsernamePasswordAuthenticationToken token =
				new UsernamePasswordAuthenticationToken(username, password);
		
		//어쏜티케이션매니저의 authenticate가 실행되면 MyUserDetailsService의 loadUserByUsername가 실행됩니다.
		//여기 리턴이 있다는 것은 로그인 성공이라는 의미
		Authentication authentication = authenticationManager.authenticate(token);
		
		System.out.println("===========로그인성공=========>");
		System.out.println("실행됨?:" + authentication);
		
		return authentication; //리턴 (여기서 리턴된 값은 시큐리티세션( new Authentication(new MyUserDetails()) ) 형식으로 저장됩니다.
		
	}

	//successfulAuthentication오버라이딩 - 로그인 성공시 이후에 실행
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		
		System.out.println("===========로그인성공 success핸들러=========>");
		//오쏘티케이션안에는 new Authentication(new MyUserDetails() 형식으로 유저객체가 저장됩니다.
		MyUserDetails principal = (MyUserDetails)authResult.getPrincipal(); //오쏘티케이션 안에 유저객체를 얻습니다.

		System.out.println(principal.getUsername());
		
		System.out.println("===========JWT 토큰 생성 후 header에 담기=========>");
		
		String token = JWTService.createToken(principal.getUsername() );
		
		response.setContentType("text/html; charset=UTF8;");
		response.addHeader("Authorization", "Bearer " + token);	
		response.getWriter().println("로그인성공(아이디):" + principal.getUsername() ); 


	}
	
	//unsuccessfulAuthentication오버라이딩
	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {

		System.out.println("===========로그인성공 fail핸들러=========>");
		System.out.println("인증에 실패함(로그인실패)");
		
		response.setContentType("text/plain; charset=UTF8");
		response.sendError(500, "아이디 비밀번호를 확인하세요");

	}

로그인을 커스터마이징하기 위해 기본 스프링 시큐리티의 로그인을 담당하는 UsernamePasswordAuthenticationFilter 를 상속받고 연결을 해줘야한다.
UsernamePasswordAuthenticationFilter loadByUser를 호출하는 필터이다.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {

위와 같이 오버라이딩한 attempAuthentication 메서드는 공식이 있다.
무조건 POST 방식으로 요청을 받아야 하며, /login 요청이 들어와야 attempAuthentication 메서드와 연결이된다.


String username = request.getParameter("username"); // form 방식으로 받을 때 이렇게 사용
String password = request.getParameter("password");

위 방식은 form 방식으로 받을 때 사용하는 방식으로, 사용자가 로그인을 시도할 때 어떤 값이 들어오는 지 확인할 수 있다.


UsernamePasswordAuthenticationToken token = 
	new UsernamePasswordAuthenticationToken(username, password);

이 값을 Authentication 인증 매니저인 AuthenticationManager로 전달한다.


Authentication authentication = 
	authenticationManager.authenticate(token); // 로그인 실패시 null 반환

유저 서비스의 loadByUser가 호출되는 부분이고, 로그인 성공시 정보를 확인할 수 있고, 실패시 Null을 반환한다.
처음 Spring Security에 대해 알아볼 때 authentication 내부에 principal이 있다는 것을 기억하면 값을 확인하기 편하다.


로그인 성공 / 실패 시 사용할 메서드 오버라이딩

UsernamePasswordAuthenticationFilter 내부의 메서드들 중에서 AbstractAuthenticationProcessingFilter 내부의 
successfulAuthentication unsuccessfulAuthentication 메서드를 이용하면 로그인 성공시와 실패시 처리해줄 비즈니스로직을 구현하여 서비스를 처리할 수가 있다.

successfulAuthentication  : JWT 토큰 발행
unsuccessfulAuthentication  : 실패시 에러 메세지

// 로그인 성공 시 이 메서드가 실행
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult) throws IOException, ServletException {

    System.out.println("로그인 sucees 후에 실행됌");
    // jwt 토큰 발행

    MyUserDetails details = (MyUserDetails)authResult.getPrincipal();


    String token = JWTService.createToken(details.getUsername()); // 토큰 생성
    // 헤더에 담는다.
    response.setContentType("text/html; charset-UTF8");
    response.setHeader("Authorization", token);
    // 데이터도 보낼 수 있는데
    response.getWriter().println("서버에서 보낸 메세지");
}

// 로그인 실패 시에는 이 메서드가 실행
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException failed) throws IOException, ServletException {

    System.out.println("로그인 실패시 실행");

    response.setContentType("text/plain; charset-UTF8");
    response.sendError(500, "아이디와 비밀번호를 확인하세요");
}

oken을 검사하는 필터 BasicAuthenticationFilter 토큰검사 커스터마이징

시큐리티 필터 BasicAuthenticationFilter는 HTTP요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolde(시큐리티세션)에 저장한다.

로그인 처리가 끝났다면, 다음으로 해야할 작업은 토큰을 검사하는 필터를 생성하는 작업이다.

  1. 로그인 이후에 token이 생성되어서 클라이언트로 전달되었다.
  2. 클라이언트는 token을 받는다.
  3. 클라이언트는 token을 기준으로 우리의 서비스를 호출하게 된다. (왜? - 토큰이 인증이야)
  4. 서버에서는 token의 유효성검사를 진행한 후, 유효한 토큰이면, 서비스를 호출하게 처리한다.

SecurityConfig 설정 파일

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		
		//1~6 생략....
		
		//7. authenticationManager얻기
		AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));		
		
		//9. 로그인진행시키기
		//10. 로그인처리하기
		//http.addFilterBefore( new CustomLoginFilter(authenticationManager),  UsernamePasswordAuthenticationFilter.class ); //로그인진행 필터 - 이렇게 해도됩니다.
		http.addFilter( new CustomLoginFilter(authenticationManager)); //로그인진행 필터
		
		//11. 토큰검사필터 등록하기
		//http.addFilterBefore( new JwtAuthrizationFilter(authenticationManager), BasicAuthenticationFilter.class); //토큰검사 필터 - 이렇게 해도됩니다.
		http.addFilter( new JwtAuthrizationFilter(authenticationManager)); //토큰검사 필터

		return http.build();
	}
		
	//8. authenticationManager객체
	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}

}

JWTAuthorizationFilter 클래스

public class JwtAuthrizationFilter extends BasicAuthenticationFilter {
	//오쏘티케이션매니저를 기본으로 받음
	public JwtAuthrizationFilter(AuthenticationManager authenticationManager) {
		super(authenticationManager);
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		
		System.out.println("=========JWT 필터 실행========>");
		
		//헤더값 얻기
		String headers = request.getHeader("Authorization");
		System.out.println("헤더값:" + headers);
		
		
		//헤더가 없거나 Bearer로 시작하지 않으면
		if(headers == null || headers.startsWith("Bearer ") == false ) {
			
			response.setContentType("text/plain; charset=UTF8");
			response.sendError(403, "토큰이 없습니다.");		
			
			return; //함수종료
		}
		
		//토큰 유효성 검사
		try {
			String token = headers.substring( 7 ); //Bearer공백 이후
			boolean result = JWTService.validateToken(token); //토큰유효성검사
			
			if(result) {
				filterChain.doFilter(request, response); //다음필터로 연결
			} else {
				response.setContentType("text/html; charset=UTF8;");
				response.sendError(403, "토큰이 만료되었습니다");	
				
			}
		} catch (Exception e) {
			response.setContentType("text/html; charset=UTF8;");
			response.sendError(403, "토큰이 위조되었습니다");	
		}
		
	}

	
}

URL요청별로 실행시키기

어떤 요청은 token을 필요로 하는 요청이 있다. (api 서비스)
어떤 요청은 token을 필요하지 않는 요청이 있다. (회원가입)

컨트롤러에 간략한 요청 코드를 생성해준다.

/join, 
/api/v1/hello, 
/api/v1/getInfo
/api/v2/~~~~

부메랑에서 URL요청별로 필터를 실행

시큐리티 설정 파일

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		
		//1~6 생략....
		
		//7. authenticationManager얻
		AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));		

		//9. 로그인진행시키기
		//10. 로그인처리하기
		//http.addFilterBefore( new CustomLoginFilter(authenticationManager),  UsernamePasswordAuthenticationFilter.class ); //로그인진행 필터 - 이렇게 해도됩니다.
		//http.addFilter( new CustomLoginFilter(authenticationManager)); //로그인진행 필터
		
		//11. 토큰검사필터 등록하기
		//http.addFilterBefore( new JwtAuthrizationFilter(authenticationManager), BasicAuthenticationFilter.class); //토큰검사 필터 - 이렇게 해도됩니다.
		//http.addFilter( new JwtAuthrizationFilter(authenticationManager)); //토큰검사 필터

		//12. 로그인 + 토큰검사필터 + URL요청별로 분기하기
		http.requestMatchers().antMatchers("/login")
													.and()
													.addFilter(new CustomLoginFilter(authenticationManager));

		http.requestMatchers().antMatchers("/api/v1/**")
													.antMatchers("/api/v2/**")
													.and()
									        .addFilter(new JwtAuthrizationFilter(authenticationManager));

		return http.build();
	}
		
	//8. authenticationManager객체
	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}

}

아직은 이해가 크게 되지 않지만, 일단 흐름을 이해하려고 노력하고 있다.
추후에 더 깊숙하게 공부해 어떤 흐름으로 정확히 동작하는 지 서술할 예정이다.

아래는 전체 소스코드이다.

MemberVO
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class MemberVO {

	private String username;
	private String password;
	private String role;
	
}

Controller
import javax.servlet.http.HttpServletRequest;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;



@RestController
public class APIController {

	// JWT - API 기반의 인증 정보를 처리할 때, 토큰 발급 ( 발행자 )
	
//	@PostMapping("/login")
//	public ResponseEntity<Object> login(@RequestBody MemberVO vo){
//		
//		// 사용자 정보를 받아서 로그인 처리
//		// ok - 로그인 성공
//		// 토큰 발행
//		String token = JWTService.createToken(vo.getUsername());
//		
//		return new ResponseEntity<>(token, HttpStatus.OK);
//	}
	
//	@PostMapping("/api/v1/getInfo")
//	public ResponseEntity<Object> getInfo(HttpServletRequest request){
//		
//		// 클라이언트에서 토큰을 헤더라는 곳에 담는다.
//		// 토큰을 전달받아 유효성을 확인한 후 만료인지, 통과인지 확인
//		String token = request.getHeader("Authorization");
//		System.out.println(token);
//		
//		// 토큰이 유효한지 확인, 
//		try {
//			boolean result = JWTService.validateToken(token);
//			System.out.println("토큰의 무결성 여부 : " + result);
//		} catch (Exception e) {
//			e.printStackTrace(); //토큰이 위조 됐을 경우
//			return new ResponseEntity<>("토큰 위조", HttpStatus.UNAUTHORIZED); //토큰 위조
//		}
//		
//		
//		
//		
//		return new ResponseEntity<>("통과된 사람이면 여기에 회원정보 발급", HttpStatus.OK);
//	}
	
	// 필터 동작 확인
	@GetMapping("/api/v1/filter")
	public String hello() {
		return "<h3>hello</h3>";
	}
	
	@PostMapping("/api/v1/getInfo")
	public ResponseEntity<Object> getInfo(HttpServletRequest request){
		
		System.out.println("getInfo메서드는 토큰이 있는 자만 호출할 권리가 있다.");
		
		return new ResponseEntity<Object>("getInfo ~~", HttpStatus.OK);
	}
	
}

MemberMapper.java
import org.apache.ibatis.annotations.Mapper;
import com.project.jwt.command.MemberVO;


@Mapper
public interface MemberMapper {

	void join(MemberVO vo);
	MemberVO login(String username);
}
MemberMapper.xml
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
	
<mapper namespace="com.project.jwt.member.MemberMapper">

	<insert id="join">
		insert into members values (#{username}, #{password}, #{role})
	</insert>
	
	<select id="login" resultType="com.project.jwt.command.MemberVO">
		select * from members where username = #{username}
	</select>

</mapper>

SecurityConfig
import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.project.jwt.util.filter.CustomLoginFilter;
import com.project.jwt.util.filter.FilterOne;
import com.project.jwt.util.filter.FilterTwo;
import com.project.jwt.util.filter.JWTAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
		
		http.csrf().disable();
		http.formLogin().disable();
		http.httpBasic().disable(); // http의 기본 인증 형태도 폐기 Authorization(아이디,비밀번호 형태 폐기) 
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션방식 비활성화
		
		// 2. 크로스 오리진 활성화
		http.cors( Customizer.withDefaults() );
		
		// 3. 시큐리티에 필터 추가
		//http.addFilter(new FilterOne()); //addFilter에는 시큐리티 타입의 필터만 등록이 된다.
		// 시큐리티 타입의 필터가 아니라 ,일반 필터라면 시큐리티 필터 전후로 필터를 추가한다.
		//http.addFilterBefore(new FilterOne(), UsernamePasswordAuthenticationFilter.class);
		//http.addFilterAfter(new FilterTwo(), FilterOne.class);
		
		// 스프링 시큐리티 타입의 필터를 커스터마이징하여 사용
		// 클라이언트에서 /login post방식으로 요청
		
		AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));		
		//http.addFilter(new CustomLoginFilter(authenticationManager) ); // 로그인 필터
		//http.addFilter(new JWTAuthenticationFilter(authenticationManager)); // JWT 인증 필터
		
		http.requestMatchers().antMatchers("/login")
							  .and()
							  .addFilter(new CustomLoginFilter(authenticationManager));
		
		http.requestMatchers().antMatchers("/api/v1/**")
							  .and()
							  .addFilter(new JWTAuthenticationFilter(authenticationManager));
		
		return http.build();
	}
	
	//1. 크로스 오리진 필터등록
	//서버가 다를 때, 옵션 요청을 보내게 되는데, 옵션 요청에 요청을 도메인 주소를 헤더에 담아 보내는 작업
	@Bean
	CorsConfigurationSource corsConfigurationSource() {
		
		CorsConfiguration configuration = new CorsConfiguration();
		//configuration.setAllowedOrigins(Arrays.asList("http://localhost:8181")); //부메랑에서 요청이 deny됨
		configuration.setAllowedOrigins(Arrays.asList("*")); //모든요청 주소를 허용 == CrossOrigin
		configuration.setAllowedMethods(Arrays.asList("*")); // * : 모든요청 메서드를 허용
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		
		return source;
		
	}
	
	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}
}
MyUserDetails
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.project.jwt.command.MemberVO;

public class MyUserDetails implements UserDetails {

	// 로그인해서 조회한 MemberVO 객체 
	private com.project.jwt.command.MemberVO memberVO;
	
	
	// 반드시 UserVO 객체를 멤버변수로 담고 생성
	public MyUserDetails(MemberVO vo) {
		this.memberVO = vo;
	}
	
	// role 리턴
	public String getRole() {
		return memberVO.getRole();
	}
	
	// 로그인시 권한을 리턴해주는 함수
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		
		List<GrantedAuthority> list = new ArrayList<>();
		
		// 권한이 여러 개라면 반복문
		list.add( () -> memberVO.getRole() );
		
		return list;
	}

	// 유저의 비밀번호를 반환하는 자리
	@Override
	public String getPassword() {
		
		return memberVO.getPassword();
	}

	// 유저의 아이디를 반환하는 자리
	@Override
	public String getUsername() {
		
		return memberVO.getUsername();
	}

	// 계정 만료 여부 확인 ( true = 네 )
	@Override
	public boolean isAccountNonExpired() {
			
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		
		return true; // 계정 락 여부, 락이 걸리지 않았습니까?
	}

	@Override
	public boolean isCredentialsNonExpired() {
		
		return true; // 비밀번호 만료 여부 / true 만료 x
	}

	@Override
	public boolean isEnabled() {
		
		return true; // 계정 사용 여뷰
	}

}
MyUserDetailsService
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.project.jwt.member.MemberMapper;
import com.project.jwt.command.MemberVO;

//빈으로 등록되어 있으면, 스프링이 UserDetailsService 타입을 찾아서, 사용자가 로그인시
//loadUserByUsername을 실행시킨다.
@Service 
public class MyUserDetailsService implements UserDetailsService{

	@Autowired
	private MemberMapper memberMapper;
	
	//loginProcessingUrl에 로그인 URL을 등록
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		System.out.println("사용자가 로그인을 시도함");
		System.out.println("사용자가 입력한 아아디 : " + username);
		
		// 로그인 시도 (비밀번호는 시큐리티가 알아서 비교 후에 처리한다.)
		// 로그인을 하여 vo객체를 얻어온다.
		
		MemberVO vo = memberMapper.login(username);
		
		// 회원정보 존재
		// 비밀번호 비교를 위해, userDeatils 타입으로 리턴
		if(vo != null) {
			return new MyUserDetails(vo); // 스프링 시큐리티가 비밀번호 비교, 권한 확인한 이후 로그인 시도 처리
		}
		// 스프링 시큐리티의 설정한 형식대로, 권한까지 처리를 한다.
		// 아이디가 없거나 비밀번호가 틀리면 login?error 페이지로 기본 이동 ( 설정으로 변경 가능 )
		// 시큐리티는 특별한 세션의 모형을 사용 -> 시큐리티세션(new Authentication(new MyUserDetails() ) ) 모형으로 저장
		
		return null;
	}
	

}

JWTService
import java.util.Date;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator.Builder;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.JWTVerifier;

public class JWTService {

	private static String secretKey = "coding404"; // 시그니처를 만들기 위한 비밀키

    // JWT 토큰 생성
    public static String createToken(String username) {
    	//암호화알고리즘
        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        
        //만료시간
        long expire = System.currentTimeMillis() + 3600000; //1시간뒤
        
        // 페이로드 생성
        Builder builder = JWT.create()
                .withSubject(username) // 토큰의 주제(subject) 설정
                .withIssuedAt(new Date()) // 토큰 발급 시간 설정 (현재 시간)
                .withExpiresAt(new Date(expire)) // 토큰 만료 시간 설정
                .withIssuer("coding404서버") //토큰발행자
        		.withClaim("admin", "나야나~"); //공개클래임

        return builder.sign(algorithm); // 비밀 키로 서명하여 토큰 생성
    }
	
    //토큰검증
    public static boolean validateToken(String token) throws JWTVerificationException { //확인단계중하나라도 실패
        Algorithm algorithm = Algorithm.HMAC256(secretKey); //검증 알고리즘
        JWTVerifier verifier = JWT.require(algorithm).build(); //token을 검증할 객체생성
         
        verifier.verify(token); // 토큰 검증을 수행하며, 만료 시간도 자동으로 검사됨

        return true; // 검증 성공 시 true 반환
    }
    
	
}
FilterOne
import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class FilterOne implements Filter{

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		
		System.out.println("필터1 실행");
		chain.doFilter(request, response);
	}

}
FilterTwo
import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class FilterTwo implements Filter{

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// TODO Auto-generated method stub
		System.out.println("필터2 실행");
		chain.doFilter(request, response);
		
	}
	
}
CustomLoginFilter
import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.project.jwt.security.MyUserDetails;
import com.project.jwt.util.JWTService;

//LoadByUser를 호출하는 필터가 UsernamePasswordAuthenticationFilter이다.
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter{
	//UsernamePasswordAuthenticationFilter : 로그인을 담당하는 필터 중 하나
	// 사용자 로그인을 커스터마이징 시킬 때 상속받고, 연결해주면 된다.
	
	private AuthenticationManager authenticationManager;
	
	public CustomLoginFilter(AuthenticationManager authenticationManager) {
		this.authenticationManager = authenticationManager;
	}
	
	// Post방식의 /login 요청이 들어오면 attemptAuthentication으로 연결
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		
		System.out.println("attemptAuthentication Filter 실행");
		
		
		String username = request.getParameter("username"); // form 방식으로 받을 때 이렇게 사용
		String password = request.getParameter("password");
		
		System.out.println(username + " " + password);
		
		//UsernamePasswordAuthentication
		// 이 값을 인증 매니저 Authuentication Manager에게 전달
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
		
		// 유저 서비스의 loadByUser가 호출된다.
		Authentication authentication = authenticationManager.authenticate(token); // 로그인 실패시 null 반환
		
		System.out.println("필터 로그인 성공 시 : " + authentication);
		
		return authentication; // 스프링 시큐리티가 이 값을 가져가 스피링 세션에 등록시킴
	}

	// 로그인 성공 시 이 메서드가 실행
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		
		System.out.println("로그인 sucees 후에 실행됌");
		// jwt 토큰 발행
		
		MyUserDetails details = (MyUserDetails)authResult.getPrincipal();
		
		
		String token = JWTService.createToken(details.getUsername()); // 토큰 생성
		// 헤더에 담는다.
		response.setContentType("text/html; charset-UTF8");
		response.setHeader("Authorization", token);
		// 데이터도 보낼 수 있는데
		response.getWriter().println("서버에서 보낸 메세지");
	}

	// 로그인 실패 시에는 이 메서드가 실행
	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {

		System.out.println("로그인 실패시 실행");
		
		response.setContentType("text/plain; charset-UTF8");
		response.sendError(500, "아이디와 비밀번호를 확인하세요");
	}

}
JWTAuthenticationFilter
import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import com.project.jwt.util.JWTService;

// BasicAuthenticationFilter는 HTTP 요청의 (BASIC) 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장
public class JWTAuthenticationFilter extends BasicAuthenticationFilter{

	// 생성자는 반드시 authenticationManager에서 받아줌
	public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(authenticationManager);
		// TODO Auto-generated constructor stub
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		System.out.println("jwt basic 헤더 검사하는 필터 실행됌");
		
		// 헤더에 담긴 토큰을 검사해서 토큰의 무결성 여부를 확인해 에러를 띄우거나, 컨트롤러를 실행한다.
		
		// 1.헤더값 얻기
		String header = request.getHeader("Authorization");
		System.out.println(header);
		
		// 토큰을 보내지 않거나, 베어러로 시작하지 않으면
		if(header == null || header.startsWith("Bearer ") == false ) {
			response.setContentType("text/plain; charset-UTF-8");
			response.sendError(403, "토큰이 없습니다.");
			
			return; //함수 종료하지 않으면 Controller로 연결
		}
		
		// 토큰의 유효성 검사
		try {
			String token = header.substring(7);
			boolean result = JWTService.validateToken(token);
			
			// 정상적인 토큰인 경우
			if(result) {
				chain.doFilter(request, response); //컨트롤러로 연결
			}else { // 토큰의 유효기간이 만료된 경우
				response.setContentType("text/plain; charset-UTF-8");
				response.sendError(403, "만료된 토큰입니다..");
			}
		} catch (Exception e) {
			response.setContentType("text/plain; charset-UTF-8");
			response.sendError(403, "잘못된 토큰입니다.");
		}
		
		
	}

	
}

build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.18'
	id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.project'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '11'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
	maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.0'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	//testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
	implementation 'com.auth0:java-jwt:4.4.0'
	
	implementation 'org.springframework.boot:spring-boot-starter-security'
	//시큐리티 타임리프에서 사용
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
	//시큐리티 테스트
	testImplementation 'org.springframework.security:spring-security-test'
	
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}
728x90