单一入口的API接口使用shiro如果进行权限鉴权

mrdong916
  • 16

问题描述

在进行数据通讯当中,全部数据都是走 http://host/api/ 这个URL地址,用业务请求码来控制请求返回的结果,每个请求码可能会有多种情况发生,不同的用户角色控制其使用接口的权限,包含一个接口中不同的情况也要控制,在使用Shiro中就方了,大佬们有好的解决方案吗

回复
阅读 5.3k
2 个回答

如果不同业务已经设计成通过请求参数来控制的话,建议使用shiro的自定义权限控制,解析对应的业务代码,进行权限控制。这种方法比较灵活,即可写死权限规则,也可以配合数据库等做成动态规则控制。

拦截所有请求, 对应shiro配置: /**=perms;
继承org.apache.shiro.authz.Permission,重写implies方法,进行权限校验。

chen
  • 1
新手上路,请多包涵

自定义过滤器,重写

isAccessAllowed

自定义realm,重写

doGetAuthorizationInfo

我需要根据链接参数不同进行授权,就是通过这种方法的。

public class CustomRealm extends AuthorizingRealm

下面是授权代码

    /**
     * 获取权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("开始鉴权------------doGetAuthorizationInfo");
        User user = (User) principalCollection.getPrimaryPrincipal();

        List<String> userRoles = new ArrayList<String>();
        Set<String> categoryIds = new HashSet<>();
        QueryWrapper queryUserWrapper = new QueryWrapper();
        queryUserWrapper.eq("uuid",user.getUuid());
        user = userService.getOne(queryUserWrapper);
        if(null != user){

            Ugroup ugroup = ugroupService.getById(user.getGroupId());

            userRoles.add(ugroup.getName());

            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.eq("idx_group_id",ugroup.getId());
            List<GroupCategory> groupCategories = groupCategoryService.list(queryWrapper);
            if (groupCategories.size() > 0){
                for (GroupCategory groupCategory : groupCategories){
                    categoryIds.add(groupCategory.getCategoryId().toString());
                }
            }

        }else{
            throw new AuthorizationException();
        }
        //为当前用户设置角色和权限
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(userRoles);
        authorizationInfo.addStringPermissions(categoryIds);
        return authorizationInfo;
    }

过滤器中重写isAccessAllowed

public class JWTFilter extends AuthorizationFilter {
 ...
 @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        logger.info("-----isAccessAllowed----");
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(response);
            }
        }

        String id =  request.getParameter("id");
        Subject subject = getSubject(request,response);

        boolean flag = subject.isPermitted(id);
        return  flag;
    }
    ...
}

需求:对属于不同用户组的用户授权访问不同类型的文章

实现思路:

doGetAuthorizationInfo中根据token获取到用户信息,从数据库中联合查询到可以访问的文章类型id,添加到用户权限中,isAccessAllowed中获取用户访问链接中的参数,调用

subject.isPermitted(id);

判断是否拥有该权限。

注意点:subject.isPermitted(id)调用前一定要先调用subject.login(token);方法,不然在isPermitted时不会进行授权

下面贴上全部代码

CustomRealm.java

package com.zyc.shiro;


import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zyc.constant.CommonConstant;
import com.zyc.dao.GroupCategoryMapper;
import com.zyc.dao.UgroupMapper;
import com.zyc.entity.GroupCategory;
import com.zyc.entity.Ugroup;
import com.zyc.entity.User;
import com.zyc.redis.JwtRedisDAO;
import com.zyc.service.GroupCategoryService;
import com.zyc.service.UgroupService;
import com.zyc.service.UserService;
import com.zyc.util.JWTUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.*;

/**
 * 鉴权
 */
public class CustomRealm extends AuthorizingRealm {

    static Logger logger = LoggerFactory.getLogger(CustomRealm.class);

    @Autowired
    private UgroupService ugroupService;

    @Autowired
    private GroupCategoryService groupCategoryService;

    @Autowired
    private UserService userService;

    @Autowired
    private JwtRedisDAO jwtRedisDAO;



    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 获取权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("开始鉴权------------doGetAuthorizationInfo");
        User user = (User) principalCollection.getPrimaryPrincipal();

        List<String> userRoles = new ArrayList<String>();
        Set<String> categoryIds = new HashSet<>();
        QueryWrapper queryUserWrapper = new QueryWrapper();
        queryUserWrapper.eq("uuid",user.getUuid());
        user = userService.getOne(queryUserWrapper);
        if(null != user){

            Ugroup ugroup = ugroupService.getById(user.getGroupId());

            userRoles.add(ugroup.getName());

            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.eq("idx_group_id",ugroup.getId());
            List<GroupCategory> groupCategories = groupCategoryService.list(queryWrapper);
            if (groupCategories.size() > 0){
                for (GroupCategory groupCategory : groupCategories){
                    categoryIds.add(groupCategory.getCategoryId().toString());
                }
            }

        }else{
            throw new AuthorizationException();
        }
        //为当前用户设置角色和权限
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(userRoles);
        authorizationInfo.addStringPermissions(categoryIds);
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        logger.info("CustomRealm------------------doGetAuthenticationInfo");
        Map claims = JWTUtils.getClaims(CommonConstant.JWT_SECRET, (String) authenticationToken.getPrincipal());

        if (claims == null) {
            //没找到帐号
            throw new UnknownAccountException();
        }

        String uuid = (String) claims.get("uuid");
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("uuid",uuid);
        User user = userService.getOne(queryWrapper);
        //logger.info(user.toString());
        if(null == user){
            throw new UnknownAccountException();
        }

        String token = jwtRedisDAO.get(CommonConstant.ADMIN_JWT_PREFIX + uuid);
        //logger.info("token {}", authenticationToken.getPrincipal());
        if (token == null || !token.equals(authenticationToken.getPrincipal())) {
            throw new AuthorizationException();
        }

        //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,可以自定义实现
        return new SimpleAuthenticationInfo(
                //用户信息
                user,
                authenticationToken.getPrincipal(),
                //realm name
                getName()
        );

    }

}

JWTFilter.java

package com.zyc.shiro;

import com.alibaba.fastjson.JSON;
import com.zyc.exception.enums.ErrorEnums;
import com.zyc.vo.RestResult;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class JWTFilter extends AuthorizationFilter {

    private static Logger logger = LoggerFactory.getLogger(JWTFilter.class);

    /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */
    //@Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        logger.info("-----isLoginAttempt----");
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return authorization != null;
    }

    /**
     * 实现用户登录
     */
    //@Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        logger.info("-----executeLogin----");

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");

        JWTToken token = new JWTToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 该注释并非现在方法的注释,是以前版本的,不要受影响,写在这里只是提醒还有其他写法
     *
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        logger.info("-----isAccessAllowed----");
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(response);
            }
        }

        String id =  request.getParameter("id");
        Subject subject = getSubject(request,response);

        boolean flag = subject.isPermitted(id);
        return  flag;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        logger.info("-----preHandle----");

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", httpServletRequest.getMethod());
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        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 true;
        }
        return super.preHandle(request, response);
    }

    /**
     * onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回 true 表示需要继续处理;如果返回 false 表示该拦截器实例已经处理了,将直接返回即可。
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) {
        logger.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");

        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
            RestResult result = new RestResult();
            result.setCode(ErrorEnums.PERMISSION_DENIED.getCode());
            result.setMsg(ErrorEnums.PERMISSION_DENIED.getMsg());
            httpServletResponse.setContentType("application/json;charset=UTF-8");
            PrintWriter out = httpServletResponse.getWriter();
            out.print(JSON.toJSONString(result));
            out.flush();
            out.close();
        } catch (IOException e) {
            logger.error(e.getMessage());
        }

        // 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
        return false;
    }

    /**
     * 非法请求返回code401
     */
    private void response401(ServletResponse resp) {
        logger.info("-----response401----");

        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;

            RestResult result = new RestResult();
            result.setCode(ErrorEnums.TOKEN_MISS.getCode());
            result.setMsg(ErrorEnums.TOKEN_MISS.getMsg());
            httpServletResponse.setContentType("application/json;charset=UTF-8");
            PrintWriter out = httpServletResponse.getWriter();
            out.print(JSON.toJSONString(result));

            out.flush();
            out.close();
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }
}

JWTToken.java

package com.zyc.shiro;

import org.apache.shiro.authc.AuthenticationToken;

public class JWTToken implements AuthenticationToken {

    private String token;

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

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

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

ShiroConfigurer.java

package com.zyc;

import com.zyc.shiro.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.authz.permission.PermissionResolver;
import org.apache.shiro.authz.permission.RolePermissionResolver;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.util.*;

@Configuration
public class ShiroConfigurer {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfigurer.class);

    /**
     * Shiro的Web过滤器Factory 命名:shiroFilter<br />
     *
     * @param securityManager
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        logger.info("注入Shiro的Web过滤器-->shiroFilter {}", ShiroFilterFactoryBean.class);
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
        // shiroFilterFactoryBean.setLoginUrl("/login")
        // 登录成功后要跳转的连接,逻辑也可以自定义,例如返回上次请求的页面
        // shiroFilterFactoryBean.setSuccessUrl("/index")
        // 用户访问未对其授权的资源时,所显示的连接
        // shiroFilterFactoryBean.setUnauthorizedUrl("/pages/403")
        /* 定义shiro过滤器,例如实现自定义的FormAuthenticationFilter,需要继承FormAuthenticationFilter
         **本例中暂不自定义实现,在下一节实现验证码的例子中体现
         */

        /*定义shiro过滤链  Map结构
         * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的
         * anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种
         * authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
         */
        // 添加自己的过滤器并且取名为jwt
        Map filterMap = new HashMap();
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);


        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了

        // <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了
        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        // filterChainDefinitionMap.put("/webui/**", "anon")
        // filterChainDefinitionMap.put("/webjars/**", "anon")
        //filterChainDefinitionMap.put("/sys/login", "anon");
        //filterChainDefinitionMap.put("/sys/logout", "anon");
        //filterChainDefinitionMap.put("/token/callback", "anon");
        //filterChainDefinitionMap.put("/dist", "anon");
        //filterChainDefinitionMap.put("/download/excel", "anon");
        //登陆相关api不需要被过滤器拦截
        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        // 所有请求通过JWT Filter
        filterChainDefinitionMap.put("/**", "jwt");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    @Bean
    public JWTFilter jwtFilter() {
        return new JWTFilter();
    }

    /**
     * Shiro Realm 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的
     *
     * @param
     * @return managerRealm
     */
    @Bean
    public CustomRealm userRealm() {
        CustomRealm userRealm = new CustomRealm();
        // 告诉realm,使用credentialsMatcher加密算法类来验证密文
        // userRealm.setCredentialsMatcher(hashedCredentialsMatcher())
        userRealm.setCachingEnabled(false);
        //自定义权限解析器
        return userRealm;
    }

    /**
     * 不指定名字的话,自动创建一个方法名第一个字母小写的bean
     *
     * @return
     * @Bean(name = "securityManager")
     */
    @Bean
    public SecurityManager securityManager() {
        logger.info("注入Shiro的Web过滤器-->securityManager {}", ShiroFilterFactoryBean.class);
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        // 关闭自带session
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }


    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     * 所以我们需要修改下doGetAuthenticationInfo中的代码;
     * )
     * 可以扩展凭证匹配器,实现 输入密码错误次数后锁定等功能,下一次
     *
     * @return
     */
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("SHA-512");
        //散列的次数,比如散列两次,相当于 md5(md5(""))
        hashedCredentialsMatcher.setHashIterations(2);
        //storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * Shiro生命周期处理器
     *
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     *
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        advisorAutoProxyCreator.setUsePrefix(true);
        return advisorAutoProxyCreator;
    }

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

}

pom.xml

<!--aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--jwt-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>
<!-- Shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏