프로젝트에서 Spring Security와 JWT를 사용한 인증, 인가 부분을 담당하게 되었다.
구현이 완료되어서 간략하게나마 벨로그에 정리해보고자 한다. (코드 양이 많아서 전체 코드는 올리지 못할 것 같다)
JWT 구현에는 java-jwt 라이브러리를 사용하였다.
User 엔터티에서 필요한 것은 권한에 필요한 컬럼이다. (모든 회원이 동일한 권한이면 필요 없긴 함)
권한 테이블을 따로 빼주기도 하는데 필자는 역정규화로 users 테이블에 VARCHAR로 추가하였다.
여러 권한을 가지고 있으면 ","로 구분 짓는다.
@Column(name = "role", nullable = false, columnDefinition = "varchar(20)")
private String role;
그리고 User 객체의 권한(String
)을 GrantedAuthority
List로 변환할 메소드를 작성한다.
public List<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> roles = new ArrayList<>();
for (String role : this.role.split(",")) {
roles.add(new SimpleGrantedAuthority(role));
}
return roles;
}
로그인 API를 작성한다.
로그인 API의 역할은 이메일, 패스워드를 데이터베이스의 값과 검증한 후 일치하면 access, refresh token을 발급해주는 것이다.
이때 refresh token은 DB에 저장한다. (유효기간이 길기 때문에 refresh token 재발급 시 기존 refresh token 무력화, refresh token 폐기처리 가능)
token은 response body로 넘겨주었다.
또한 회원가입, 로그인 API의 경우 인가에서 자유로워야하기 때문에(아무나 접근할 수 있어야 한다) security configure에서 인가 대상에서 제외시켜주자.
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(
request.getEmail(), request.getPassword());
Authentication authentication = authenticationManager.authenticate(jwtAuthenticationToken);
우선 입력 받은 이메일과 패스워드로 JwtAuthenticationToken
(AbstractAuthenticationToken
를 구현한 클래스) 을 생성하고 AuthenticationManager
의 authenticate
메소드를 호출한다.
security configuer에서 AuthenticationProvider
를 구현한 JwtAuthenticationProvider
를 빈으로 등록했기 때문에 해당 provider가 호출된다.
User user = userService.login(principal, credentials);
List<GrantedAuthority> authorities = user.getAuthorities();
String accessToken = getAccessToken(user.getEmail(), authorities);
String refreshToken = getRefreshToken(user.getEmail(), authorities);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(
new JwtAuthentication(accessToken, refreshToken, user.getEmail()), null, authorities);
authenticationToken.setDetails(user);
return authenticationToken;
provider 안에서는 입력 받은 값으로 로그인을 진행하고, 해당 사용자의 권한을 가져온다.
이메일과 가져온 권한으로 access token과 refresh token을 생성하고 (token의 header, issuer 등의 정보는 yaml에 저장해 configure를 생성해서 사용했음) detail에 user 객체를 넣는다. (생략해도 되는 과정, 필자는 나중에 사용)
이제 다시 컨트롤러에서 Authentication
을 가져왔으므로, access token과 refresh token을 꺼내어 반환하면 된다.
필자는 user 정보도 같이 반환해줘야해서 detail에 저장한 user 객체도 dto로 변환해서 반환했다.
사실, 위에서는 불필요한 절차가 많다. 로그인 시 이메일, 비번이 일치하면 이메일과 권한으로 토큰들을 발급해주면 그만이다.
하지만AbstractAuthenticationToken
를 구현하여JwtAuthenticationToken
를 만드는 등의 절차를 수행한 것은 추후에 API 인가 처리를 진행할 때 JWT 인증 필터를 구현하여SecurityContextHolder
에Authentication
을 넣어줘야되기 때문이다. (물론 로그인 시 토큰만 발급해주면 되는 것은 맞다)
JWT 필터에서 해야될 일은 토큰을 검증한 후 권한을 부여하는 것이다.
앞서 말했듯이 권한을 부여하는 것은 SecurityContextHolder
에 Authentication
을 넣어주면 된다.
이때 필자는 헤더에 access token이 존재하고 refresh token이 존재하지 않을때만 토큰 검증 후 권한 부여 처리를 진행했다.
refresh token이 존재하는 경우는 access token 재발급 요청의 경우이기 때문에 access token이 만료된 상태일 것이고, 토큰 검증에서 에러가 발생할 것이기 때문이다.
WebSecurityConfigurerAdapter
를 구현하는 방법이 deprecated 되었다.
따라서 오버라이드할 필요 없이 빈으로 등록하는 방법으로 구현했다.
config에서 할 일은 다음과 같다.
PasswordEncoder
를 빈으로 등록한다. (필자는 BCrypt 사용)AuthenticationManager
에 자동 등록)UsernamePasswordAuthenticationFilter
앞에 넣어준다.doFilter()
를 try-catch로 묶어줘서 구현)AccessDeniedHandler
를 구현해 빈으로 등록한다 (권한이 없는데 인가 필요한 API 접근 시 에러 반환)AuthenticationManager
를 빈으로 등록한다.이외에도 설정할 것이 많지만 JWT를 통한 인증, 인가에는 포함되지 않으므로 생략
큰 흐름으로 정리하자면 다음과 같다.
로그인
로그인 시 access, refresh token 발급 (유효기간은 1시간, 2주로 설정했음)
API 요청 (인가 처리)
인가 필요한 API 요청 시 header에 발급 받은 access 토큰 넣어서 보냄 ->
JWT 필터에서 토큰 검증 후 권한 부여 (SecurityContextHolder
에 넣어줌) ->
security config에서 설정한 해당 API의 권한에 일치하지 않으면 AccessDeniedHandler
가 에러 반환 ->
인가 통과하면 컨트롤러에서 @AuthenticationPrincipal
어노테이션으로 context holder에 넣어준 Authentication
의 principal 사용 가능
refresh token으로 access token 재발급
access token 만료 확인 (만료되지 않았는데 재발급 요청하는 경우 해킹으로 간주) ->
refresh token 값이 DB에 저장된 해당 사용자의 refresh token과 일치하는지, 유효기간 안지났는지 검증 ->
검증되면 access token 재발급