스프링 부트 애플리케이션이 있는데 아파치 시로를 통합하려고했습니다. 첫 번째 반복으로서 JWT 방법을 인증하고 승인합니다. 세션은 전혀 없습니다.JWT가있는 Spring Boot 및 Apache Shiro - 올바르게 사용하고 있습니까?
내가 작성한 방식대로 모든 REST 요청에는 유효성을 검사해야하는 JWT 헤더가 있어야합니다. 나는 시로 (shiro) 필터에서 그것을하고있다. 유효성 검사 후 필터는 컨텍스트를 설정하여 모든 REST 컨트롤러 메서드가 가져 와서 실행할 수 있습니다.
커뮤니티의 의견을 듣고 제 설정이 올바른지 확인하겠습니다. 또한, 특정 문제가 있습니다 (적어도 IMO), 나는 그것에 직면하고 있습니다. 그래서 누군가가 올바른 방법으로 빛을 던지면 큰 도움이 될 것입니다.
다음은 구성 및 영역 디자인을 보여주는 코드 조각입니다.
니핏 1 ShiroConfiguration
private AuthenticationService authenticationService;
/**
* FilterRegistrationBean
* @return
*/
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
filterRegistration.setEnabled(true);
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
filterRegistration.setOrder(1);
return filterRegistration;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
dwsm.setRealm(authenticationService());
final DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// disable session cookie
sessionManager.setSessionIdCookieEnabled(false);
dwsm.setSessionManager(sessionManager);
return dwsm;
}
/**
* @see org.apache.shiro.spring.web.ShiroFilterFactoryBean
* @return
*/
@Bean(name="shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager, JWTTimeoutProperties jwtTimeoutProperties, TokenUtil tokenUtil) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
//TODO: Create a controller to replicate unauthenticated request handler
bean.setUnauthorizedUrl("/unauthor");
Map<String, Filter> filters = new HashMap<>();
filters.put("perms", new AuthenticationTokenFilter(jwtTimeoutProperties, tokenUtil));
filters.put("anon", new AnonymousFilter());
bean.setFilters(filters);
LinkedHashMap<String, String> chains = new LinkedHashMap<>();
chains.put("/", "anon");
chains.put("/favicon.ico", "anon");
chains.put("/index.html", "anon");
chains.put("/**/swagger-resources", "anon");
chains.put("/api/**", "perms");
bean.setFilterChainDefinitionMap(chains);
return bean;
}
@Bean
@DependsOn(value="lifecycleBeanPostProcessor")
public AuthenticationService authenticationService() {
if (authenticationService==null){
authenticationService = new AuthenticationService();
}
return authenticationService;
}
@Bean
@DependsOn(value="lifecycleBeanPostProcessor")
public Authorizer authorizer() {
return authenticationService();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
니핏 2 AuthenticationFilter
public class AuthenticationTokenFilter extends PermissionsAuthorizationFilter {
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String authorizationHeader = httpRequest.getHeader(TOKEN_HEADER);
String authToken;
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
httpRequest.setAttribute(alreadyFilteredAttributeName, true);
AuthenticationService.ensureUserIsLoggedOut(); // To not end up getting following error.
if (authorizationHeader != null && !authorizationHeader.isEmpty()) {
if (authorizationHeader.startsWith(BEARER_TOKEN_START_WITH)) {
authToken = authorizationHeader.substring(BEARER_TOKEN_START_INDEX);
} else if (authorizationHeader.startsWith(BASIC_TOKEN_START_WITH)) {
String caseId = UUID.randomUUID().toString();
log.warn("{} Basic authentication is not supported but a Basic authorization header was passed in", caseId);
return false;
} else {
// if its neither bearer nor basic, default it to bearer.
authToken = authorizationHeader;
}
try {
if(tokenUtil.validateTokenAgainstSignature(authToken, jwtTimeoutProperties.getSecret())) {
Map<String, Object> outerClaimsFromToken = tokenUtil.getOuterClaimsFromToken(authToken, jwtTimeoutProperties.getSecret());
JWTAuthenticationToken jwtAuthenticationToken = new JWTAuthenticationToken(outerClaimsFromToken.get(TokenUtil.CLAIM_KEY_USERID),
(String) outerClaimsFromToken.get(TokenUtil.CLAIM_KEY_INNER_TOKEN));
SecurityUtils.getSubject().login(jwtAuthenticationToken);
} catch (JwtException | AuthenticationException ex) {
log.info("JWT validation failed.", ex);
}
}
return false;
}
니핏 3 TokenRestController
public Response getToken() {
AuthenticationService.ensureUserIsLoggedOut(); // To not end up getting following error.
// org.apache.shiro.session.UnknownSessionException: There is no session with id
// TODO: In case of logging in with the organization, create own token class implementing HostAuthenticationToken class.
IAMLoginToken loginToken = new IAMLoginToken(authenticationRequestDTO.getUsername(), authenticationRequestDTO.getPassword());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(loginToken);
} catch (AuthenticationException e) {
log.debug("Unable to login", e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
}
AuthenticatingUser user = (AuthenticatingUser) subject.getPrincipal();
String authToken = authenticationService.generateToken(user);
return ResponseEntity.status(HttpStatus.OK).body(new AuthenticationResponseDTO(authToken));
});
,
발췌문 4 : AuthorizingRealm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if (token instanceof IAMLoginToken) {
IAMLoginToken usernamePasswordToken = (IAMLoginToken) token;
UserBO user = identityManagerRepository.getUserByUsername(usernamePasswordToken.getUsername(), true);
if (user != null && user.getSecret() != null && !user.getSecret().isEmpty()) {
if(passwordEncoder.matches(String.valueOf(usernamePasswordToken.getPassword()), user.getPassword())) {
if (!isActive(user)) {
throw new AuthenticationException("User account inactive.");
}
return new SimpleAuthenticationInfo(toAuthenticatingUser(user).withSecret(user.getSecret()), usernamePasswordToken.getPassword(), getName());
}
}
} else if (token instanceof JWTAuthenticationToken) {
JWTAuthenticationToken jwtToken = (JWTAuthenticationToken) token;
String userId = (String) jwtToken.getUserId();
String secret = cache.getUserSecretById(userId, false);
if (secret != null && !secret.isEmpty()) {
Map<String, Object> tokenClaims = tokenUtil.getClaims(jwtToken.getToken(), secret);
String orgId = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_ORG);
String email = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_EMAIL);
String firstName = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_FIRSTNAME);
String lastName = (String) tokenClaims.get(TokenUtil.CLAIM_KEY_LASTNAME);
Set<String> permissions = (Set<String>) tokenClaims.get(TokenUtil.CLAIM_KEY_PERMISSIONS);
return new SimpleAccount(new AuthenticatingUser(userId, orgId, email, firstName, lastName, permissions), jwtToken.getToken(), getName());
}
}
throw new AuthenticationException("Invalid username/password combination!");
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setStringPermissions(((AuthenticatingUser)principals.getPrimaryPrincipal()).getPermissions());
return authorizationInfo;
}
문제 및 문제
여기에서 언급 한 바와 같이 동일한 오류가 발생했습니다. Shiro complaining "There is no session with id xxx" with DefaultSecurityManager 기본적으로 Shiro는 세션 사용을 중지하거나 유효성을 검사하기를 원합니다. 그것을 성취 할 수있는 방법이 있습니까? 나는 대답에서 언급 한 것과 같은 수정을 구현하여 문제를 해결했다. 즉
ensureUserIsLoggedOut()
은 무엇을 하는가?구성의 ShiroFilterFactoryBean 정의에서 알 수 있듯이 일부 필터 체인 정의를 설정하고 있습니다. 그리고 거기에
/api
으로 시작하는 모든 API 호출 앞에 인증 필터가있을 것입니다. 하지만 문제는 예외를 추가하고 싶습니다. 예 :/api/v0/login
은 그 중 하나입니다. 이것을 성취 할 수있는 방법이 있습니까?매우 제한된 문서와 비슷한 오픈 소스 프로젝트 샘플을 발견 했으므로 전반적인 구성이 적절한지 모르겠습니다.
의견을 환영합니다.
불행히도 나는 동일한 효과를 얻었습니다. 두 유형의 필터가 모두 작동하며 여전히 동일한 문제가 발생합니다. 하지만 'authc'필터를 사용하는 측면에서 옳습니다. 그것의 오른쪽 필터. –