Spring Boot:基于Apache Shiro实现权限认证和授权

Apache Shiro简介

Apache Shiro是一个安全开源框架,可用于处理认证、授权、session管理和加解密。

  • Authentication(认证):用户身份识别,通常被称为用户“登录”。

  • Authorization(授权):访问控制,比如某个用户是否具有某个操作的使用权限。

  • Session Management(会话管理):特定于用户的会话管理,甚至在非web或EJB应用程序。

  • Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。

核心概念

  • Subject:代表当前用户,可以是一个人,也可以是第三方服务。

  • SecurityManager:Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。对于 Web 应用一般使用DefaultWebSecurityManager。

  • Realms:Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

Spring Boot集成

添加依赖

<!--apache shiro-->
<dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-spring-boot-web-starter</artifactId>
       <version>1.7.1</version>
</dependency>

自定义ShiroRealm

继承AuthorizingRealm,实现其中的两个方法。

doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。

doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。

@Component
@RequiredArgsConstructor
@Slf4j
public class ShiroAuthorizeRealm extends AuthorizingRealm {
   // tokenUtils 为自定义token生产和校验工具类
    private final TokenUtils tokenUtils;

    /**
     * @description: 授权校验
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizeUser user = (SimpleAuthorizeUser) getAvailablePrincipal(principalCollection);
        Set<String> roles = user.getRoles();
        Set<String> perms = user.getPermissions();

        // 将用户用户角色和权限赋值Shiro对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles);
        info.setStringPermissions(perms);
        return info;
    }

    /**
     * @description: 认证校验
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        if (token == null) {
            throw new UnauthorizedException("token非法无效");
        }

        // 校验token有效性
        SimpleAuthorizationUser user = tokenUtils.getAuthorizationUser(token);
        if (user == null) {
            throw new UnauthorizedException("token已过期");
        }

        // 刷新token,(实现: 用户在线操作不掉线功能)
        tokenUtils.refreshToken(token);

        //查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方	SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
        return new SimpleAuthenticationInfo(user, token, getName());
    }
}

注意:

这里为了方便,自定义TokenUtils,用于生成和获取token;自定义SimpleAuthorizeUser,用于封装登录用户详情和权限信息,方便校验。

自定义AuthenticatingFilter

public class ShiroAuthorizeFilter extends AuthenticatingFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String token = getToken(request);
        if (token == null) {
            throw new UnauthorizedException("token非法无效");
        }
        return new AuthorizeToken(token);
    }

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // OPTIONS方法直接返回true
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }

        //token不存在則返回失敗
        String token = getToken(request);
        if (token == null) {
            return false;
        }

        try {
            return executeLogin(request, response);
        } catch (Exception e) {
            throw new UnauthorizedException("Token失效,请重新登录", e);
        }
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", "true");
        httpResponse.setCharacterEncoding("UTF-8");
        String json = JSON.toJSONString(R.unauthorized("token非法无效,请重新登录"));
        httpResponse.getWriter().print(json);
        return false;
    }

    /**
     * @description: 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    private String getToken(ServletRequest request) {
        //获取请求token
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(AuthConstants.HEADER);
        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isEmpty(token)) {
            token = httpServletRequest.getParameter(AuthConstants.HEADER);
        }
        return token;
    }
}

自定义Token

public class AuthorizeToken extends UsernamePasswordToken {

    private String token;

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

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

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

注意:

由于Shiro内部对Token进行UsernamePasswordToken.class类型判断,所以自定义Token要么继承UsernamePasswordToken,要么重写Realm的supports方法。

自定义Configuration

@Configuration
public class ShiroAutoConfiguration extends ShiroWebFilterConfiguration {

    @Bean
    public Realm realm(TokenUtils tokenUtils) {
        return new ShiroAuthorizeRealm(tokenUtils);
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(Realm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setRememberMeManager(null);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-
         * StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    @Override
    protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
        //采用父类的默认方法生成shiroFilterFactoryBean
        ShiroFilterFactoryBean shiroFilterFactoryBean = super.shiroFilterFactoryBean();
        //获取shiroFilterFactoryBean里的Filters集合
        Map filters = shiroFilterFactoryBean.getFilters();
        //put进一个自己编写的过滤器,并命名,上面会引用到
        filters.put("auth", new ShiroAuthorizeFilter());
        shiroFilterFactoryBean.setFilters(filters);

        return shiroFilterFactoryBean;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        //-- 可以匿名访问的url
        //登录接口排除
        chainDefinition.addPathDefinition("/login", "anon");
        //登出接口排除
        chainDefinition.addPathDefinition("/logout", "anon");
        //性能监控  TODO 存在安全漏洞
        chainDefinition.addPathDefinition("/actuator/**", "anon");
        //swagger
        chainDefinition.addPathDefinition("/", "anon");
        chainDefinition.addPathDefinition("/doc.html", "anon");
        chainDefinition.addPathDefinition("/swagger-ui.html", "anon");
        chainDefinition.addPathDefinition("/swagger**/**", "anon");
        chainDefinition.addPathDefinition("/webjars/**", "anon");
        chainDefinition.addPathDefinition("/v2/**", "anon");

        //-- 其余资源都需要认证
        chainDefinition.addPathDefinition("/**", "auth");
        return chainDefinition;
    }

    /**
     * 下面的代码是添加注解支持
     *
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        /**
         * 解决重复代理问题 github#994
         * 添加前缀判断 不匹配 任何Advisor
         */
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}
  1. 声明Bean securityManager,注入自定义Realm

  2. 继承ShiroWebFilterConfiguration,重写shiroFilterFactoryBean方法,并注入自定义filter

  3. 声明Bean shiroFilterChainDefinition,实现全局URL访问控制

  4. 添加注解Processor支持

添加全局异常处理


/**
 * @description: 权限认证全局异常处理,
 * 由于common-core中定义了全局异常处理,所以@Order设置为1,优先执行
 */
@Order(1)
@RestControllerAdvice
@Slf4j
public class AuthorizeGlobalExceptionHandler {

    @ExceptionHandler({AuthorizationException.class, UnauthorizedException.class})
    public R<?> handleException(AuthorizationException e) {
        log.error("权限异常处理:" + e.getMessage(), e);

        String message = "您的权限有误:" + e.getMessage();
        R result = R.unauthorized(message);

        return result;
    }
}

认证和授权流程

  1. 用户登录时生成token信息,设置过期时间,使用Redis存储,这步在登录时实现;

  2. 客户端调用接口时将token作为参数传给服务端,服务端根据token信息认证用户;

  3. Shiro框架通过自定义Filter过滤器捕获并进行认证和权限校验;

  4. 校验成功处理后续请求并返回结果给客户端,失败则抛出异常并返回

详细流程请查看下图

 

验证一下

  1. 请求token不存在或失效

2. 无权限

参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值