시큐리티의 기본적인 설정이 끝난 후, 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 |
댓글