1. 인증필터가 인증요청을 가로채 인증관리자에 위임 -> 응답을 바탕으로 보안컨텍스트 구성
2. 인증관리자는 인증공급자(인증논리)를 통해 인증을 처리
3. 인증공급자 : 인증논리 구현체 : 인증에 필요한 자격증명 제공 : AuthenticationProvider
- < 사용자 세부정보서비스 : 사용자 관리책임구현 > : UserDetailsService
- < 암호 인코더 : 암호관리 구현 > : PasswordEncoder
4. 보안 컨텍스트 : 인증데이터 저장
UserDetailsService Interface
- UserDetails - loadUserByUsername(String) : 사용자이름으로 사용자 세부정보(UserDetails)를 얻는다
- 해당 코드에서는 User 클래스의 builder를 통해 UserDetails 인스턴스를 간단하게 생성함 : UserDetails를 따로 구현할 필요 X
- User class -> UserDetails
UserDetails는 사용자(User)를 기술한다- username / password / authorities
- 인터페이스 : 이용권리 나타내는 메소드 가짐
PasswordEncoder Interface
- String encode(rawPW) : raw Password를 변환해 반환
- boolean matches(rawPW, encodedPW) : 인코딩문자열 - 원시문자열 일치여부 확인
: 접근을 요청한 엔티티의 세부정보 + 인증프로세스완료여부, 권한의 컬렉션, ...
:
OncePerRequestFilter(Filter) Interface
- doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain)
- filterChain.doFilter() : CorsFilter / CsrfFilter / BasicAuthenticationFilter / CustomFilters
: 필터체인 : 체인의 다음필터로 요청전달 -> 필요한 여러가지 필터 수행
- CustomFilter 적용하기
config class에서 http요청에
- http.addFilterBefore(CustomFilter, custom 이후에 사용될 필터)
- http.addFilterAfter(CustonFilter, custom 이전에 사용될 필터)
Http 요청이 들어온다.
들어온 요청이 AuthenticationFilter에 걸리면, 요청의 payload로부터 ID/PW를 추출하는데 이를 User Credentials 즉, 사용자 자격 증명 정보라 한다.
이 정보를 기반으로 해 Authentication의 구현체의 일종인 UsernamePasswordAuthenticationToken 객체를 생성한다.
: HTTP요청 -> AuthenticationFilter : UserCredential 추출 -> UsernamePasswordAuthenticationToken 객체 생성(Authentication의 구현체)
UsernamePasswordAuthenticationToken은 추후 인증이 끝나고 SecurityContextHolder.getContext()에 등록될 Authentication 객체이다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationProvider마다 인증에 필요한 Authentication 객체가 다르다 -> support()를 통해 Authentication객체에 맞는 AuthenticationProvider를 찾는다.
이 과정이 AuthenticationManager 구현체인 ProviderManager에서 이루어진다.
5.AuthenticationProvider는 자격증명정보 등을 저장소로부터 받아와 실제 인증로직을 수행한다 : UserDetailsService를 통해 UserDetails 객체 형태로 받아온다.
DaoAuthenticationProvider
- retrieveUser() : UserDetailsService객체를 통해 로그인 요청한 유저의 UserDetails 객체를 가져온다
- additionalAuthenticationChecks() : 입력받은 정보(username, credetial)와 userDetails객체의 정보와 비교해 인증을 체크하는 메소드이다. 실질적으로 DB의 데이터와 id, 비밀번호를 입력한 값과 비교하는 곳
인증된 Authentication 객체 반환
: UsernamePasswordAuthenticationFilter -> AbstractAuthenticationProcessingFilter의 doFilter()에서 실행된 attemptAuthentication()의 결과로 인증된 Authentication 객체가 반환된다.
인증이 성공적으로 되었다면,Authentication 객체를 SecurityContextHolder 내의 SecurityContext에 저장한다
SecurityContext
+ ( JWTFilter, JWTProvider )
: JWT 생성, 해석, 변환
+ AuthenticationProvider의 역할도 겸임
public class JwtProvider {
private final Key key;
private static final String AUTHORITIES_KEY = "auth";
private static final String BEARER_TYPE = "bearer";
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 3; // 3시간
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; // 7일
public JwtProvider(@Value(("${jwt.secret}")) String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
// Token 생성
public String generateAccessToken(Authentication authentication, long now) {
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, getAuthorities(authentication))
.setExpiration(new Date(now + ACCESS_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public TokenDto generateTokenDto(Authentication authentication) {
long now = (new Date()).getTime();
Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME);
return TokenDto.builder()
.grantType(BEARER_TYPE)
.accessToken(generateAccessToken(authentication, now))
.refreshToken(generateRefreshToken(refreshTokenExpiresIn))
.refreshTokenExpiresIn(refreshTokenExpiresIn.getTime())
.build();
}
// Token 기반 Authentication 구현체 생성 : AuthenticationProvider 역할
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
if(claims.get(AUTHORITIES_KEY) == null) {
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
}
Collection<? extends GrantedAuthority> authorities = getAuthorities(claims);
return new UsernamePasswordAuthenticationToken(
new User(claims.getSubject(), "", authorities),
"",
authorities
);
}
// Token 유효성 검사
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
private String generateRefreshToken(Date tokenExpiresIn) {
return Jwts.builder()
.setExpiration(tokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
private String getAuthorities(Authentication authentication) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
}
private Collection<? extends GrantedAuthority> getAuthorities(Claims claims) {
return Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 토큰 분리
String jwt = resolveToken(request);
// 토큰 유효성 검증
if (!jwt.equals("nothing") && jwtProvider.validateToken(jwt)) {
//SecurityContextHolder에 Authentication 저장
SecurityContextHolder.getContext().setAuthentication(jwtProvider.getAuthentication(jwt));
// Authentication 받아옴
}
//filterChain
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
// request HEADER에서
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
// 7 = PREFIX.Length
// PREFIX 부분을 날리고 JWT만 token에 할당
return bearerToken.substring(7);
}
return "nothing";
}
}