2022/2022-1

[SpringBoot] SpringBoot + SpringSecurity + JWT (회원가입 / 로그인 구현)

JWonK 2022. 8. 10. 15:56
728x90
반응형

본 프로젝트를 진행할 때 처음에는 SpringSecurity + Oauth2를 이용한 로그인을 구현하려 했으나 방향성이 바뀌어 일반적인 사용자 정보를 입력한 회원가입 + 로그인을 구현하게 되었다. 

 

회원가입 및 로그인 모두 클라이언트로부터 API를 전송받는 방식으로 진행하였다.

 

 

1. Spring Security 처리 과정


공부할 때 작성했던 Session 기반 로그인과정과 JWT 기반 로그인과정 글이 있다. 본 게시글과 차이점은 아래 게시글들은 구글링으로 다른 분들의 다양한 블로그 글들을 참고하여 따라해본 예제이고, 현재 게시글은 거기에 더해 실제 프로젝트에 적용하게 된 구현코드이다.

 

아래 게시글에서는 현재 스프링에서 지원하지 않는 인터페이스도 존재하여 그 부분까지 수정하였다.

 

https://wonsjung.tistory.com/443

 

[SpringBoot] Spring Security 처리 과정

1. Spring Security 처리 과정 Spring Security의 처리 과정은 기본적으로 Session을 활용한 구현 방식이다. 후에 Token 기반 구현 방식도 공부하고 업로드 할 예정이다. 1. 사용자가 로그인 정보와 함께 인증

wonsjung.tistory.com

 

https://wonsjung.tistory.com/447

 

[SpringBoot] SpringBoot + SpringSecurity + JWT 구현하기

현대 웹 서비스에서는 토큰을 사용하여 사용자들의 인증 작업을 처리하는 것이 가장 좋은 방법이다. 본 프로젝트에서 요구하는 것 또한 JWT + Oauth2 구현이며 이번 글은 토큰 기반의 인증 시스템

wonsjung.tistory.com

 

 

 

 

[ 0. 사전 세팅 ]

먼저 프로젝트에서 사용할 Dependency들을 build.gradle에 추가해준다.

plugins {
	id 'org.springframework.boot' version '2.7.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.html'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:2.6.2'
	implementation group: 'org.springframework.security.oauth', name: 'spring-security-oauth2', version: '2.5.2.RELEASE'

	implementation 'org.springframework.boot:spring-boot-starter-mustache'
	implementation 'io.jsonwebtoken:jjwt:0.9.1'
}

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

 전 게시글과 달리 본 프로젝트에서 아직 정적 자원 제공 클래스는 작성하지 않았다. 아직 프론트 담당하는 분들과 협업하지 않아서 후에 수정할 예정이다.

 

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@AllArgsConstructor
public class Member extends BaseTimeEntity implements Serializable {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Setter
    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String sex;

    @Column(nullable = false)
    private int age;

    @Column(nullable = false)
    private String city;

    @Setter
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserRole role;

    public String getRoleKey(){
        return this.role.getKey();
    }

}

본 프로젝트에서 필요한 회원 정보 이력을 바탕으로 작성한 Member 클래스이다.

 

 

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    private final TokenUtils tokenUtils;
    private final UserDetailsService userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;
   
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        StaticResourceRequestMatcher staticResourceRequestMatcher = PathRequest.toStaticResources().atCommonLocations();
        return (web) -> web.ignoring().requestMatchers(staticResourceRequestMatcher);
    }

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

        http
                        .csrf().disable().authorizeRequests()
                        .anyRequest().permitAll()
                .and()
                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                        .formLogin()
                            .disable()
                            .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);


        return http.build();
    }

    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() throws Exception{
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
        customAuthenticationFilter.setFilterProcessesUrl("/user/login");
        customAuthenticationFilter.setAuthenticationSuccessHandler(customFormLoginSuccessHandler());
        customAuthenticationFilter.afterPropertiesSet();
        return customAuthenticationFilter;
    }

    @Bean
    public CustomFormLoginSuccessHandler customFormLoginSuccessHandler(){
        return new CustomFormLoginSuccessHandler(tokenUtils);
    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception{
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public CustomAuthenticationProvider customAuthenticationProvider(){
        return new CustomAuthenticationProvider((MemberDetailsServiceImpl) userDetailsService, bCryptPasswordEncoder());
    }

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

}

SpringSecurity에 대한 기본적인 설정 등을 추가해준다. SpringSecurity에 대한 설정 클래스에서는

  1. WebSecurityCustomizer 메소드를 통해 정적 자원들에 대해서는 Security를 적용하지 않음을 추가한다.
  2. filterChain 메소드를 통해 기본적인 security를 적용시켜준다.
  3. form 기반의 로그인을 비활성화한다. 

Bean으로 등록된 코드 중 아직 작성하지 않은 코드가 대부분이다. 아래에서 차례로 구현과정을 설명할 예정이다.

 

 

 

전 게시글에서도 언급을 했듯이 Spring은 현재 WebSecurityConfigurerAdapter를 Deprecated하여 지원하지 않는다.

필요한 config들은 모두 Bean으로 등록하여 사용하여야한다. 자세한 Document는 아래 링크를 참조하면 된다.

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

 

Spring Security without the WebSecurityConfigurerAdapter

<p>In Spring Security 5.7.0-M2 we <a href="https://github.com/spring-projects/spring-security/issues/10822">deprecated</a> the <code>WebSecurityConfigurerAdapter</code>, as we encourage users to move towards a component-based security configuration.</p> <p

spring.io

 

 

[ 1. 회원가입 ]

로그인 과정을 보기 전에 회원가입 로직을 살펴보자.

@PostMapping("/signUp")
public ResponseEntity<MemberDto> signUp(@RequestBody final SignUpDto signUpDto){
    boolean isPresent = memberService.findByEmail(signUpDto.getEmail()).isPresent();
    
    if(isPresent){
        return ResponseEntity.badRequest().build();
    } else{
        MemberDto dto = memberService.signUp(signUpDto);
        return ResponseEntity.ok(dto);
    }
}

 

회원가입을 위해 필요한 정보 API로 받아오는 SignUpDto로 변환해주고, 중복되는 메일이 존재하는지 확인한다.

만약 존재하는 메일이 있다면 badRequest를 반환하여 회원가입을 실패함을 알려주고, 중복되는 메일이 존재하지 않으면 회원가입을 성공적으로 진행해준다.  memberService, memberRepository는 DTO 스펙에 맞춰 구현해주면 된다. 어려운 부분이 아니므로 따로 구현 코드는 올리지 않았다.

 

Pig 1. 회원가입

Postman으로 확인해본 회원가입 결과이다. 

DB에 저장된 모습

Hibernate로 확인해본 쿼리문과 DB 결과 모습이다. (뒤에 role, sex 부분은 잘린 사진)

회원가입할 때는 password를 1234로 하였으나 DB에 저장할 때는 Spring Security에서 Bean으로 등록한 bCryptPassword를 통해 Encode한 후 저장해준다.

 

회원가입은 어렵지 않게 진행할 수 있고, 이제 로그인을 어떻게 진행하는지 알아보자.

 

 

 

 

[ 2. 로그인 요청 ]

사용자는 로그인 하기 위해 아이디와 비밀번호를 입력해서 로그인 요청을 하게 된다. 이번에 작성하는 예제에서는 로그인 API를 호출하고, Json으로 사용자의 아이디(이메일)와 비밀번호를 보내는 상황이다.

{
    "email" : "hongdong@gmail.com",
    "password" : "1234"
}

이런 형태로 요청을 할 것이다.

 

 

 

 

[ 3. UserPasswordAuthenticationToken 발급 ]

전송이 오면 AuthenticationFilter로 요청이 먼저 오게 되고, 아이디와 비밀번호를 기반으로 UserPasswordAuthenticationToken을 발급해주어야 한다. 프론트 단에서 유효성 검사를 하겠지만, 아래의 예제에서는 생략한다. 해당 Filter를 구현하면 아래와 같다.

@Slf4j
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public CustomAuthenticationFilter(final AuthenticationManager authenticationManager){
        super.setAuthenticationManager(authenticationManager);
    }

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

        final UsernamePasswordAuthenticationToken authRequest;

        try{
            final LoginUserDto loginUserDto = new ObjectMapper().readValue(request.getInputStream(), LoginUserDto.class);

            log.info("사용자가 로그인을 시도함 아이디 : " + loginUserDto.getEmail() + "  비밀번호 : " + loginUserDto.getPassword());

            authRequest = new UsernamePasswordAuthenticationToken(loginUserDto.getEmail(), loginUserDto.getPassword());
        } catch (IOException exception){
            throw new InputNotFoundException();
        }
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);

    }
}

로그인 요청 전송에는 위에서 작성한 것과 마찬가지로 아이디(이메일)와 비밀번호만 존재하므로, 이를 받아주는 DTO를 생성하여 받아준다. 그리고 이를 통해 UsernamePasswordAuthenticationToken을 발급해준다.

 

 

만약 아이디와 비밀번호가 제대로 전달되지 않았을 경우에는 예외 처리를 해주어야 하므로 InputNotFountException 클래스를 생성하여 처리한다.

public class InputNotFoundException extends RuntimeException{
    public InputNotFoundException(){
        super();
    }
}

 

 

이렇게 직접 작성한 Filter를 이제 적용시켜야 하므로 UsernamePasswordAuthenticationFilter 필터 이전에 적용시켜야 한다.

적용하는 코드는 제일 처음에 작성한 WebSecurityConfig에 있으니 확인하면 될 듯하다.

 

 

 

[ 4. UsernamePasswordToken을 AuthenticationManager에게 전달 ]

AuthenticationFilter는 생성한 UsernamePasswordToken을 AuthenticationManager에게 전달한다. AuthenticationManager은 실제로 인증을 처리할 여러 개의 AuthenticationProvider를 가지고 있다.

 

 

 

[ 5. UsernamePasswordToken을 AuthenticationProvider에게 전달 ]

AuthenticationManager는 전달 받은 UsernamePasswordToken을 순차적으로 AuthenticationProvider들에게 전달하여 실제 인증의 과정을 수행해야 하며, 실제 인증에 대한 부분은 authenticate 함수에 작성을 해주어야 한다. SpringSecurity에서는 Username으로 DB에서 데이터를 조회한 다음에, 비밀번호의 일치여부를 검사하는 방식으로 작동을 한다. 그렇기 때문에 먼저 UsernamePasswordToken 토큰으로부터 아이디를 조회해야 하고 그 코드는 아래와 같다.

 

@RequiredArgsConstructor
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final MemberDetailsServiceImpl memberDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;

        final String email = token.getName();
        
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

회원가입에서 언급했던 바와 같이 비밀번호는 bCryptPassword를 통해 Encode했으므로 비교할 때도 똑같이 인코딩을 적용하여 비교해주어야한다. 만약 비밀번호가 일치한다면 인증된 UsernamePasswordAuthenticationToken을 새로 발급하여 넘겨준다.

 

memberDetailsService에서 사용자를 조회하는 코드는 전에 작성했던 게시글에 존재하므로 생략한다. 아래 게시글에서 [5, 6, 7]번 과정에서 확인할 수 있다.

https://wonsjung.tistory.com/447

 

[SpringBoot] SpringBoot + SpringSecurity + JWT 구현하기

현대 웹 서비스에서는 토큰을 사용하여 사용자들의 인증 작업을 처리하는 것이 가장 좋은 방법이다. 본 프로젝트에서 요구하는 것 또한 JWT + Oauth2 구현이며 이번 글은 토큰 기반의 인증 시스템

wonsjung.tistory.com

 

 

 

[ 6. 인증 처리 후 인증된 토큰을 AuthenticationManager에게 반환 ]

이제 CustomAuthenticationProvider에서 UserDetailsService를 통해 조회한 정보와 입력받은 비밀번호가 일치하는지 확인하여, 일치한다면 인증된 토큰을 생성하여 반환해주어야 한다. DB에 저장된 사용자 비밀번호는 암호화가 되어있기 때문에, 입력으로부터 들어온 비밀번호를 PasswordEncoder를 통해 암호화하여 DB에서 조회한 사용자의 비밀번호와 매칭되는지 확인해주어야 한다. 만약 비밀번호가 매칭되지 않는 경우에는 BadCredentialsException을 발생시켜 처리해준다.

 

@RequiredArgsConstructor
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final MemberDetailsServiceImpl memberDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;

        final String email = token.getName();
        final String password = (String) token.getCredentials();

        final MyUserDetails userDetails = (MyUserDetails) memberDetailsService.loadUserByUsername(email);

        if(!bCryptPasswordEncoder.matches(password, userDetails.getPassword())){
            throw new BadCredentialsException(userDetails.getName() + "Invalid password");
        }

        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

위와 같이 작성한 CustomAuthenticationProvider를 Bean으로 등록해주어야 한다. 마찬가지로 WebSecurityConfig에 등록하며 코드는 위에 있으니 확인할 수 있다. 추가로 Provider를 등록하기 위해서 AuthenticationManager를 먼저 등록해주어야 하는데 WebSecurityConfigurerAdapter로 등록하는 방법은 현재 사용불가능하기 때문에 따로 등록해주어야 한다. 이것에 대한 코드도 위 WebSecurityConfig에 작성하였으니 확인할 수 있다.

 

 

 

[ 7. 인증된 UsernamePasswordAuthenticationToken을 AuthenticationFilter에게 전달 ]

AuthenticationProvider에서 인증이 완료된 UsernamePasswordAuthenticationToken을 AuthenticationFilter로 반환하고, 이 부분까지 성공적으로 진행하면 AuthenticationFilter에서는 LoginSuccessHandler로 전달한다.

 

CustomLoginSuccessHandler는 AuthenticationProvider를 통해 인증이 성골될 경우 처리되는데, TokenUtils에 대해서는 아래에서 작성한다. 또한, 인증과 관련해 자주 사용되는 상수는 아래의 AuthConstants 클래스에 정의해두었다.

@Slf4j
@RequiredArgsConstructor
public class CustomFormLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private final TokenUtils tokenUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        final Member member = ((MyUserDetails) authentication.getPrincipal()).getMember();

        System.out.println(member.getPassword());

        final Token token = tokenUtils.generateToken(member.getEmail(), "USER");
        response.addHeader(AuthConstants.AUTH_HEADER, AuthConstants.TOKEN_TYPE + " " + token.getAccessToken());
        response.addHeader(AuthConstants.REFRESH_HEADER, AuthConstants.TOKEN_TYPE + " " + token.getRefreshToken());

        PrintWriter writer = response.getWriter();
        ObjectMapper objectMapper = new ObjectMapper();

        String allToken = objectMapper.writeValueAsString(token);
        log.info("Token = {}", allToken);

        writer.println(objectMapper.writeValueAsString(token));
        writer.flush();
    }
    
}

 

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthConstants {

    public static final String AUTH_HEADER = "Auth";
    public static final String REFRESH_HEADER = "Refresh";
    public static final String TOKEN_TYPE = "BEARER";

}

CustomFormLoginSuccessHandler 역시 Bean으로 등록해주어야하고, 마찬가지로 위에서 작성한 WebSecurityConfig에서 코드를 확인할 수 있다.

 

 

 

[ 8. 인증된 UsernamePassword 토큰을 기반으로 JWT 발급 ]

전달받은 Authentication 정보를 활용해 Json Web Token을 생성해주어야 하는데, 토큰과 관련된 요청을 처리하는 TokenUtils를 아래와 같이 만들어 줄 수 있다.

@Log4j2
@Component
@PropertySource("classpath:application-oauth.yml")
public class TokenUtils {

    private static String secretKey;

    // Access 토큰 유효시간 15분
    static final long AccessTokenValidTime = 15 * 60 * 1000L;
    // Refresh Token
    static final long RefreshTokenValidTime = 2 * 24 * 60 * 1000L;


    @Value("${password}")
    public void setSecretKey(String path){
        secretKey = path;
    }

    @PostConstruct
    protected void init(){
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public Token generateToken(String uid, String role){

        Claims claims = Jwts.claims().setSubject(uid);
        claims.put("role", role);

        Date now = new Date();
        return new Token(
                Jwts.builder()
                        .setClaims(claims)
                        .setIssuedAt(now)
                        .setExpiration(new Date(now.getTime() + AccessTokenValidTime))
                        .signWith(SignatureAlgorithm.HS256, secretKey)
                        .compact(),
                Jwts.builder()
                        .setClaims(claims)
                        .setIssuedAt(now)
                        .setExpiration(new Date(now.getTime() + RefreshTokenValidTime))
                        .signWith(SignatureAlgorithm.HS256, secretKey)
                        .compact()
        );
    }

    public boolean isValidToken(String token){
        try{
            Jws<Claims> claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token);

            return claims.getBody()
                    .getExpiration()
                    .after(new Date());
        } catch (Exception e){
            return false;
        }
    }

    public String getUid(String token){
        return Jwts.parser().setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody().getSubject();
    }

}

인증이 성공되고 나면 CustomLoginSuccessHandler에서 Token이 생성되고 되고, 생성된 토큰을 반환하게 된다.

 

 

WebSecurityConfig에서 등록한 customAuthenticationFilter 적용 Url을 /user/login으로 지정하였다. 따라서 해당 url로 아이디와 비밀번호를 올바르게 전송하면 위와 같이 AccessToken과 RefreshToken을 발급받은 것을 확인할 수 있다.

 

 

동일한 정보를 다르게 입력하여 로그인 요청을 하게 되면

이런식으로 Unauthorized가 발생하는 것을 확인할 수 있다.

 

 

2. Spring Security 처리 과정


이제 토큰을 생성해주는 부분까지는 마무리를 하였고, 토큰을 발급받은 사용자만 원하는 로직을 처리할 수 있도록 해주어야 한다. 아래의 내용에서는 Interceptor를 활용해 유효한 토큰을 가진 사용자만 접근할 수 있도록 접근 제어를 해주고 있다.

 

 

[ 1. 유효한 토큰 검증을 위한 인터셉터 추가 ]

이 클래스는 토큰을 검증하도록 설정한 API에 대해 요청을 intercept하여 토큰의 유효성 검사를 진행한다. 유효성 검사에 실패하면 예외 API로 redirect를 시키고 있다.

@Slf4j
@RequiredArgsConstructor
public class JwtTokenInterceptor implements HandlerInterceptor {

    private final TokenUtils tokenUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Auth");

        if(token != null && tokenUtils.isValidToken(token)){
            return true;
        }
        response.sendRedirect("/error/unauthorized");
        return false;
    }
}

 

 

[ 2. 예외 처리 컨트롤러 추가 ]

토큰의 유효성 검증에 실패한 경우 아래의 /error/unauthorized API로 redirect된다.

@RestController
@RequestMapping(value = "/error")
public class ErrorController {

    @GetMapping(value = "/unauthorized")
    public ResponseEntity<Void> unauthorized() {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }

}

 

 

 

[ 3. 인터셉터 추가 및 패턴 적용 ]

작성한 인터셉터 클래스를 설정에 추가하고, 토큰의 유혀성을 검증할 API의 Path 패턴을 적용한다.

이번 예제에서는 회원 가입을 제외한 나머지 /user url에 대해 유효한 토큰을 헤더에 포함시켜 요청한 경우만 API를 호출 가능하도록 설정하였다.

@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final TokenUtils tokenUtils;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtTokenInterceptor())
                .excludePathPatterns("/user/signUp")
                .addPathPatterns("/user/**");
    }

    @Bean
    public FilterRegistrationBean<HeaderFilter> getFilterRegistrationBean(){
        FilterRegistrationBean<HeaderFilter> registrationBean = new FilterRegistrationBean<>(createHeaderFilter());
        registrationBean.setOrder(Integer.MIN_VALUE);
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }

    @Bean
    public HeaderFilter createHeaderFilter(){
        return new HeaderFilter();
    }

    @Bean
    public JwtTokenInterceptor jwtTokenInterceptor(){
        return new JwtTokenInterceptor(tokenUtils);
    }
}

 

 

 

[ 4. 사용자 API 추가 ]

@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/")
    public String hello(HttpServletRequest request, HttpServletResponse response){
        String token = request.getHeader("Auth");
        System.out.println("token = " + token);
        return "Hello !!";
    }

    @GetMapping("/information")
    public String test(){
        return "OK!!!!!!!";
    }
}

두 가지 예로 원하는 동작이 정상적으로 수행하는지 확인해보자.

@RequestMapping("/user")가 존재하기 때문에 위에 해당하는 url을 방문하기 위해서는 유효한 Token을 가지고 있는 사용자만 접근할 수 있다.

 

 

/user/ 에 토큰 없이 접근했을 때

/user/에 접근하려하였으나 헤더에 토큰 정보를 아무것도 담지 않았다. 따라서 결과는 401 Unauthorized를 반환받는다. 만약 위에서 발급받은 토큰을 헤더에 담아서 접근하면?

 

 

 

/user/ 에 토큰 정보를 헤더에 담아 요청하였을 때

토큰을 헤더에 담아 똑같은 경로에 요청을 하였더니 200 Ok와 함께 원하던 Hello !!를 반환하는 것을 확인할 수 있다.

 

 

Log 정보와 Intellij 출력도 원하는 대로 정상 출력하는 것을 확인할 수 있다.

 

 

 

 

 

 

 

두 번째로 /user/information을 확인해보자.

동일하게 401이 발생한다.

 

 

 

토큰을 담아 전송하였더니 200이 발생하며 성공적으로 OK!!!!!!가 반환되는 것을 알 수 있다.

 

 

 

 

 

3. Spring Security 처리 과정 요약


1. 사용자가 아이디 비밀번호로 로그인 요청 (Request)

2. AuthenticationFilter에서 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에게 전달

3. AuthenticationManager는 등록된 AuthenticationProvider(s)을 조회하여 인증 요구

4. AuthenticationProvider UserDetailsService를 통해 입력받은 아이디에 대한 사용자 정보를 DB에서 조회함

5. 입력받은 비밀번호를 암호화하여 DB의 비밀번호와 매칭되는 경우 인증이 성공된 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager로 반환

6. AuthenticationManager UsernamePasswordAuthenticationToken AuthenticationFilter로 전달

7. AuthenticationFilter는 전달 받은 UsernamePasswordAuthenticationToken LoginSuccessHandler로 전송하고, 토큰을 response의 헤더에 추가하여 반환

 

 

 

여기까지 SpringBoot + SpringSecurity + JWT를 이용한 회원가입 / 로그인 구현을 소개했다.

다음 글은 JWT에서 Access Token 만료와 Refresh를 통한 Access Token 재발급 과정이 어떻게 수행되는지 작성할 예정이다.

728x90
반응형