SpringSecurity + JWT token

ys·2024년 3월 22일

SpringSecurity+JWT

목록 보기
1/3
  • 저번 게시글에서는 간단하게 spring security에 대해서 알아보았다
  • 이번시간에는 spring security에 token을 발급하는 기능과
  • 응답 스펙도 Api로 정의해서
  • Spring Security + Jwt Token + Api 기능을 잘 활용하도록 해보자

Api 스펙 정의

  • api 스펙은 간단하게 정의하겠다
  • 다음과 같이 result를 통해서, http conde와 설명등을 주고
  • 제네릭 클래스를 이용해, body에 전달하고 싶은 데이터를 담아서 주는 형식으로 정의하였다
  • result안에 성공, 실패를 모두 정의한 후 static 매서드로 만들어서, 편리하게 응답을 할 수 있게 하였다
  • 실패의 경우 ErrorCodeIfs라는 인터페이스를 만들고, enum 타입으로, 각각의 오류마다 구현해 result값을 다르게 응답하게 하였다
  • 그 다음 exception을 만들어서, runtime Exception을 구현한, 오류가 발생시 예외를 터트려, exception handler에서 예외 처리를 해주기로 하였다
  • exception 또한, ApiExceptionIfs로 인터페이스를 만들고 각각의 예외를 구현하였다
  • 다음 그림은, 디렉토리 구조이다


SpringSecurity + Jwt Token

✅Jwt Token

  • jwt token에 대해서는 다음 블로그에서 정리를 해두었다
  • https://velog.io/@yys/%EC%9B%B9%EC%9D%B8%EC%A6%9D.2JWT-Token
  • 간단하게 확인해보자면...
  • jwt token토큰을 서버측에서 생성을 하고, 클라이언트에게 토큰을 http 메시지 header에 "Authorization"라는 이름으로 전달을 해준다
  • 클라이언트는 전달 받은, 토큰을 가지고 요청 때,header에 토큰을 넣어 전달을 해주면, 서버는 토큰이 유효하고, 무결한지 검증을 하고 확인이 되면 해당 요청을 실행해준다
  • jwt token 방식은, 이렇게 토큰을 이용하기 때문에, 서버와 클라이언트가 stateless 하다는 장점이 있다
  • 하지만, token을 탈취당한다면, 문제가 생기는 단점 또한 있다


✅Spring Security

  • spring security에 대해서는 다음 블로그에서 정리를 해두었다
  • https://velog.io/@yys/Spring-Security
  • 스프링 시큐리티는 클라이언트의 요청이 여러개의 필터를 거쳐 DispatcherServlet(Controller)으로 향하는 중간 필터에서 요청을 가로챈 후 검증(인증/인가)을 진행한다.
  • Delegating Filter Proxy
    • 서블릿 컨테이너 (톰캣)에 존재하는 필터 체인에 DelegatingFilter를 등록한 뒤 모든 요청을 가로챈다.
  • 가로챈 요청은 SecurityFilterChain에서 처리 후 상황에 따른 거부, 리디렉션, 서블릿으로 요청 전달을 진행한다.
  • 다음과 같이 서블릿 필터 체인의 DelegatingFilter → Security 필터 체인 (내부 처리 후) → 서블릿 필터 체인의 DelegatingFilter 순서로 작동한다
  • 다음 그림은, SecurityFilterChain의 필터 목록과 순서이다
  • 다음에서 필요한 설정을 사용하고, 구현해서 커스텀도 사용할 수 있다

✅Form 로그인 방식의 UsernamePasswordAuthenticationFilter와 차이

  • Form을 이용한 로그인 방식을 사용한다면
  • 아이디 비밀번호를 form형식으로 전송하고, Seucrity 필터UsernamePasswordAuthentication 필터에서 검증을 진행하게 된다
  • 회원 검증의 경우 UsernamePasswordAuthenticationFilter 호출한 AuthenticationManager을 통해 진행하게 된다
  • 그런데 JWT는 SecurityConfig에서 formLogin방식을 disable할 것이므로, 기본적으로 활성화 된 UsernamePasswordAuthentication Filter는 작동하지 않는다
  • 그렇기에 우리는 해당 필터를 커스텀 해서 등록해야 한다(= 구현해서 filter등록을 해줘야 한다)

- SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;
    private final JwtUtil jwtUtil;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{


        return http
                
                // csrf disable
                .csrf((auth) -> auth.disable())
                // Form 로그인 방식 disable -> jwt 인증 방식을 사용할 것이기 때문에
                .formLogin((auth) -> auth.disable())
                // http basic 인증 방식 disable
                .httpBasic((auth) -> auth.disable())
                // 경로별 인가 작업
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .anyRequest().authenticated()
                // jwt는 세션을 stateless하게 관리한다
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();

    }
   
}
  • 먼저 spring security가 12.3버전으로 업데이트됨에 따라, 기존 방식이 람다식 방식으로 대체되었습니다(자세한건 공식문서 참조)
  • @Configuration : 설정파일로 등록
  • @EnableWebSecurity : spring security 설정을 하겠단 어노테이션
  • 먼저 jwt token 인증 방식을 사용할 것이기 때문에 Form 로그인 방식, http Basic 인증 방식을 모두 disalbe해준다
  • authorizeHttpRequests 를 통해 경로별 인가 작업을 따로 해준다
    • "/login", "/","/join" 경로는 permitAll()을 통해 인가 작업 없이 동작하게 해주었고
    • "/admin" 주소는 "ADMIN"이라는 role이 있어야지 접근 할 수 있게 해주었다
    • .anyRequest().authenticated() 그 외의 주소들은, 인가를 모두 받아야 한다
  • jwt는 세션을 항상 stateless하게 관리하므로 stateless설정을 넣어준다
  • bCryptPasswordEncoder로 비밀번호를 암호화 하기 위해 Bean으로 등록해준다

회원가입, 로그인

  • 먼저 회원가입부터 보자
  • 회원가입 정보가 Dto로 컨트롤러에 넘어오면 서비스를 거쳐 entity로 변환후 repository로 이동 후 db에 저장된다
  • 이때, @Bean으로 등록된 BCryptPasswordEncoder를 통해 비밀번호를 암호화해서 등록하겠다

  • 다음은 로그인 과정이다, 다음 그림은 servlet container에 그려진 것 처럼 보이지만, 실제로는 security filter 안에서 동작되는 중이다
  • authenticationManager에서 토큰의 검증을 담당한다
  • UserDetailService를 이용해서, db에서 로그인 정보가 맞는지를 검증한다
  • 검증이 성공하면, SucessfulAuthentication으로 이동하고, 실패하면 AuthenticationFailureHandler 로 이동을 한다
  • SucessfulAuthentication 에서 jwt token을 만든 후, response의 header에 담아서 client에게 전달해 주면 된다

✅회원가입

- UserEntity

@Entity
@Table
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class UserEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;
    private String role;
}

- JoinDTO

  • 회원가입을 하기 위한 정보를 받는 DTO
  • 아이디와 비밀번호가 필요하다
@Data
public class JoinDTO {

    private String email;
    private String password;
}

- JoinResponse

  • 회원가입이 성공적으로 끝나면
  • 회원 정보인 email을 주어서 어떤 회원이, 가입이 완료되었는지를 알려준다
@Data
@Builder
public class JoinResponse {

    private String email;
}

- UserRepository

  • JpaRepository를 구현하였다
public interface UserRepository extends JpaRepository<UserEntity,Long> {
    UserEntity findByEmail(String email);
}

- JoinService

@Service
@RequiredArgsConstructor
public class JoinService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;


    public UserEntity joinProcess(JoinDTO joinDTO){
        UserEntity findByEmail = userRepository.findByEmail(joinDTO.getEmail());
        if (findByEmail!=null){
            throw new ApiException(UserError.DUPLICATE_USER_EMAIL);
        }
        UserEntity saveUser = UserEntity.builder()
                .email(joinDTO.getEmail())
                .password(bCryptPasswordEncoder.encode(joinDTO.getPassword()))
                .role("ROLE_ADMIN")
                .build();

        return userRepository.save(saveUser);
    }
}
  • 아이디가 중복된다면, UserErrror의 DUPLICATE_USER_EMAIL라는 오류를 내주고, 나중에 exceptionHandler 에서 한번에 처리해준다
  • 중복되지 않았다면, builder 패턴을 이용하고(비밀번호는 bCryptPasswordEncoder를 통해 암호화 해서 저장한다)

- JoinController

  • join url로 security에 걸리지 않고 접속할 수 있다
  • 응답은 처음 Api 스펙에서 정의한대로, 상태 코드 + joinResponse로 응답을 해준다
@RestController
@RequiredArgsConstructor
public class JoinController {

    private final JoinService joinService;

    @PostMapping("/join")
    public Api<JoinResponse> joinProcess(JoinDTO joinDTO){
        UserEntity userEntity = joinService.joinProcess(joinDTO);

        return Api.OK(JoinResponse
                .builder()
                .email(userEntity.getEmail())
                .build());

    }
}
  • 잘 응답 되는 것을 볼 수 있다!

✅로그인

  • 앞서 봤듯이, config 설정에서 form방식의 필터를 disable 했다
  • 우리가 커스텀해서 필터를 등록해줘야 한다
  • UsernamePasswordAuthenticationFilter구현한 LoginFilter을 만들고, config에 등록해줄 것이다

- LoginFilter

  • 해당 필터는 servlet filter가 아니라, SpringSecurity에 등록할 LoginFilter이다
@RequiredArgsConstructor
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

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

        //클라이언트 요청에서 username, password 추출
        String email = request.getParameter("email");
        String password = request.getParameter("password");

        //스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password, null);

        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) {
    }
}
  • 우리는 request에서 아이디 비밀번호를 가지고, 인증용 객체인 UsernamePasswordAuthenticationToken을 만든다
  • 그리고 UserDetailService를 통해 아이디, 비밀번호가 맞는지 확인하고, 맞다면 authenticationManager를 통해서, Authentication을 만든다**
  • 그리고 옳지 않다면, AuthenticationException 예외를 던지고, 이 예외는 unsuccessfulAuthentication에서 다시 로그인 페이지로 이동하는 등의 예외처리를 할 수 있다
  • 만약 예외처리를 해주지 않는다면, AuthenticationFailureHandler 곳에서 예외에 대한 메시지를 전달해 줄 수 있다.

- CustomUserDetails

  • Entity를 최대한 순수하게 만들고 싶어서, SpringSecurity안의 과정에서 데이터를 전달할 dto를 하나 만들어준다
  • UserDetails 인터페이스를 구현한다
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        Collection<GrantedAuthority> collections = new ArrayList<>();
        collections.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userEntity.getRole();
            }
        });
        return collections;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

- CustomUserDetailService

  • 먼저 UserDetialService를 구현한다
  • email(username)으로 회원을 찾으면, 아까 만든 CustomUserDetials객체를 전달한다
  • 못 찾으면, UsernameNotFoundException의 예외처리를 해줘야 한다
  • UsernameNotFoundExceptionAuthenticationException을 상속받고 있으므로, 예외처리시 AutheticationException을 처리해도 된다!
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByEmail(username);
        if (userEntity != null){
            return new CustomUserDetails(userEntity);
        }
        return null;
    }
}

자 이제 SpringSecurity의 로그인 과정에 대한 모든 부분에 대해서 구현을 하였다

  • 이제 만든 LoginFilter을 SecurityConfig에 등록시켜주자!

- SecurityConfig에 LoginFilter 추가

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{


        return http
                // csrf disable
                .csrf((auth) -> auth.disable())
                // Form 로그인 방식 disable -> jwt 인증 방식을 사용할 것이기 때문에
                .formLogin((auth) -> auth.disable())
                // http basic 인증 방식 disable
                .httpBasic((auth) -> auth.disable())
                // 경로별 인가 작업
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .anyRequest().authenticated())
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class)
                // jwt는 세션을 stateless하게 관리한다
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();

    }
    // authenticationManager을 Bean으로 등록!
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
        return configuration.getAuthenticationManager();
    }
}
  • 우리가 구현한 LoginFilter을 추가해보자
  • addFilterAt() : 원하는 자리에 필터 등록
  • addFilterBefore() : 해당 필터 전에
  • addFilterAfter() : 해당 필터 후에
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)),
UsernamePasswordAuthenticationFilter.class)
  • UsernamePasswordAuthenticationFilter.class 자리에, LoginFilter을 추가해준다
  • 생성자로 authenticationManager를 사용하므로 넣어주고
  • authenticationManager는 생성자로 authenticationConfiguration사용하므로 @Bean으로 등록 후 추가해준다
  • authenticationConfiguration은 spring이 등록해주므로, 생성자 주입으로 받는다

✅JWT 생성, 검증

이제 로그인 성공을 한다면, successfulAuthentication()를 통해 Jwt를 생성하는 로직을 추가하자

  • application.yml에 secret-key를 설정해주자
  • 이곳에서 토큰의 생성 시간도 설정해줄 수 있다
  • @Value를 이용해서 사용할 수 있다
spring:
  jwt:
    secret-key: springbootstudyjwttoken20240321thiskeyshouldbelong

🤔JWTUtil

  • JwtUtil이라고 jwt token을 생성하는 만들자!
@Component
public class JwtUtil {
    private final SecretKey secretKey ;
    public JwtUtil(@Value("${spring.jwt.secret-key}") String secret){
        this.secretKey  =new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    // 전달 받은 토큰에서 payLoad의 claims에서 username(email)을 얻기
    public String getUsername(String token){
        JwtParser parser = makeParser();

        return parser.parseSignedClaims(token)
                .getPayload()
                .get("username", String.class);
    }
    // 전달 받은 토큰에서 payLoad의 claims에서 role을 얻기
    public String getRole(String token){
        JwtParser parser = makeParser();

        return parser.parseSignedClaims(token)
                .getPayload()
                .get("role", String.class);
    }

    // 전달 받은 토큰이 만료되었는지 확인 -> Bool 반환
    public Boolean isExpired(String token){
        JwtParser parser = makeParser();

        return parser.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();
    }
    //  parser을 이용해서, jwt token 검증 메서드
    public boolean validToken(String token){

        try{
            makeParser()
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e){
            if (e instanceof SignatureException){
                throw new TokenException(TokenErrorCode.INVALID_TOKEN);
            }
            else if (e instanceof ExpiredJwtException) {
                // 만료된 토큰
                throw new TokenException(TokenErrorCode.EXPIRED_AT,e);
            }
            else {
                // 그 외 처리
                throw new TokenException(TokenErrorCode.INVALID_TOKEN,e);
            }
        }
    }

    private JwtParser makeParser() {
        return Jwts.parser().verifyWith(secretKey).build();
    }

}
  • 해당 JwtUtil을 스프링 빈으로 등록한 후, 계속 사용할 것이므로 @Component 등록을 해준다
  • 우리가 가지고 있는 secretKey는 String 타입이므로, 생성자를 이용해 생성자 안에서 HS256으로 SecretKey타입으로 변환해서 생성하게 코드를 짜준다
  • createJwt 메서드를 통해, Jwt token을 만들고 claim에 username, role을 넣어주고, 시간, 만료시간을 지정해주고 sercrekey를 통해 서명해주고 compact를 해준다
  • getUsername, getRole, isExpired는 Jwts.parser를 이용해 claim과 정보를 통해 token에서 원하는 정보를 뽑는 메서드이다
  • validToken은 token에 대해서 해당 예외들이 나면 예외를 던져 주는 메서드이다
  • 만약 try문이 성공한다면, true값을 반환한다

➡️JwtFilter

  • JwtFilter는 어떤 역할을 하는지 정확한 이해가 필요하다!!!

클라이언트가 처음 로그인 할 때는, 요청 request 메시지에 token이 없다
즉, 로그인 과정에서는 token을 검증하는 JwtFilter가 동작할 필요가 없다

그런데, 특정 경로와 함께 token을 전달한다면? 이 경우에는 request 메시지에 token이 있다.
이 경우에는 token을 검증하는 JwtFilter가 동작해야 한다

  • HTTP 인증 방식은 RFC 7235 정의에 따라 아래 인증 헤더 형태를 가져야 한다.
Authorization: 타입 인증토큰
//예시
Authorization: Bearer 인증토큰string
  • 아 즉, 🤔 우리는 다음 조건을 통해서, JwtFilter을 동작시키면 되겠다는 아이디어를 가질 수 있다

- LoginFilter에 JwtUtil을 주입하기!

  • LoginFilter의 successfulAuthentication에서 토큰을 발행해줘야 하기 때문에
  • JwtUtil을 주입 받아준다
  • SecurityConfigLoginFilter을 추가하는 부분에더 JwtUtil을 주입받고 넣어줘야 한다!

- LoginFilter의 successfulAuthentication

@RequiredArgsConstructor
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;

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

        //클라이언트 요청에서 username, password 추출
        String email = request.getParameter("email");
        String password = request.getParameter("password");

        System.out.println(email);

        //스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password, null);

        return authenticationManager.authenticate(authToken);
    }
    //로그인 성공시 실행하는 메소드 (여기서 JWT를 발급하면 됨)
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {

        CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();
        // username(email)가져오기
        String email = customUserDetails.getUsername();
        // 권한 가져오기
        Collection<? extends GrantedAuthority> authorities = customUserDetails.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority authority = iterator.next();
        String role = authority.getAuthority();

        // jwt 생성
        String token = jwtUtil.createJwt(email, role, 60 * 60 * 10L);

        response.addHeader("Authorization","Bearer " + token);
    }
    //로그인 실패시 실행하는 메소드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        response.setStatus(401);
    }
}
  • Authentication에서 getPrincipal을 통해 + downcation으로 CustonUserDetails를 가져온다
  • 그 후 JwtUtil의 메서드를 통해 username과 role즉 권한을 가져오고
  • createJwt메서드를 통해 JWT Token을 생성한다
  • 그 후에 RFC 7235 정의에 따라 Autorization 헤더 이름에 Bearer +token으로 header에 토큰을 넣어서 전달해준다
  • 로그인을 성공하면
  • body는 비어있고, header에 token이 들어가 있는 형태로 응답을 한다
  • 응답이 성공하면, 다음과 같이 응답을 보낸다.

🤔JwtFilter!!!

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";

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

        // request에서 Authorization 헤더를 찾기
        String authorization = request.getHeader(HEADER_AUTHORIZATION);

        // Authorization 헤더 검증 -> jwt token인지
        if (authorization == null || !authorization.startsWith("Bearer ")){
            log.info("token null");
            filterChain.doFilter(request,response);
            // 다음 조건이 해당하면 -> 메서드 종료
            return;
        }

        // 가져온 값에서 접두사 제거 -> 토큰 꺼내 오기
        String token = getAccessToken(authorization);

        if (jwtUtil.validToken(token)){

            // 토큰에서 username, role 획득
            String email = jwtUtil.getUsername(token);
            String role = jwtUtil.getRole(token);

            // userEntity를 생성해서 해당 값을 넣어준다
            UserEntity userEntity = UserEntity.builder()
                    .email(email)
                    .role(role)
                    .password("temppassword")
                    .build();

            //UserDetails에 회원 정보 객체 담기
            CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);

            //스프링 시큐리티 인증 토큰 생성
            Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

            //세션에 사용자 등록
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }


        filterChain.doFilter(request, response);
    }

    private String getAccessToken(String authorization){
        if (authorization != null && authorization.startsWith(TOKEN_PREFIX)){
            return authorization.substring(TOKEN_PREFIX.length());
        }
        return null;
    }
}
  • 아까 말했듯, 이 JwtFilter는 요청에 Authentication이라는 header가 있을 때만 실행한다.
  • 즉 if문을 통해서, 만약 토큰이 없다면 다음 JwtFilter는 doFilter을 통해 넘기게 된다
  • getAccessToken메서드를 통해 실제 token만 꺼내고
  • JwtUtil의 validToken을 통해 true이면, 토큰에서 이름과, role을 꺼내 인증 객체를 하나 만들어 주고
  • 그 객체를 통해 CustonUserDetails 객체를 만들고
  • UsernamePasswordAuthenticationToken을 생성해 SecurityConextHolder안의 SecurityConext안에 Authentication을 저장한다!!!
  • 이는 세션으로 해당 요청이 끝날 때까지 사용자를 등록하게 되고
  • 요청이 끝나게 되면 해당 세션은 삭제된다
  • 이 세션은 Stateless 상태로 관리되므로 해당 요청이 끝나면 소멸된다
  • OncePerRequestFilter : 요청에 대해서 한번만 작동하는 필터
    • 여기서는 해당요청에 대해서, token을 검증하고 해당 요청에서만 작동하는 세션을 만들기 위해서 사용된다!!!

- SecurityConfig에 JwtFilter 추가

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;
    private final JwtUtil jwtUtil;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{


        return http
                // csrf disable
                .csrf((auth) -> auth.disable())
                // Form 로그인 방식 disable -> jwt 인증 방식을 사용할 것이기 때문에
                .formLogin((auth) -> auth.disable())
                // http basic 인증 방식 disable
                .httpBasic((auth) -> auth.disable())
                // 경로별 인가 작업
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .anyRequest().authenticated())
                .addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class)
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class)
                // jwt는 세션을 stateless하게 관리한다
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();

    }
    // authenticationManager을 Bean으로 등록!
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
        return configuration.getAuthenticationManager();
    }
  • Login filter 전에, JwtFilter을 적용해서 올바른 token이면, 해당 요청동안 유지되는 세션을 통해, 요청을 처리한다
  • 이번엔 addFilterBefore() 메서드를 사용했다

✅토큰을 header에 넣고 로그인

- AdminController

@RestController
public class AdminController {

    @GetMapping("/admin")
    public String adminP(){
        return "admin Controller";
    }
}
  • 로그인을 성공한 후, header로 받은 token값을 이용해서

  • 인가가 필요한 admin 페이지에 해당 token값을 header에 넣은 후 요청을 보내본다

  • 정상 작동하는 모습을 볼 수 있다!!!


  • SpringSecurity + JwtToken에 대해서 이제 알아보았다
  • cors, crfs에 대해서도 추가적으로 알아보자!
  • 지금은 token이 유효시간이 지나면, TokenException이나는 것으로 예외처리를 했지만, refresh token을 발급받는 부분도 공부를 해보자!
profile
개발 공부,정리

0개의 댓글