springboot/shiro-jwt权限控制集成

最近整理权限,对springboot整合shiro和jwt遇到的一些问题,简要记录。这里对shiro和jwt不做过多阐述,有兴趣的可以去官网看看。传送门:shiro官网 jwt官网

本文代码地址:码云shiro-jwt

大体思路

  • springboot创建rest-api提供接口
  • 集成shiro,自定义filter实现认证和授权
  • 集成jwt,生成token 编码token 解码token
  • 正确处理认证授权抛出的异常
  • 包装api统一返回值,对api全局异常处理

对shiro和jwt理解的不深,不耍流氓,直接干货了!

pom文件引入shiro和jwt已经fastjson的支持

    
      org.apache.shiro
      shiro-spring-boot-web-starter
      1.4.0
    
    
    
      com.auth0
      java-jwt
      3.4.0
    
    
    
      com.alibaba
      fastjson
      1.2.47
    
自定义realm处理认证和授权的逻辑
/**
   * 设置realm支持的authenticationToken类型
   */
  @Override
  public boolean supports(AuthenticationToken token) {
    return null != token && token instanceof JwtToken;
  }

  /**
   * 登陆认证
   *
   * @param authenticationToken jwtFilter传入的token
   * @return 登陆信息
   * @throws AuthenticationException 未登陆抛出异常
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
      throws AuthenticationException {
    //getCredentials getPrincipal getToken 都是返回jwt生成的token串
    String token = (String) authenticationToken.getCredentials();

    String username = JwtUtils.getUserName(token);
    if (username == null) {
      throw new AccountException("token invalid");
    }
    //如果需要可以根据业务实现db操作,这里根据service写死
    LoginUser loginUser = loginUserService.findByUserName(username);
//    if (loginUser == null) {
//      throw new AuthenticationException("User didn't existed!");
//    }

    if (!JwtUtils.verify(username, loginUser.getPassword(), token)) {
      throw new UnknownAccountException("Username or password error");
    }

    return new SimpleAuthenticationInfo(token, token, getName());
  }

  /**
   * 授权认证
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    String token = principalCollection.toString();
    //根据token获取权限授权
    String userName = JwtUtils.getUserName(token);
    LoginUser loginUser = loginUserService.findByUserName(userName);
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    authorizationInfo.setRoles(loginUser.getRoles());
    authorizationInfo.setStringPermissions(loginUser.getPermissions());
    return authorizationInfo;
  }
自定义token
/**
 * 参照UsernamePasswordToken,用于扩展业务,由于rest api不需要rememberMe,已丢弃
 *
 * @author likai
 * @Date 2018/10/18
 */
public class JwtToken implements HostAuthenticationToken {

  private String username;
  private char[] password;
  private String host;

  private String token;

    ....setter getter省略,重写下面两个方法返回token

@Override
  public Object getPrincipal() {
    return this.getToken();
  }

  @Override
  public Object getCredentials() {
    return this.getToken();
  }
自定义jwtFilter,重写BasicHttpAuthenticationFilter里的方法,

这里说下几个方法,首先跟踪这个Basic 的filter,一路找到PathMatchingFilter的isFilterChainContinued,对拦截器的请求链进行判断,里面调用了onPreHandle方法,AccessControlFilter对它进行了重写,查看方法是对 允许的请求和未允许的请求做预判断,isAccessAllowed方法里面最终调用了Subject.login(req,res);进行判断当前操作者是否有权限,这里我们是用token判断的,用不到,多以我们的请求都是未允许的,只需要重写onAccessDenied进行判断即可

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    return this.isAccessAllowed(request, response, mappedValue) || this.onAccessDenied(request, response, mappedValue);
  }

自定义filter的逻辑是关键,需要重写以下几个方法

onAccessDenied方法逻辑很简单,isLoginAttempt 根据请求header是否携带token判断是否已经登录,成功则执行executeLogin,调用realm方法进行认证token,不成功则直接sendChallenge,返回未登陆出去。具体方法已经重写,可以看代码注释,

/**
 * 自定义拦截器
 *
 * @author likai
 * @Date 2018/10/18
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {

  private static final Logger LOGGER = LoggerFactory.getLogger(JwtFilter.class);

  private static final String AUTHZ_HEADER = "token";
  private static final String CHARSET = "UTF-8";

  /**
   * 处理未经验证的请求
   */
  @Override
  protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
      throws Exception {
    boolean loggedIn = false;
    if (this.isLoginAttempt(request, response)) {
      loggedIn = this.executeLogin(request, response);
    }

    if (!loggedIn) {
      this.sendChallenge(request, response);
    }

    return loggedIn;
  }

  /**
   * 请求是否已经登录(携带token)
   */
  @Override
  protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    String authzHeader = WebUtils.toHttp(request).getHeader(AUTHZ_HEADER);
    return authzHeader != null;
  }

  /**
   * 执行登录方法(由自定义realm判断,吃掉异常返回false)
   */
  @Override
  protected boolean executeLogin(ServletRequest request, ServletResponse response)
      throws Exception {
    String token = WebUtils.toHttp(request).getHeader(AUTHZ_HEADER);
    if (null == token) {
      String msg = "executeLogin method token must not be null";
      throw new IllegalStateException(msg);
    }
    //交给realm判断是否有权限,没权限返回false交给onAccessDenied
    JwtToken jwtToken = new JwtToken(token);
    try {
      this.getSubject(request, response).login(jwtToken);
      return true;
    } catch (AuthenticationException e) {
      return false;
    }
  }

  /**
   * 构建未授权的请求返回,filter层的异常不受exceptionAdvice控制,这里返回401,把返回的json丢到response中
   */
  @Override
  protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
    HttpServletResponse httpResponse = WebUtils.toHttp(response);
    String contentType = "application/json;charset=" + CHARSET;
    httpResponse.setStatus(Code.UNAUTHENTICATED);
    httpResponse.setContentType(contentType);
    try {
      String msg = "对不起,您无权限进行操作!";
      RestResponse unauthentication = RestResponse.newBuilder()
          .setCode(Code.UNAUTHENTICATED)
          .setMsg(msg).build();
      PrintWriter printWriter = httpResponse.getWriter();
      printWriter.append(JSON.toJSONString(unauthentication));
//      byte[] data = unauthentication.toString().getBytes(CHARSET);
//      OutputStream outputStream = httpResponse.getOutputStream();
//      outputStream.write(data);
    } catch (IOException e) {
      LOGGER.error("sendChallenge error,can not resolve httpServletResponse");
    }

    return false;
  }

  /**
   * 请求前处理,处理跨域
   */
  @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请求直接返回正常状态
    if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
      httpServletResponse.setStatus(HttpStatus.OK.value());
      return false;
    }
    return super.preHandle(request, response);
  }
}

自定义了token filter realm,接下来只需要把这些自定的bean,放入shiro的配置中去,

/**
 * @author likai
 * @Date 2018/10/18
 */
@Configuration
public class ShiroConfig {

  private static final String JWT_FILTER_NAME = "jwt";

  /**
   * 自定义realm,实现登录授权流程
   */
  @Bean
  public Realm authRealm() {
    return new AuthRealm();
  }

  /**
   * 配置securityManager 管理subject(默认),并把自定义realm交由manager
   */
  @Bean
  public DefaultSecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(authRealm());
    //非web关闭sessionManager(官网有介绍)
    DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
    DefaultSessionStorageEvaluator storageEvaluator = new DefaultSessionStorageEvaluator();
    storageEvaluator.setSessionStorageEnabled(false);
    defaultSubjectDAO.setSessionStorageEvaluator(storageEvaluator);
    securityManager.setSubjectDAO(defaultSubjectDAO);

    return securityManager;
  }

  /**
   * 拦截链
   */
  @Bean
  public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultSecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    shiroFilterFactoryBean.setFilters(filterMap());
    shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap());

    return shiroFilterFactoryBean;
  }

  /**
   * 自定义拦截器,处理所有请求
   */
  private Map filterMap() {
    Map filterMap = new HashMap<>();
    filterMap.put(JWT_FILTER_NAME, new JwtFilter());
    return filterMap;
  }

  /**
   * url拦截规则
   */
  private Map definitionMap() {
    Map definitionMap = new HashMap<>();
    definitionMap.put("/login", "anon");
    definitionMap.put("/**", JWT_FILTER_NAME);
    return definitionMap;
  }

  /**
   * 开启注解
   */
  @Bean
  @DependsOn("lifecycleBeanPostProcessor")
  public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    // 强制使用cglib代理,防止和aop冲突
    defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
    return defaultAdvisorAutoProxyCreator;
  }

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

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

至此,shiro和jwt已经全部集成了,在controller上加入注解就可以验证了,其他代码参见git和码云:码云重送门 github重送门git挂了稍后补充地址
本文只是简单做了demo,如有不正之处请联系作者或提出Issues进行修改,万分感谢!
微信:

springboot/shiro-jwt权限控制集成_第1张图片
728961272908841360.jpg

你可能感兴趣的:(springboot/shiro-jwt权限控制集成)