본문 바로가기
프레임워크/스프링

[Spring boot] MYSQL + JPA + JWT + Spring Security 를 사용해서 로그인 구현

by Yikanghee 2022. 4. 17.

MYSQL + JPA + JWT + Spring Security 를 사용해서 로그인 구현하기 위해 JWT구현 로직을 공부했다

  • JWT가 유효한 토큰인지 인증하기 위한 Filter
@RequiredArgsConstructor
//자동으로 Constructor를 만들어줌
public class JwtAutenticationFilter extends GenericFilterBean {
	
	private final JwtTokenProvider jwtTokenProvider;

	// Request로 들어오는 Jwt Token의 유효성을 검증하는 filter를 filterChain에 등록
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
		throws IOException,ServletException{
	
	// 헤더에서 JWT를 받아옴
	String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
	
	//유효한 토큰인지 확인
	if(token != null && jwtTokenProvider.validateToken(token)) {
	
		//토큰이 유효하면 토큰으로부터 유저 정보를 받아옴
		Authentication authentication = jwtTokenProvider.getAuthentication(token);
		//SecurityContext에 Authentication 객체를 저장함
		SecurityContextHolder.getContext().setAuthentication(authentication);
	}
	chain.doFilter(request, response);
}
  • JWT Token을 생성, 인증, 권한 부여, 유효성 검사, PK 추출 기능을 제공하는 클래스
@RequiredArgsConstructor
//자동으로 Constructor를 생성
@Component
//빈 직접 등록
public class JwtTokenProvider {
	
	private String secretKey = "mobee";
	
	//토큰 유효시간 30분
	private long tokenValidTime = 30 * 60 * 1000L;

	private final UserDetailsService userDetatilsService;

	// 객체 초기화, secretKey를 Base64로 인코딩
	protected void init() {
	
		secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
	
	//JWT 토큰 생성
	public String createToken(String nickname, String profileImgUrl, List<String> roles){
	
	Claims claims = Jwts.claims().setSubject(nickname);
	//JWT payload에 저장되는 정보단위
	claims.put("nickname", nickname);
	claims.put("profileImgUrl", StringUtils.hasText(profileImgUrl) ? prifileImgUrl : "");
	claims.put("roles", roles);
	// 정보는 key / value 쌍으로 저장된다
	
	Date now = new Date();
	
	return Jwts.builder()
		.setClaims(claims) //데이터 정보저장
		.setIssuedAt(now) //토큰 발행 시간
		.setExpiration(new Date(now.getTime() + tokenVaildTime)) // setExpire Time
		.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과 signature 에 들어갈 secret값 세팅
		.compact(); //토큰 생성
}

	//인증 성공시 SecurityContextHolder에 저장할 Authentication 객체 생성
	public Authentication getAuthentication(String token) {
		UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
		return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

	// 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("Authorization");
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}
  • Spring Security에서 사용자의 정보를 담는 인터페이스
@RequiredArgsConstructor
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    private final AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String nickname) throws UsernameNotFoundException {
        Account account = accountRepository.findByNickname(nickname)
                .orElseThrow(() -> new UsernameNotFoundException("Can't find " + nickname));

        return new UserDetailsImpl(account);
    }
  • DB에서 유저 정보를 직접 가져오는 인터페이스
public class UserDetailsImpl implements UserDetails {

    private final Account account;

    public UserDetailsImpl(Account account) {
        this.account = account;
    }

    public Account getAccount() {
        return account;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return account.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role))
                .collect(Collectors.toList());
    }

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

    @Override
    public String getUsername() {
        return account.getNickname();
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return false;
    }
  • 스프링 시큐리티의 웹 보안 기능 초기화 및 설정
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함 Spring Security Filter Chain 을 사용한다는 것
//@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    @Bean // 패스워드 인코딩
    public BCryptPasswordEncoder encodePassword(){
        return new BCryptPasswordEncoder();
    }

/*    AuthenticationManager 를 이용하여, 원하는 시점에 로그인이 될 수 있도록 바꿔줌,
    먼저, AuthenticationManager 를 외부에서 사용 하기 위해, AuthenticationManagerBean 을 이용하여 
		Sprint Securtiy 밖으로 AuthenticationManager 빼 내야 한다.*/
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {

        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(ImmutableList.of("*")); // 스프링부트 2.4부터 변경?
        configuration.setAllowedMethods(ImmutableList.of("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(ImmutableList.of("Authorization", "TOKEN_ID", "X-Requested-With", "Content-Type", "Content-Length", "Cache-Control"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().configurationSource(corsConfigurationSource()); 
	// http.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
        http
                .httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
                .headers().frameOptions().disable().and()
                .csrf().disable() // csrf 보안 토큰 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
                .and()
                .authorizeRequests() // 요청에 대한 사용권한 체크
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST, "/api/posts/**").authenticated()
                .antMatchers(HttpMethod.PUT, "/api/posts/**").authenticated()
                .antMatchers(HttpMethod.DELETE, "/api/posts/**").authenticated()
                .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
                .and()
                .addFilterBefore(new JwtAutenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    }

댓글