로그인에 성공하면 JWT로 토큰을 발행하도록 할 것이다. 이번 포스팅 샘플 프로젝트는 Kakao 로그인 API를 이용해 로그인 시스템을 구현하는데, 이럴때는 비밀번호가 있지 않고 USER의 ID만 존재한다.
우선 토큰발생하는 클래스를 만들어보자.
JwtManager 클래스를 만든다.(이전 포스팅에 자세히 설명되어 있음)
[springboot, jwt] jwt 로 토큰 생성, 유효시간 관리 하기
이번에는 jwt를 이용한 로그인 인증을 만들려 한다. 일전에 @EnableAuthorizationserver deprecated 되면서 찾던 중 jwt가 있어 이걸 활용하기로 했다(DB로 토큰유효성 확인도 안해서 더 나은거 같기도 하고
lemontia.tistory.com
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 설정을 비슷한걸로 이전에 포스팅한게 있는데 봐도 좋을 듯 하다.
[spring security, oauth2] invalid_token 에러를 핸들링(ExceptionHandler)하여 output form 설정하기
저번 글에 이어 쓴 글이다. 이전에 Oauth2 토큰 발급을 요청할때 Exception을 핸들링 하는 것에 대해 알아봤다. 이번에는 ResourceServer 에서 핸들링 하는 방법을 알아보려 한다. 유효하지 않은 토큰으로
lemontia.tistory.com
끝.
'공부 > 프로그래밍' 카테고리의 다른 글
[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 |
댓글