[CowAPI] 9. Spring Boot JWT, Role with Spring Security

준돌·2022년 6월 7일
1

오늘의 Cow

목록 보기
14/45
post-thumbnail

9. Spring Boot JWT, Role with Spring Security


요구사항

  • JWT를 통해 사용자를 인증 및 인가를 처리하기 위해 SpringBoot Security를 사용합니다.
  • Role을 Admin과 User로 나눕니다.

😎 [CowAPI]8-2. SpringBoot Security

  • Role를 스스로 구현해 보았지만 공식 문서와 블로그를 찾으며 구조가 잘못되었다는 것을 발견하게 되었습니다.
  • 최대한 CowAPI의 DB를 수정하지 않고 동작할 수 있도록 구현해보았습니다.

SpringBoot Security


  • SpringBoot doc에서 확인할 수 있는 아키텍쳐 이미지 입니다.
  • 여기서 필터들의 우선 순위와 종류를 알 수 있습니다.

😎 CowAPI Security


Architecture

  • http request : http method를 통한 요청입니다.
  • Config : 유저 인증을 처리하는 UsernamePasswordFilter 전에 필터를 추가합니다.
  • Filter : 필터링을 담당합니다.
  • Converter : http request를 authentication으로 변환합니다.
  • Manager : 인증 및 권한을 관리합니다.
  • Provider : JWT 토큰을 발급하고 Role를 부여합니다.
  • Service : UserDetail를 반환합니다.
  • User : UserDetail로 유저의 정보를 담는 Entity입니다.

😎 코드 리뷰


Gradle.build

dependencies {
	compile "org.springframework.boot:spring-boot-starter-security"
    
  	implementation group: 'io.jsonwebtoken', name:'jjwt-impl', version:'0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
  • dependency를 추가합니다.

SecurityConfig 코드

  • SecurityConfig 코드는 여기서 확인할 수 있습니다.

1. request -> Config

public class UserAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
    ...

    @Override
    public void configure(HttpSecurity httpSecurity) {
        UserAuthenticationFilter userAuthenticationFilter = new UserAuthenticationFilter(userAuthenticationConverter, userAuthenticationManager);
        httpSecurity.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
  • http request를 받고 유저 인증을 처리하는 UsernamePasswordFilter 전에 커스텀한 필터를 추가합니다.

2. Config -> Filter

public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
    ...

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {

        // UserConverter 를 통해 http request -> authentication
        UserAuthentication authentication = (UserAuthentication) userAuthenticationConverter.convert((HttpServletRequest) servletRequest);
        
        // UserManager 를 통해 만료 확인
        // UserManager 를 통해 권한 및 jwt 얻기
        if(userAuthenticationManager.isCredentialsNonExpired(authentication)) authentication = (UserAuthentication) userAuthenticationManager.authenticate(authentication);
        else throw new ResponseStatusException(HttpStatus.FORBIDDEN, "만료된 JWT 토큰 입니다.");

        // SecurityContext 에 저장
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 다음 Filter 를 실행하기 위한 코드. 마지막 필터라면 필터 실행 후 리소스를 반환한다.
        filterChain.doFilter(servletRequest, servletResponse);
    }
}
  • 필터링을 적용하고 다음 필터를 진행합니다.

3. Filter <-> Converter

public class UserAuthenticationConverter implements AuthenticationConverter {
	
    ...
    
    @Override
    public Authentication convert(HttpServletRequest request) {

        // bearer ... -> header token
        String headerToken = getTokenFromRequest(request);

        // header token -> user email
        String userEmail = getUserEmailFromToken(headerToken);

        // generate default Token
        UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(userEmail, headerToken);

        // new user authentication
        return UserAuthentication.builder()
                .userToken(userToken)
                .build();
    }
    
    ...
    
}
  • Http request를 authentication으로 변환합니다.

4. Filter -> Manager

public class UserAuthenticationManager implements AuthenticationProvider {
	
    ...
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UserAuthentication userAuthentication = (UserAuthentication) authentication;
        userAuthenticationProvider.setUserAuthenticationToken(userAuthentication);
        userAuthenticationProvider.setUserAuthenticationRole(userAuthentication);
        userAuthenticationProvider.setUserAuthenticationUserDetail(userAuthentication);

        if(!validateToken(userAuthentication)) throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, "JWT 토큰 오류");
        if(!supports(userAuthentication.getClass())) throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, "Not Support Object");
        return userAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return Authentication.class.isAssignableFrom(authentication);
    }
    
    ...
    
}
  • 사용자의 인증 및 역할을 관리합니다.

5. Manager -> Provider

public class UserAuthenticationProvider {

	...

    public void setUserAuthenticationUserDetail(UserAuthentication userAuthentication) {
        UserDetails userDetails = userAuthenticationService.loadUserByUsername(userAuthentication.getPrincipal().toString());
        userAuthentication.getUserToken().setDetails(userDetails);
    }

    public void setUserAuthenticationToken (UserAuthentication userAuthentication) {
        String jwtToken = getJwtToken(userAuthentication);
        userAuthentication.setCredential(jwtToken);
    }

    public void setUserAuthenticationRole (UserAuthentication userAuthentication) {
        UserDetails userDetails = userAuthenticationService.loadUserByUsername(userAuthentication.getPrincipal().toString());
        userAuthentication.setAuthorities(userDetails.getAuthorities());
    }
    
    ...
    
}
  • JWT 토큰을 발급하고 Role를 부여합니다.

6. Provider -> Service

public class UserAuthenticationService implements UserDetailsService {

	...
    
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 사용자 입니다."));
        if(user.getAdmin()) user.setAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN")));
        else user.setAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")));
        return user;
    }
    
    ...
    
}
  • Service : UserDetail를 반환합니다.

7. Service -> User

public class User implements UserDetails {
    @Id
    private String email;
    
    ...
    
}
  • 유저 정보를 갖는 UserDetails인 User Entity입니다.

자세한 코드는 여기 에서 확인하실 수 있습니다.


😎 개발을 진행하면서


  • 현재 CowAPI의 DB에 적용될 수 있도록 JWT와 Role을 구현했습니다.
  • Security를 처음 사용하기도 하고 스스로 구현하기 때문에 시간은 많이 걸렸습니다.
  • 하지만, Security 동작 원리나 구조에 대해서 많은 것을 알아갈 수 있었습니다.
  • 추후에 CORS와 OAuth를 적용할 예정입니다.

profile
눈 내리는 겨울이 좋아!

0개의 댓글