로그인에 성공하면 JWT로 토큰을 발행하도록 할 것이다. 이번 포스팅 샘플 프로젝트는 Kakao 로그인 API를 이용해 로그인 시스템을 구현하는데, 이럴때는 비밀번호가 있지 않고 USER의 ID만 존재한다.
우선 토큰발생하는 클래스를 만들어보자.
JwtManager 클래스를 만든다.(이전 포스팅에 자세히 설명되어 있음)
public class JwtManager {
// 30분
private long ACCESS_TOKEN_VALIDATiON_SECOND = 60 * 30;
// 1개월
private long REFRESH_TOKEN_VALIDATiON_SECOND = 60 * 60 * 24 * 30;
private String secretKey;
public JwtManager(String secretKey) {
this.secretKey = secretKey;
}
public JwtManager(
String secretKey
, Long accessTokenValidationSecond
, Long refreshTokenValidationSecond
) {
this.secretKey = secretKey;
this.ACCESS_TOKEN_VALIDATiON_SECOND = accessTokenValidationSecond;
this.REFRESH_TOKEN_VALIDATiON_SECOND = refreshTokenValidationSecond;
}
private Key getSigninKey(String secretKey) {
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
// 토큰 해석
public Claims validTokenAndReturnBody(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSigninKey(secretKey))
.build()
.parseClaimsJws(token)
.getBody();
} catch(ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
e.printStackTrace();
throw new InvalidParameterException("유효하지 않은 토큰입니다");
}
}
// 유저id 조회
public String getName(String token) {
return validTokenAndReturnBody(token).get("username", String.class);
}
// 토큰 만료
private Boolean isTokenExpired(String token){
Date expiration = validTokenAndReturnBody(token).getExpiration();
return expiration.before(new Date());
}
public String generateAccessToken(String username) {
return doGenerateToken(username, ACCESS_TOKEN_VALIDATiON_SECOND * 1000l);
}
public String generateRefreshToken(String username) {
// return doGenerateToken("refresh-" + username, REFRESH_TOKEN_VALIDATiON_SECOND * 1000l);
return doGenerateToken(username, REFRESH_TOKEN_VALIDATiON_SECOND * 1000l);
}
public Long getValidationAccessTokenTime(){
return ACCESS_TOKEN_VALIDATiON_SECOND;
}
private String doGenerateToken(String username, Long expireTime) {
Claims claims = Jwts.claims();
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
.signWith(getSigninKey(secretKey), SignatureAlgorithm.HS256)
.compact();
}
}
해당 클래스를 빈으로 등록한다.
...
@Bean
public JwtManager jwtManager() {
String secretKey = "[sercetKey 입력]";
Long accessTokenExpireSecond = 60 * 60 * 24; // 1일
Long refreshTokenExpireSecond = 60 * 60 * 24 * 30; // 1개월
return new JwtManager(secretKey, accessTokenExpireSecond, refreshTokenExpireSecond);
}
...
이제 토큰을 전달할 Object를 생성한다
@Getter
public class TokenDto {
@NonNull
private String accessToken;
@NonNull
private String refreshToken;
@NonNull
private Long expireSec; // 만료시간(second)
public TokenDto(@NonNull String accessToken, @NonNull String refreshToken,
@NonNull Long expireSec) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expireSec = expireSec;
}
}
토큰을 생성관리하는 서비스를 만들었다
@Service
@RequiredArgsConstructor
public class JwtService {
private final JwtManager jwtManager;
// 토큰 발급
public TokenDto createToken(String key) {
String accessToken = jwtManager.generateAccessToken(key);
String refreshToken = jwtManager.generateRefreshToken(key);
return new TokenDto(
accessToken
, refreshToken
, jwtManager.getValidationAccessTokenTime());
}
// refresh 토큰 갱신
public TokenDto createTokenByrefreshToken(String refreshToken) {
String key = jwtManager.getName(refreshToken);
String accessToken = jwtManager.generateAccessToken(key);
String refreshTokenNew = jwtManager.generateRefreshToken(key);
return new TokenDto(
accessToken
, refreshTokenNew
, jwtManager.getValidationAccessTokenTime());
}
}
그럼 이제 로그인 할때 토큰을 발행해 보자.
@Service
@RequiredArgsConstructor
public UserService {
private final JwtService jwtService;
...
// 로그인 & 토큰발행
public ResponseToken login(Long userId) {
return new ResponseToken(jwtService.createToken(userId.toString()));
}
...
리턴할때의 ResponseToken 파일은 다음과 같다.
@Getter
public class ResponseToken {
@NonNull
private String accessToken;
@NonNull
private String refreshToken;
@NonNull
private Long expireSec;
public ResponseToken(TokenDto tokenDto) {
this.accessToken = tokenDto.getAccessToken();
this.refreshToken = tokenDto.getRefreshToken();
this.expireSec = tokenDto.getExpireSec();
}
}
호출하고 나면 다음과 같은 결과를 받는다.
이제 중요한 Filter를 거는 것이다. 다음처럼 설정한다.
@Configuration
@RequiredArgsConstructor
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtManager jwtManager;
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()// rest api이므로 csrf 보안이 필요없으므로 비활성화
.httpBasic().disable() // rest api 이므로 로그인폼으로 이동 비활성화
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션필요없음
.and()
.authorizeRequests().antMatchers(whitelistedUriPatterns()).permitAll()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtManager) // jwt 로 접근허용 필터 생성
, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling() // exception이 날 경우 catch하여 result form 일관하게 만듬(필요없음 제거)
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
;
}
private String[] whitelistedUriPatterns() {
return new String[]{
"/health",
"/oauth2/authorization/**",
"/swagger*/**", "/webjars*/**", "/v2/api-docs",
"/user/login/**",
};
}
}
addFilterBefore 와 authenticationEntryPoint를 등록한다.
filter는 접근하는 것에 대해 필터하는 역할로 JwtAuthenticationFilter를 구현하여 등록했다. JwtAuthenticationFilter 파일을 보자.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtManager jwtManager;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 헤더에서 로드
HttpServletRequest requestServlet = (HttpServletRequest) request;
String authorization = requestServlet.getHeader("Authorization");
// 유효한지 확인
if(StringUtils.isEmpty(authorization) == false) {
String accessToken = authorization.replace("Bearer ", "");
String name = jwtManager.getName(accessToken);
// username이 나오지 않는다면 잘못된 토큰이 만들어진 것이므로 에러를 리턴한다
if(StringUtils.isEmpty(name) == true) {
// error
throw new InvalidParameterException("유효하지 않은 토큰입니다");
}
// 여기서는 외부 소셜로그인을 통해 가입한 것이기 때문에 비밀번호 관리를 하지 않지만,
// 만약 자체 로그인 시스템이 있다면 DB조회하여 비밀번호를 넣어주어야 한다.
Authentication authentication = new UsernamePasswordAuthenticationToken(
name,
"password"
, getAuthorities());
// 접근 허가
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
// 권한을 설정하는 곳인데, 추가하고 싶다면 다음 2가지중 하나를 해야한다
// 1) username을 기반으로 DB에 조회하여 ROLE_ 을 확인한다
// 2) token을 만들때 ROLE_을 넣는다.
private Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
return grantedAuthorityList;
}
}
이제 유효하지 않은 토큰이 올 때마다 Exception이 발생하게 두었다.
OncePerRequestFilter 를 상속받아 구현했는데, 처음엔 GenericFilterBean를 구현했는데 필터를 여러번 호출하는게 보여 1 Reuqest당 1번만 호출하는 OncePerRequestFilter로 교체했다.
이제 Exception이 발생한 경우 대비한 결과 유형을 설정한 CustomAuthenticationEntryPoint 파일을 보자.
// 토큰 유효성 실패 시, Exception 결과를 정해진 form으로 리턴
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
System.out.println("======================= token error");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
try (OutputStream os = response.getOutputStream()) {
HashMap fail = new HashMap();
fail.put("errMsg", "유효하지 않은 토큰입니다");
fail.put("errCd", "ERROR");
objectMapper.writeValue(os, fail);
os.flush();
}
}
}
설정한대로 잘 오는지 확인
output form 설정을 비슷한걸로 이전에 포스팅한게 있는데 봐도 좋을 듯 하다.
끝.
'공부 > 프로그래밍' 카테고리의 다른 글
[react] 하위 Component에서 children 을 지정하지 않아 에러가 나는 경우-TS2322: Type '{ children: never[];... (0) | 2021.04.12 |
---|---|
[react] material-ui, styled-components 같이 쓸때 테마 적용 안될때 (0) | 2021.04.09 |
[springboot, jwt] jwt 로 토큰 생성, 유효시간 관리 하기 (1) | 2021.04.05 |
[react, springboot] kakao 로그인 API 연동 (0) | 2021.04.02 |
[react, next.js, redux] material-ui 를 이용해 다크테마 적용하기(Layout.js 적용) (0) | 2021.04.01 |
댓글