9. Spring Boot JWT, Role with Spring Security
- JWT를 통해 사용자를 인증 및 인가를 처리하기 위해 SpringBoot Security를 사용합니다.
- Role을 Admin과 User로 나눕니다.
- Role를 스스로 구현해 보았지만 공식 문서와 블로그를 찾으며 구조가 잘못되었다는 것을 발견하게 되었습니다.
- 최대한 CowAPI의 DB를 수정하지 않고 동작할 수 있도록 구현해보았습니다.
SpringBoot Security
😎 CowAPI Security
Architecture
- http request : http method를 통한 요청입니다.
- Config : 유저 인증을 처리하는 UsernamePasswordFilter 전에 필터를 추가합니다.
- Filter : 필터링을 담당합니다.
- Converter : http request를 authentication으로 변환합니다.
- Manager : 인증 및 권한을 관리합니다.
- Provider : JWT 토큰을 발급하고 Role를 부여합니다.
- Service : UserDetail를 반환합니다.
- User : UserDetail로 유저의 정보를 담는 Entity입니다.
😎 코드 리뷰
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'
}
- 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 {
UserAuthentication authentication = (UserAuthentication) userAuthenticationConverter.convert((HttpServletRequest) servletRequest);
if(userAuthenticationManager.isCredentialsNonExpired(authentication)) authentication = (UserAuthentication) userAuthenticationManager.authenticate(authentication);
else throw new ResponseStatusException(HttpStatus.FORBIDDEN, "만료된 JWT 토큰 입니다.");
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(servletRequest, servletResponse);
}
}
3. Filter <-> Converter
public class UserAuthenticationConverter implements AuthenticationConverter {
...
@Override
public Authentication convert(HttpServletRequest request) {
String headerToken = getTokenFromRequest(request);
String userEmail = getUserEmailFromToken(headerToken);
UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(userEmail, headerToken);
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를 적용할 예정입니다.