인가(Authorization) 처리를 위한 핵심 컴포넌트
인가를 처리하기 위해 Spring Security Filter 중 FilterSecurityInterceptor
가 사실상 권한부여 처리를 AccessDecisionManager
에게 위임함으로써 접근 제어 결정을 쉽게 해준다.
@Bean
public ConnectionBasedVoter connectionBasedVoter() {
Pattern pattern = Pattern.compile( "^/api/user/(\\d+)/post/.*$");
RequestMatcher requiresAuthorizationRequestMatcher = new RegexRequestMatcher(pattern.pattern(), null);
return new ConnectionBasedVoter(requiresAuthorizationRequestMatcher, (String url) -> {
/* url에서 targetId를 추출하기 위해 정규식 처리 */
Matcher matcher = pattern.matcher(url);
long id = matcher.find() ? toLong(matcher.group(1), -1) : -1;
return Id.of(User.class, id);
});
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
decisionVoters.add(new WebExpressionVoter());
decisionVoters.add(connectionBasedVoter());
// 모든 voter가 승인해야 해야한다.
return new UnanimousBased(decisionVoters);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.accessDeniedHandler(accessDeniedHandler)
//...
.antMatchers("/api/_hcheck").permitAll()
.antMatchers("/api/auth").permitAll()
.antMatchers("/api/user/join").permitAll()
.antMatchers("/api/user/exists").permitAll()
.antMatchers("/api/**").hasRole("USER")
//...
.accessDecisionManager(accessDecisionManager())
// ...
http
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/swagger-resources", "/webjars/**", "/templates/**", "/h2/**");
}
configure() 메소드에서 addFilterBefore()에 정의된 jwtAuthenticationTokenFiler()
, UsernamePasswordAuthenticationFilter.class
Filter 먼저 실행
-> 여러 Filter 단계를 거쳐 controller가 호출됨.
SecurityContextHolder에 저장된 Authentication이 없다면 암호화 된 JWT를 가져와서 (obtainAuthorizationToken 함수) JwtAuthenticationToken을 생성하고, SecurityContextHolder에 저장 (SecurityContext에 Authentication 클래스 객체를 setAuthentication)
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
String authorizationToken = obtainAuthorizationToken(request);
if (authorizationToken != null) {
try {
JWT.Claims claims = verify(request, authorizationToken);
log.debug("Jwt parse result: {}", claims);
// 만료 10분 전
if (canRefresh(claims, 6000 * 10)) {
String refreshedToken = jwt.refreshToken(authorizationToken);
response.setHeader(tokenHeader, refreshedToken);
}
Long userKey = claims.userKey;
Email email = claims.email;
String name = claims.name; // 이름 프로퍼티 가져오기
List<GrantedAuthority> authorities = obtainAuthorities(claims);
if (userKey != null && email != null && authorities.size() > 0) {
JwtAuthenticationToken authentication =
new JwtAuthenticationToken(new JwtAuthentication(userKey, email, name), null, authorities);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
log.warn("Jwt processing failed: {}", e.getMessage());
}
}
}
else {
log.debug("SecurityContextHolder not populated with security token, as it already contained: '{}'",
SecurityContextHolder.getContext().getAuthentication());
}
chain.doFilter(request, response);
}
//...
private boolean canRefresh(JWT.Claims claims, long refreshRangeMillis) {
long exp = claims.exp();
if (exp > 0) {
long remain = exp - System.currentTimeMillis();
return remain < refreshRangeMillis;
}
return false;
}
private List<GrantedAuthority> obtainAuthorities(JWT.Claims claims) {
String[] roles = claims.roles;
return roles == null || roles.length == 0 ?
Collections.emptyList() :
Arrays.stream(roles).map(SimpleGrantedAuthority::new).collect(toList());
}
private String obtainAuthorizationToken(HttpServletRequest request) {
String token = request.getHeader(tokenHeader);
if (token != null) {
if (log.isDebugEnabled())
log.debug("Jwt authorization api detected: {}", token);
try {
token = URLDecoder.decode(token, "UTF-8");
String[] parts = token.split(" ");
if (parts.length == 2) {
String scheme = parts[0];
String credentials = parts[1];
return BEARER.matcher(scheme).matches() ? credentials : null;
}
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage(), e);
}
}
return null;
}
private JWT.Claims verify(HttpServletRequest request, String token) {
return jwt.verify(token);
}
UsernamePasswordAuthenticationFilter
가 HttpServletRequest에서 사용자가 보낸 아이디와 패스워드를 인터셉트한다. 프론트 단에서 유효성검사를 할 수도 있지만, 무엇보다 안전을 위해서 다시 한번 사용자가 보낸 아이디와 패스워드의 유효성 검사를 해줄 수 있다.잠깐! 여기서 @AuthenticationPrincipal도 컨트롤러 단에서 세션의 정보들에 접근할게 될때 사용하는 어노테이션인데, 사용자가 보낸 아이디와 패스워드를 인터셉트하는가??
@GetMapping(path = "user/me")
public ApiResult<User> me(@AuthenticationPrincipal JwtAuthentication authentication) {
return OK(
userService.findById(authentication.id)
.orElseThrow(() -> new NotFoundException(User.class, authentication.id))
);
}
정답은? @AuthenticationPrincipal 은 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 와 같다. 접근 주체를 받기 때문에 Filter에 걸린다.
configure() 함수 내 accessDecisionManager()를 간략하게 설명하면 AuthenticationManager
가의 역할은 인증된 사용자가 리소스에 접근을 허용할지 안할지 Voter
들에게 물어본다. (마치 아까 AuthenticationManager가 Provider들에게 요청하듯이)
Voter
들의 결정을 모아서 최종 결정을 해준다.
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
decisionVoters.add(new WebExpressionVoter());
decisionVoters.add(connectionBasedVoter());
// 모든 voter가 승인해야 해야한다.
return new UnanimousBased(decisionVoters);
}
List<AccessDecisionVoter<>> (투표자들의 리스트)에 권한을 줄지 안줄지 기준인 Voter(투표자)들을 담는다. 등록된 Voter들을 가지고 권한을 줄지 안줄지 Manager가 최종 결정한다.
다음 기준을 가지고 결정한다.
new UnanimousBased()
으로 생성했기 때문에 모든 Voter가 승인해야 권한을 주게 된다.
@Bean
public ConnectionBasedVoter connectionBasedVoter() {
Pattern pattern = Pattern.compile( "^/api/user/(\\d+)/post/.*$");
RequestMatcher requiresAuthorizationRequestMatcher = new RegexRequestMatcher(pattern.pattern(), null);
return new ConnectionBasedVoter(requiresAuthorizationRequestMatcher, (String url) -> {
/* url에서 targetId를 추출하기 위해 정규식 처리 */
Matcher matcher = pattern.matcher(url);
long id = matcher.find() ? toLong(matcher.group(1), -1) : -1;
return Id.of(User.class, id);
});
}
다음 정규식(^/api/user/(\\d+)/post/.*$
)이 가지는 RESTURI 의 경우, 인가 처리를 실행한다. 이때, 정규식에서 숫자(\d+)부분을 찾아 targetId를 생성하여 매개변수로 return 함으로써 ConnectionBasedVoter을 생성한다.
ConnectionBasedVoter은 AccessDecisionVoter 클래스를 구현체로 커스터마이징 개발하여 권한을 줄 기준을 작성한다.
(프로그래머스) 단순 CRUD는 그만! 웹 백엔드 시스템 구현 온라인 스터디(Java반) 강의를 수강하고 제가 이해한대로 정리했습니다. 문제가 될시 삭제하겠습니다!