Shiro与OAuth2集成详解

一、集成场景说明

  • Shiro 负责权限控制和会话管理
  • OAuth2 提供身份认证(如通过第三方授权服务器获取 access_token)
  • 前端携带 access_token 访问后端,后端用 Shiro 校验 token 并进行授权

二、集成思路

  1. 自定义 OAuth2 认证过滤器:拦截请求,校验 access_token
  2. 自定义 Realm:根据 access_token 获取用户信息和权限
  3. 配置 Shiro Filter Chain:指定哪些接口需要 OAuth2 认证

三、关键实现步骤

1. 添加依赖

<!-- Shiro 依赖 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>最新版本</version>
</dependency>
<!-- 可选:OAuth2/JWT 相关依赖(如 jjwt 或 jose4j) -->

2. 自定义 OAuth2Token

public class OAuth2Token implements AuthenticationToken {
    private String token;

    public OAuth2Token(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

3. 自定义 OAuth2Filter

public class OAuth2Filter extends AuthenticatingFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String token = getAccessToken(request);
        return token == null ? null : new OAuth2Token(token);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 不允许匿名访问
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        String token = getAccessToken(request);
        if (token == null) {
            // 返回401
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }
        // 提交给Realm进行认证
        return executeLogin(request, response);
    }

    private String getAccessToken(ServletRequest request) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String token = httpRequest.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        return token;
    }
}

4. 自定义 OAuth2Realm

public class OAuth2Realm extends AuthorizingRealm {

    // 必须支持OAuth2Token
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof OAuth2Token;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String accessToken = (String) token.getCredentials();

        // 1. 校验token有效性(如解析JWT,或请求OAuth2服务器)
        // 2. 获取用户信息
        String userId = parseUserIdFromToken(accessToken); // 自定义实现

        if (userId == null) {
            throw new AuthenticationException("Token无效或已过期");
        }

        return new SimpleAuthenticationInfo(userId, accessToken, getName());
    }

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String userId = (String) principals.getPrimaryPrincipal();

        // 1. 根据userId查角色和权限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRole("user");
        info.addStringPermission("data:read");
        return info;
    }
}

5. Shiro 配置集成 Filter

@Configuration
public class ShiroConfig {

    @Bean
    public OAuth2Realm oAuth2Realm() {
        return new OAuth2Realm();
    }

    @Bean
    public SecurityManager securityManager(OAuth2Realm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", new OAuth2Filter());
        factoryBean.setFilters(filters);

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/api/login", "anon");
        filterChainDefinitionMap.put("/api/**", "oauth2"); // 所有API接口必须携带token
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return factoryBean;
    }
}

四、集成 JWT(可选)

如果 access_token 是 JWT,可以用 jjwt 或 jose4j 进行校验和解析:

public String parseUserIdFromToken(String token) {
    Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    return claims.getSubject();
}

五、常见问题与建议

  • token 校验失败:确保解析逻辑和密钥正确。
  • 权限控制:在 doGetAuthorizationInfo 中根据实际业务查权限。
  • 前端必须携带 Authorization: Bearer xxxxx 头部
  • OAuth2 服务器集成:如需和第三方认证服务器对接,需实现 token 校验逻辑(如远程校验)。

六、进阶实践

1. 多端支持(移动端、前端 SPA)

  • 移动端/SPA 前端:通常在登录后获取 access_token,后续所有请求都在 HTTP Header(如 Authorization: Bearer xxxxx)中携带 token。
  • Shiro 后端:所有需要保护的接口都用自定义的 OAuth2Filter 拦截,拒绝无 token 或 token 无效的请求。

注意事项:

  • 前端需统一处理 401 响应,做跳转或弹窗提示。
  • 后端可根据 token 解析出的用户信息,做个性化响应。

2. Token 刷新机制

在 OAuth2 中,access_token 通常有较短有效期,refresh_token 用于换取新 token。

实现思路:

  • 前端在 access_token 过期后,自动携带 refresh_token 请求后端刷新接口 /api/token/refresh
  • 后端可实现 /api/token/refresh,校验 refresh_token,有效则生成新 access_token 返回。
  • Shiro 的 OAuth2Filter 只负责校验 access_token,刷新逻辑可独立实现。

接口示例:

@PostMapping("/api/token/refresh")
public ResponseEntity<?> refresh(@RequestBody RefreshTokenDTO dto) {
    // 校验 refresh_token,生成新 access_token
    String newToken = oauth2Service.refreshToken(dto.getRefreshToken());
    return ResponseEntity.ok(newToken);
}

3. Shiro 权限注解支持

Shiro 支持方法级权限控制,可以结合 OAuth2 实现细粒度授权。

注解示例:

@RequiresAuthentication
@RequiresRoles("admin")
@RequiresPermissions("data:write")
public void updateData() { ... }

Spring Boot 集成需开启注解支持:

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    return advisor;
}

4. 分布式部署场景

  • Token 校验建议:
    • 采用 JWT(自包含,无需远程校验),每个服务只需持有公钥/密钥即可校验。
    • 如果使用 opaque token(不透明 token),需每次向 OAuth2 认证服务器校验,可能影响性能。
  • 会话管理建议:
    • 不建议用 Shiro 的 session,建议无状态(stateless)设计,所有认证信息都在 token 内。

5. 异常处理与安全建议

  • 统一异常处理:如 token 失效、无权限,返回标准 401/403 响应,前端统一处理。
  • 安全建议
    • access_token 不要存储在本地不安全位置(如 localStorage),推荐 HttpOnly Cookie 或安全存储。
    • refresh_token 只用于换取新 token,不用于业务接口认证。
    • token 过期时间设置合理,防止被长期滥用。
    • 对高危接口增加额外校验,如验证码、二次认证。

6. 对接第三方认证服务(如 Authing、Keycloak、微信、阿里云)

通用流程:

  1. 前端通过第三方认证服务登录,获取 access_token。
  2. 后端 OAuth2Realm 在 doGetAuthenticationInfo 中调用第三方接口校验 token 或解析 JWT。
  3. 获取用户信息,完成 Shiro 的 Subject 认证和授权。

Keycloak JWT 校验示例:

public String parseUserIdFromToken(String token) {
    // 使用 Keycloak 公钥解析 JWT
    Claims claims = Jwts.parser().setSigningKey(keycloakPublicKey).parseClaimsJws(token).getBody();
    return claims.getSubject();
}

微信/阿里云等:一般需要远程请求第三方接口校验 token,获取用户信息。


7. 代码结构建议

  • OAuth2TokenOAuth2FilterOAuth2Realm 独立包管理,便于维护。
  • 所有权限控制、认证逻辑都集中在 Realm 和 Filter,业务代码只关心授权注解。
  • 刷新 token、异常处理等可放在公共服务层。

七、常见问题排查

问题排查建议
token 校验失败校验算法/密钥/第三方接口是否正确,token 是否过期
权限注解不生效检查 Advisor 配置是否开启,注解是否正确使用
401/403 响应前端未处理前端需拦截响应码,做统一跳转/弹窗
分布式部署 session 丢失推荐无状态,避免依赖 Shiro session
refresh_token 被盗用设置合理有效期,绑定设备/IP,定期轮换

创作不易,点赞关注,互通有无!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猩火燎猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值