shiro+spring boot:权限管理自定义完整流程

shiro+spring boot:权限管理自定义完整流程

  • 权限管理自定义完整步骤
    • 步骤1 自定义token认证:UsernamePasswordToken
    • 步骤2 自定义过滤器:AccessControlFilter
    • 步骤3 自定义Realm:AuthorizingRealm
    • 步骤4 自定义认证匹配方法:HashedCredentialsMatcher
    • 步骤5 自定义shiro策略配置:ShiroConfig

权限管理自定义完整步骤

步骤1 自定义token认证:UsernamePasswordToken

在shiro包中新建CustomUsernamePasswordToken类,继承自UsernamePasswordToken。覆写该类的2个方法:getPrincipal方法、getCredentials方法。

public class CustomUsernamePasswordToken extends UsernamePasswordToken() {
   private String token;
   
   public CustomUsernamePasswordToken(String token) {
       this.token = token;
   }

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

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

步骤2 自定义过滤器:AccessControlFilter

基本流程:
1:通过过滤器拦截用户请求接口,从request中获取到用户认证信息(比如token),如果没有获取到用户认证信息的话,返回提示写入到response响应给前端。
2:将前端传来的认证信息(如token)整合成我们自定义的shiro用户认证UsernamePasswordToken;
3:主体提交认证
4:对用户认证抛出的异常进行try catch封装处理用户认证抛出的异常,写入response响应前端。

在shiro包中新建CustomAccessControlFilter类,继承自AccessControlFilter,该类覆写2个方法:isAccessAllowed方法、onAccessDenied方法。

public class  CustomAccessControlFilter extends AccessControlFilter{
  /** 如果返回true,就流转到下一个链式调用
      如果返回false,就会流转到onAccessDenied方法
      这里一般我们可以直接返回false,在onAccessDenied里面处理
  */
  @Override
  protected boolean  isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception{
      return false;
  }

  /** 如果返回true,就会流转到下一个链式调用
      如果返回false,就不会再继续流转了
  */
  @override
  protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
      HttpServletRequest request = (HttpServvletRequest) servletRequest;
      log.info("request 接口地址:{}", request.getRequestURI());
      log.info("request 接口的请求方式:{}", request.getMethod());
      /**
          需要做3步:
          1)取出用户认证信息,比如token或者cookie。并进行校验,如果检验错误,则需要返回false,并将提示写入response响应前端
          2) 携带了通过验证的认证信息后,则将认证信息整合成步骤1中自定义的 shiro用户认证的UsernamePasswordToken
          3)根据shiro用户认证的UserrnamePasswordToken,主体提交认证getSubject

         以上3步需根据你的具体的需要进行编写,比如你的用户认证是token,还是cookie,还是session。所以以下代码仅供参考流程。
         以下代码中,默认约定好由前端是把认证信息放入request的header中的"TOKEN"字段里面。获取了 token以后,不为空的话就直接封装为自定义的shiro用户认证的customUsernamePasswordToken,最后提交认证。当然如果这个过程中有异常,则将信息写入到response响应给前端。
      */
      String accessToken = request.getHeader("TOKEN");
      try {
          if (StringUtils.isEmpty(accessToken)) {
              throw new BusinessException(4010001, "认证token不能为空,请重新登录获取");
           }
          CustomUsernamePassword customUsernamePasswordToken = new CustomUsernamePasswordToken(accessToken);
          getSubject(servletReques,servletResponse).login(customUsernamePasswordToken);
      } catch (BusinessException e) {
          customResponse(servletResponse, e.getCode(), e.getMsg());
          return false;
      } catch (AuthenticationException e){
          if (e.getCause() instanceof BusinessException) {
              BusinessException  exception = (BusinessException)  e.getCause();
              customResponse(servletResponse, exception.getCode(), exception.getMsg());
         } else {
              customResponse(servletResponse, 4010001, "认证token不能为空,请重新登录获取");
         }
         return false;
      } catch (Exception e) {
          customResponse(servletResponse, 5000001, "系统服务异常");
          return false;
      }
      return true;
  }

  /** 自定义响应前端方法
  */
  private void customResponse(ServletResponse response, int code, String msg) {
     try {
         DataResult result = DataResult.getResult(code, msg);
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_AVLUE);
         response.setCharacterEncoding("UTF-8");
         String str = JSON.toJsonString(rresult);
         OutputStream outputStream = response.getOutputStream();
         outputStream.write(str.getBytes("UTF-8"));
         outputStream.flush();
     } catch (IOException e) {
         log.error("CustomResponse_error:{}", e);
     }  
  } 
}

步骤3 自定义Realm:AuthorizingRealm

自定义realm中定义了 用户认证的逻辑,以及通过认证后,用户有哪些角色信息和权限信息,shiro从而来判断该用户 是否可以访问该资源。
1、用户认证业务逻辑:在步骤2的主体提交认证信息后(onAccessDenied中getSubject),会流转到用户认证器(ModularRealmAuthentication),用户认证器会通过自定义域中 的doGetAuthenticationInfo方法获取用户认证凭证
注:用户认证底层代码一开始会通过supports方法判断所传入的AuthenticationToken是否是有效的,因此我们自定义的realm中还需要覆写supports方法。其中shiro里面的doSingleRealmAuthentication的源代码如下:

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    if (!realm.supports(token)) {
         String msg = "Realm [" + realm + "] does not suport authentication token  [" + token + "]";
         throw new UnsupportedTokenException(msg);
    }  else {
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            String msg = "Realm [" + realm + "] wasunable to find account data for the submitted AuthenticationToken [" + token + "]";
            throw new UnknownAccountException(msg);
        } else {
            return info;
        }
    }
}

2、用户授权的业务逻辑:即主体提交授权后(eg:subject.checkRoles(“admin”),@RequiresPermissions(“role:add”)),授权器(ModularRealmAuthorizer)就会通过自定义域的doGetAuthorizationInfo方法获取该用户所拥有的角色信息、权限信息。然后判断该用户是否有权访问该资源。

在shiro包中新建一个CustomRealm继承自AuthorizingRealm,并且覆写其中3个方法:doGetAuthenticationInfo方法、doGetAuthorizationInfo方法、support方法。

public class CustomRealm extends AuthorizingRealm {
    @Override
    public boolean supports (AuthenticationToken token) {
        return token instanceof CustomUsernamePasswordToken;
    }

    @Override
    protected Authentication Info doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //首先把前端传来的token进行强转
        CustomUsernamePasswordToken customUsernamePasswordToken = (CustomUsernamePasswordToken) token;
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(customUsernamePasswordToken.getPrincipal(), customUsernamePasswordToken.getCredentials(), this.getName()); 
        return info;
    }
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 注:在本例中,是将用户角色信息和权限信息在用户第一次登录的时候,就已经写入到token里面了,所以可以通过JwtTokenUtils工具类直接取出来放入SimpleAuthorizationInfo中。
        // 实际开发中,请根据你自己从前端拿到的数据,来判断该用户有哪些角色信息和权限信息,然后放入SimpleAuthorizationInfo中。
        String accessToken = (String) principals.getPrimaryPrincipal();
        Claims claimsFromToken = JwtTokenUtils.getClaimsFromToken(accessToken);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if (claimsFromToken.get("角色信息") != null) {
            info.addStringPermissions((Collection<String> claimsFromToken.get("角色信息")));
        }
        if (claimsFromToken.get("权限信息") != null) {
            info.addStringPermissions((Collection<String> claimsFromToken.get("权限信息")));
        }
        return info;
    }
}

步骤4 自定义认证匹配方法:HashedCredentialsMatcher

shiro在第3步中获取到AuthenticationInfo之后,还需要判断该用户是否有权限可以登录认证,即执行assertCredentialsMatch方法。然后在assertCredentialsMatch方法中,实际是使用CredentialsMatcher类的doCredentialMatch进行判断是否匹配。但是shiro中自带的doCredentialsMatch方法有时并不能满足我们自定义的需求,因此这时也需要我们再自己自定义重写。以下为shiro中部分源码:

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) thorws AuthenticationException {
    // 如果缓存中没有Authentication的话,从第3步骤中自定义的doGetAuthenticationInfo中获取,并且存入缓存中
    Authentication Info info = getCachedAuthenticationInfo(token);
    if (info == null) {
        info = doGetAuthenticationInfo(token);
        log.debug("Looked up AuthnetiocationInfo [{}] from doGetAuthenticationInfo", info);
        if (token != null && info != null) {
            cacheAuthenticationInfoIfPossible(token, info);
        }
    } else {
        log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }

    // 获取到doGetAuthenticationInfo后,再通过assertCredentialsMatch方法验证是否匹配
    if (info != null) {
        assertCredentialsMatch(token, info);
    } else {
        log.debug("No Authnetication Info found for submitted AuthenticationToken [{}]. Returning null.", token);
    }
    
    return info;
}
protected void assertCredentialMatch (AuthenticationToken token, AuthenticationInfo info) thorws AuthenticationException {
    CredentialsMatcher cm = getCredentialsMatcher();
    if (cm != null) {
        if (!cm.doCredentialMatch(token, info)) {
            String msg = "报错信息,此处省略";
            throw new IncorrectCredentialsException(msg);
        }
    } else {
        throw new AuthenticationException("报错西悉尼,此处省略");
    }
}
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = hashProvidedCredentials(token, info);
    Object accountCredentials = getCredentials(info);
    return equals(tokenHashedCredentials, accountCredentials);
}

因此我们在shiro包中自定义CustomHashedCredentialsMatcher继承自HashedCredentialsMatcher, 需要覆写其中的1个方法:doCredentialsMatch方法。

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
    // 本例中使用了redis,可根据你的实际情况进行改写
    @Autowired
    private RedisService redisService;

    // 以下为本例的判断token是否有效的逻辑,具体请根据你的需求逻辑来进行编写。
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        CustomUsernamePasswordToken customUsernamePasswordToken = (CustomUsernamePasswordToken) token;
        String accessToken = (String) customUsernamePasswordToken.getCredentials();
        String userId = JwtTokenUtils.getUserId(accessToken);
        log.info("doCredentialsMatch...userId={}", userId);
        // 判断用户是否删除
        if (redisService.hasKey(Constant.DELETED_USER_KEY + userId)) {
            throw new BusinessException(BaseResponseCode.ACCOUNT_HAS_DELETED_ERROR);
        } 
        // 判断用户是否被锁定
        if (redisService.hasKey(Constant.ACCOUNT_LOCK_KEY + userId)) {
            throw new BusinessException(BaseResponseCode.ACCOUNT_LOCK);
        }
        // 检验token
        if(!JwtTokenUtil.validateToken(accessToken)) {
            throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);
        }
        return true;
    }
}

步骤5 自定义shiro策略配置:ShiroConfig

在config包中新建ShiroConfig类进行shiro的策略配置

@Configuration
public class ShiroConfig {
    @Bean
    public CustomHashedCredentialsMatcher customHashedCredentialsMatcher() {
        return new CustomHashedCredentialsMatcher();
    }
    
    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(customHashedCredentialsMatcher);
        return customRealm;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager defaultWehbSecurityManager = new DefaultWebSecurityManager();
        defaultWehbSecurityManager.setRealm(customRealm());
        return defaultWehbSecurityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        
        LinkedHashMap<String, Filter> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("token", new CustomAccessControlFilter());
        shiroFilterFactoryBean .setFilters(linkedHashMap);
        // 配置拦截策略
        LinkedHashMap<String, String> hashMap = new LinkedHashMap<>();
        hashMap.put("/api/usr/login", "anon");
        hashMap.put("/swagger/**", "anon");
        hashMap.put("/druid/**", "anon");
        hashMap.put("/**", "token,authc");
        shiroFilterFactoryBean.setFilterEFINITIONMap(hashMap);
        return shiroFilterFactoryBean;   
    }

    /** 开启shiro aop注解支持
    */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

你可能感兴趣的:(springboot,shiro,spring,java,过滤器,jwt)