带有 JWT 的 Spring Boot 和 Apache Shiro - 我使用正确吗?

Posted

技术标签:

【中文标题】带有 JWT 的 Spring Boot 和 Apache Shiro - 我使用正确吗?【英文标题】:Spring Boot and Apache Shiro with JWT - Am I using it correctly? 【发布时间】:2018-05-06 18:00:29 【问题描述】:

我有 Spring Boot 应用程序,我尝试将 Apache shiro 与它集成。作为第一次迭代,我以 JWT 方式进行身份验证和授权,没有任何会话。

按照我的架构方式,每个 REST 请求都必须包含需要验证的 JWT 标头。我正在使用 shiro 过滤器进行操作。验证后,过滤器设置一个上下文,任何 REST 控制器方法都可以获取并对其进行操作。

我希望社区的意见,以确保我的配置是正确的。 此外,我面临着某些问题(至少是 IMO)。因此,如果有人能阐明正确的处理方式,将不胜感激。

以下是一些代码 sn-ps 演示我的配置和领域设计。

片段 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:授权领域

@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 就是其中之一。有没有办法做到这一点?

总的来说,我不确定我想出的配置是否合适,因为我发现文档和类似的开源项目示例非常有限。

欢迎任何反馈。

【问题讨论】:

【参考方案1】:

我通过阻止 Shiro 使用 Subject 的会话来跨所有 Subject 的请求/调用/消息存储该 Subject 的状态,解决了不需要的会话验证和管理的第一个问题。

我只需要将以下配置应用于我的 shiro 配置中的会话管理器。 https://shiro.apache.org/session-management.html#disabling-subject-state-session-storage

【讨论】:

【参考方案2】:

您可能应该将您的令牌过滤器与“perms”过滤器分开。查看 BasicAuth 过滤器或“authc”过滤器。这应该可以帮助您解决所遇到的问题。您基本上使用的是“authz”过滤器(我猜这就是您需要这些解决方法的原因)

【讨论】:

不幸的是我得到了同样的效果。两种类型的过滤器都有效,我仍然遇到同样的问题。但是就使用“authc”过滤器而言,您是对的。它是正确的过滤器。

以上是关于带有 JWT 的 Spring Boot 和 Apache Shiro - 我使用正确吗?的主要内容,如果未能解决你的问题,请参考以下文章

带有 JWT auth 和 csrf 令牌的 Spring Boot STATELESS 应用程序

使用带有 JWT 的 Angular 和 Spring Boot 服务的 PUT 请求出现 403 错误

带有加密 JWT 访问令牌的 Spring Boot OAuth2

如何在 Spring Boot 微服务架构中使用 Keycloak 实现 JWT?

获取 JWT 后,带有 Spring Boot 后端的 Angular 在现有路由上返回 404

Spring/Boot - JWT 未在 GET 请求中获取