Spring Boot и Apache Shiro с JWT - правильно ли я его использую?

У меня есть приложение Spring Boot, и я попытался интегрировать с ним Apache shiro. В качестве первой итерации, я аутентифицирую и авторизирую способ JWT, без сессий вообще.

Как я это делал, каждый запрос REST должен содержать заголовок JWT, который должен быть проверен. Я делаю это в фильтре Широ. После проверки, фильтр устанавливает контекст, который любой метод контроллера REST сможет извлечь и воздействовать на него.

Я хочу, чтобы мнение сообщества убедилось, что моя конфигурация верна. Более того, есть определенные проблемы (по крайней мере, ИМО), с которыми я сталкиваюсь. Так что если кто-то может пролить свет на правильный способ его передачи, был бы очень признателен.

Ниже приведены некоторые фрагменты кода, демонстрирующие мою конфигурацию и дизайн области.

Фрагмент 1: ШироКонфигурация

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;
}

Проблемы и проблемы

  • Та же ошибка, что и здесь. Широ жаловался "Нет сеанса с идентификатором xxx" с DefaultSecurityManager. Я в основном хочу, чтобы Широ прекратил использовать и / или проверять сеансы. Есть ли способ достичь этого? Я решил это путем реализации того же самого исправления, как упомянуто в ответе, вот что ensureUserIsLoggedOut() делает.

  • Как вы можете видеть в определении конфигурации ShiroFilterFactoryBean, я устанавливаю некоторые определения цепочки фильтров. И там вы можете видеть, что я устанавливаю каждый вызов API, который начинается с /api будет иметь фильтр аутентификации впереди. Но дело в том, что я хочу добавить некоторые исключения. Такие как, /api/v0/login это один из них. Есть ли способ добиться этого?

  • В целом, я не уверен, подходит ли подходящая мне конфигурация, так как я нашел очень ограниченную документацию и похожие примеры проектов с открытым исходным кодом.

Любые отзывы приветствуются.

2 ответа

Я решил первую проблему нежелательной проверки и управления сеансом, запретив Широ использовать сеанс субъекта для хранения состояния этого субъекта в запросах / вызовах / сообщениях для всех субъектов.

Мне просто нужно было применить следующую конфигурацию к моему менеджеру сеансов в моей конфигурации shiro. https://shiro.apache.org/session-management.html

Вероятно, вам следует отделить свой токен-фильтр от фильтра "perms". Взгляните на фильтр BasicAuth или "authc". Это должно помочь вам обойти проблемы, которые вы видите. Вы в основном используете фильтр 'authz' (который я предполагаю, поэтому вам нужны эти обходные пути)

Другие вопросы по тегам