본문 바로가기
개발 일기/spring

Spring Security 설정 (JWT 1)

by URMOO 2022. 8. 31.
반응형

시큐리티의 기본적인 설정이 끝난 후, JWT 설정을 해주었다.

로그인 기능이 있어야 하므로, 간단한 유저테이블과 권한 테이블을 만든 후 관련 설정을 시작하였다.

이 포스팅에서는 로그인 api를 요청할 경우, 토큰을 생성해서 발급하는 과정까지 담아보았다.

본 포스팅의 코드는 여기서 확인 가능합니다😉

1. 기본설정

1. build.gradle

spring boot 버전이 올라가면서, 마찬가지로 jwt에 관한 설정도 조금씩 변경되었다.

implementation group: 'io.jsonwebtoken', name: 'jjwt-api', 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'

2. 코드

우선, 컨트롤러를 생성 후, 로그인 관련 서비스를 만들어주었다.

Controller.java

@RestController
@RequiredArgsConstructor
public class Controller {

  private final AuthService authService;

  @PostMapping("/login")
  public TokenDto login(@RequestBody LoginDto loginDto){
    return authService.login(loginDto);
  }

}

AuthService.java

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {

  private final AuthenticationManager authenticationManager;
  private final TokenProvider tokenProvider;

  public TokenDto login(LoginDto loginDto) {

    //임시 인증 객체 생성
    UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(loginDto.getName(), loginDto.getPassword());

    //인증 객체 -> AuthenticationProvider에서 인증 작업
    Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
    //인증이 오류없이 완료됐다면 SecurityContext에 해당 인증 객체 등록
    SecurityContextHolder.getContext().setAuthentication(authentication);

    //토큰으로 변환 시켜 토큰 관련 객체 리턴
    return tokenProvider.createToken(authentication);
  }
}

authenticationManager를 통해 인증객체를 처리하는데, 나는 CustomAuthenticationProvider를 만들어서 그쪽에서 인증객체를 만들어주었다.

CustomAuthenticationProvider.java

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

  private final UserRepository userRepository;

  @Override
  public Authentication authenticate(Authentication authentication){
    String name = authentication.getName();
    String password = authentication.getCredentials().toString();

    User user = userRepository.findByName(name)
        .orElseThrow(NullPointerException::new);

    if (!user.isMatchPassword(password)) {
      //패스워드가 틀릴경우 exception 발생 로직
    }

    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(
        user.getAuthorities().getRole()); //"ROLE_USER" 와 같은 권한

    //임시 인증객체를 진짜 인증객체로 생성
    return new UsernamePasswordAuthenticationToken(name, password,
        new ArrayList<>(Collections.singletonList(simpleGrantedAuthority)));
  }

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

AuthenticationProvider를 implement 하고, 관련로직들을 만들어주었다. 지금 코드의 경우 user와 authority를 1:n으로 만들어 SimpleGrantedAuthority를 단일객체로 생성하고, List로 변환을 해주었는데, 만약 n:m 관계라면 user가 가지고 있는 모든 권한들을 리스트로 만들어 넘겨주어야한다.

exception관련 개발은.. 넘 만들기 귀찮아서 따로 만들지는 않았다...

AuthenticationProvider를 만들기만해서 바로 작동이 되는 것은 아니다. AuthenticationManager Bean을 만들어 내 Provider를 쓴다고 설정을 해주어야한다. 해당 설정은 SecurityConfig에 해주었다.

SecurityConfig.java

//...
public class SecurityConfig {

  private final AuthenticationProvider authenticationProvider;
    //other codes... 
  @Bean
  public AuthenticationManager authenticationManagerBean() {
    List<AuthenticationProvider> authenticationProviderList = new ArrayList<>();
    authenticationProviderList.add(authenticationProvider);
    ProviderManager authenticationManager = new ProviderManager(authenticationProviderList);
    authenticationManager.setAuthenticationEventPublisher(defaultAuthenticationEventPublisher());
    return authenticationManager;
  }

  @Bean
  DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher() {
    return new DefaultAuthenticationEventPublisher();
  }

}

여기까지 완료하고 제대로된 로그인 값을 입력하면 Authentication 객체가 생성될 것이다. 이제 이를 사용하여 JWT를 발생하고, 필요 값을 응답해주어야한다. 앞서 AuthService의 login return 부분에서 createToken 함수를 사용하는 것을 확인할 수 있다. 해당 로직을 위해 TokenProvider를 만들어주었다.

TokenProvider.java

@Slf4j
@Component
@RequiredArgsConstructor
public class TokenProvider implements InitializingBean {

    //토큰 생성시 필요한 secretKey
  private String secretKey = "..키 값 넣기";
  //토큰 만료 시간
  private long tokenValidityInSeconds = 86400000; //60 * 60 * 24 * 1000

  private Key key;

  @Override
  public void afterPropertiesSet() {
    byte[] keyBytes = Decoders.BASE64.decode(secretKey);
    this.key = Keys.hmacShaKeyFor(keyBytes);
  }

  public TokenDto createToken(Authentication authentication) {
    //authentication에 있는 권한 목록을 "ROLER_USER,ROLE_ADMIN" 형태로 변환
    String authorities = authentication.getAuthorities()
        .stream()
        .map(GrantedAuthority::getAuthority)
        .collect(Collectors.joining(","));

     //토큰 만료 시간 설정
    long now = (new Date()).getTime();
    Date validity = new Date(now + tokenValidityInSeconds);

    Map<String, Object> headerMap = new HashMap<>();
    headerMap.put("typ", "JWT");
    //Jwts.builder()로 토큰 생성하고, TokenDto로 리턴
    return new TokenDto(Jwts.builder()
        .signWith(key, SignatureAlgorithm.HS512)
        .setHeader(headerMap)
        .setSubject("security-test")
        .claim("user_name", authentication.getName())
        .claim("Bearer", authorities)
        .setExpiration(validity)
        .compact(), validity.toInstant().toEpochMilli());
  }
}

secretKey와 tokenValidityInSeconds의 경우 profile에 설정하여 값을 가져오는게 맞다고 생각하지만.. 해당 코드에선 그냥 값을 지정해주었다.

tokenValidityInSeconds는 밀리초 단위를 입력해야하므로 원하는 시간에 1000을 곱해주어야한다.

위의 코드를 사용하여 TokenDto를 만들어 준 후, 로그인을 하면 아래와 같은 응답이 출력된다.

{
    "accessToken": "토큰 값",
    "tokenType": "Bearer",
    "expiresIn": 1662001354277
}

accessToken에 값의 경우 길어서 생략했는데, 이를 jwt.io에 복사해 확인해 보면, 토큰을 파싱할 경우 생성되는 값들을 확인 할 수 있다.

 

 

다음 포스팅에서는, 토큰과 함께 api를 호출하고 권한별 처리하는 방법을 알아보자.

반응형

'개발 일기 > spring' 카테고리의 다른 글

Spring Security 설정 (Hierarchy)  (0) 2022.09.02
Spring Security 설정 (JWT 2)  (0) 2022.08.31
Spring Security 설정 (기본)  (2) 2022.08.31
Spring 에러 발생 시, Slack 알람 만들기  (0) 2022.08.11
특정 API만 Swagger에 노출  (0) 2022.08.11

댓글