본문 바로가기

STUDY/Spring

Spring Boot | JWT를 사용하는 Spring Security 로그인 ( REST API )

출처: https://www.toptal.com/java/rest-security-with-jwt-spring-security-and-java

1. pom.xml에 의존성 추가

스프링 시큐리티 추가

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

테스트 코드 작성을 위해서는 spring-security-test 도 필요하다.

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <scope>test</scope>
</dependency>

 

2. security관련 package생성 후 config클래스 작성

WebSecurityConfigureAdapter를 상속받아 클래스를 생성

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}

 

  • security를 추가하면 자동으로 기본 로그인 페이지가 생성되는데, 그 설정을 취소하고 csrf사용도 취소함
  • signIn과 signUp(로그인과 회원가입)에 대한 요청은 모두 허용, 그 외 요청은 인증된 회원만 접근 가능하도록 설정
  • exceptionHandling과 addFilterBefore에 대한 클래스들은 아래에서 작성할 것임
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.httpBasic().disable()	// security에서 기본으로 생성하는 login페이지 사용 안 함 
  .csrf().disable()	// csrf 사용 안 함 == REST API 사용하기 때문에  
  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)	// JWT인증사용하므로 세션 사용  함
  .and()
  	.authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크
  		.antMatchers("/*/signIn", "/*/signUp").permitAll() // 가입 및 인증 주소는 누구나 접근가능
  		.anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
  .and()
  	.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
  .and()
  	.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}

 

+ swagger를 사용할 경우 추가해주기

@Override
public void configure(WebSecurity web) throws Exception {
  web.ignoring().antMatchers("/v2/api-docs",
                              "/configuration/ui",
                              "/swagger-resources/**",
                              "/configuration/security",
                              "/swagger-ui.html",
                              "/webjars/**");
}

 

+ customUserDetails등록

직접 UserDetailsService를 커스텀하게된다면 꼭 등록해주기!

// SecurityConfig.java

@Autowired
private CustomUserDetailService customUserDetailService;

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth.userDetailsService(customUserDetailService);
}

CustomUserDetailService클래스도 작성해주고...

@Service
public class CustomUserDetailService implements UserDetailsService {
	
	@Autowired
	UserMapper userMapper;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userMapper.getUserInfo(username);
		if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
        } else {
            return user;
        }
	}
}

 

3. jwt토큰 생성 및 유효성 검증 관련 컴포넌트 클래스 작성

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {	// JWT토큰 생성 및 유효성을 검증하는 컴포넌트 
	
}

아래 코드와 같이 작성해주면 되는데

SECRET_KEY는 properties파일에서 불러오는 값

createToken메서드를 통해 JWT 토큰이 생성되고, 이 토큰값을 통해 유저 정보에 접근할 수 있다.

인자값 중 userPk는 user를 식별할 수 있는 값이며, roles는 말그대로 role인데, 이는 userDetails를 상속받아 userDto를 생성해보면 무슨 값을 넘겨주면 되는지 이해할 수 있다. USER, ADMIN같은 값임.

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {	// JWT토큰 생성 및 유효성을 검증하는 컴포넌트 
	
    @Value("spring.jwt.secret")
    private String SECRET_KEY;

    private long tokenValidMilisecond = 1000L * 60 * 60; // 1시간만 토큰 유효

    private final UserDetailsService userDetailsService;

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

    // Jwt 토큰 생성
    public String createToken(String userPk, Collection<? extends GrantedAuthority> roles) {
      Claims claims = Jwts.claims().setSubject(userPk);
      claims.put("roles", roles);
      Date now = new Date();
      return Jwts.builder()
            .setClaims(claims) // 데이터
            .setIssuedAt(now) // 토큰 발행일자
            .setExpiration(new Date(now.getTime() + tokenValidMilisecond)) // set Expire Time
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 암호화 알고리즘, secret값 세팅
            .compact();
    }

}

 

만약 CustomUserDetailService했다면 당연히 JwtProvider에서도 사용해야 한다... Autowired 어노테이션 잊지말기...

 

@Autowired
private CustomUserDetailService customUserDetailsService;

4. CustomAuthenticationEntryPoint클래스 작성

권한이 없을 경우 알려주는.. 401 에러

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		// TODO Auto-generated method stub
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied");
	}

}

 

 

5. 로그인 해보기!

컨트롤러

/*** 유저 로그인 ***/
@ApiOperation(value="유저 로그인")
@PostMapping(value="/users/signIn")
public ResponseEntity<?> userSignIn(@RequestBody User userInfo) {

  Map<String, Object> resultMap = new HashMap<String, Object>();

  User loginUser = userService.userSignIn(userInfo);
  if(loginUser == null) {
    resultMap.put("resCd", "9999");
    resultMap.put("resMsg", "로그인 실패");
  }else {
    resultMap.put("token", loginUser.getU_token());
    resultMap.put("resCd", "0000");
    resultMap.put("resMsg", "로그인 성공");
  }
  
  return new ResponseEntity<>(resultMap, HttpStatus.OK);
}

서비스

public User userSignIn(User userInfo) {
  User user = userMapper.userSignIn(userInfo.getU_email());
  if(!passwordEncoder.matches(userInfo.getU_password(), user.getU_password())) {
  	return null;
  }else {
    String token = jwtTokenProvider.createToken(user.getU_email(), user.getAuthorities());
    user.setU_token(token);
  	return user;
  }
}

 

포스트맨으로 실행해본 결과 토큰값이 잘 도착한다..