springMVC整合shiro权限框架示例与实践

为什么写这篇文章

看过那么多框架、教程,大部分shiro的文章或教程是我见过思路最糟糕的。作者完不清楚想要表达什么起到什么作用,把大段大段的理论讲一通。你见过哪个java教程上来就给你讲一堆基础类库,讲虚拟机的。或者hibernate教程上来就给讲他有的设计有多精妙,管理的东西有多庞大的。

然后我曾经硬着头看了1周的所谓shiro教程,看完发现自己还是什么都不会,什么也做不出来。倍受打击。当年初学时看think in java都没这么失落过。

后来想想不对,就直接去找spring整合shiro的教程。折腾了一周总算做出来一个可以项目实用的东西了。但中间走过不少坑,其中有些可能是作者漏了,还有些是因为我也是整合shiro的要适应项目里的各个东西,适合自己项目的用法(这里吐槽一下,shiro会乱的原因就是配置的方式太多种了,而且好多文章都力求全面讲,对于一个项目真不需要全用到)。 不要跟我讲什么使用文本管理配置权限,什么写根据角色控制访问,哪个能用的项目会这么搞。浪费lz时间。 说什么从简单入手,你这个简单没鸟用,我后要改成从数据库读权限列表,读角色,根本就不可能在你这个简单的例子上逐渐改造,这还不是浪费时间还是什么,而且会用到你这框架的人,自己没几个项目拿来练手么。这不是浪费时间是什么。

另外各文章或教程,shiro的运行原理或者方式,只字未提,极力各种介绍概念。喂,我们不是搞学术的。后面看有些文章会把各个过程中加入作用说明和自己的理解的话,这个还挺不错的。但一直心里有一个疑问困扰着我,shiro是基于session(sessionId),还是基于tokken(每次访问都要传),因为我一直不知道前端要传什么参数,登录功能完成后,一直调不通登录之后的接口,初学时也很多东西不知道。讽刺的是这个答案要等自己调通了才知道。答案:shiro是基于session(sessionId),至少默认是这样的,tokken方式我没研究过(也不是我的菜,总感觉性能太差了,心生厌恶。流量不大,小项目还是session好用)。 真·心累。写这篇文章就是为了解救像我之前一样迷茫的同学。

解决什么问题

搭建一个项目可以用的shiro。 集成以下内容,使用相同内容的同学可以直接搬过去用了:

  1. spring,使用xml配置。喜欢注解配置的可以自己改造或找别的文章,个别文章我也是根据注解去配置xml的,看得懂就行。
  2. springMVC
  3. ACL(访问控制列表,用户角色权限都有),从数据库读取,在系统中是可配置的。
  4. 基于注解在contrllor方法之上的。这是我使用shiro的最初需求,因为restfull API中的通配符和path value,使用传统url拦截器(或过滤器),识别和判断很麻烦,检测太仔细又怕性能太差。
  5. ORM什么的随便,这个基本和shiro没关系,我这里用了hibernate
  6. 前后端分离,返回json数据。所以不需要跳转(未登录或没权限时)。

执行步骤

  1. 访问服务器,web.xml中的shiroFilter拦截,
  2. 拦截进入applicationContext-shiro.xml中配置的shiroFilter。在filterChainDefinitions中配置,/api/sys/login = anon 开放登陆接口,可以直接访问。
  3. 调用Login2Controller中的接口login方法,在方法中,账号和密码生成token,执行登录subject.login(token)
  4. 调用MyRealm#doGetAuthenticationInfo方法,验证登录信息。如果登录失败,抛出异常throw new UnknownAccountException("账号或密码错误")。如果登录成功,返回一个由用户信息、密码初始化的 SimpleAuthenticationInfo
  5. 回到login方法。处理登录失败和成功的情况:登录失败时这里直接抛出异常,会有@ControllerAdvice统一处理springmvc的异常,返回json数据。登录成功会把一些当前登录用户信息放到session中方便取用提高程序性能,如用户信息、部门、角色、权限等。要存在哪里看具体情况,普通sesssion、shiro的session,我直接存在principal里。到这里完成登录。
  6. 访问需要登录的接口,看applicationContext-shiro.xml中配置的shiroFilter,不是= anon的接口。会调用配置的shiroLoginFilter。
  7. 执行ShiroLoginFilter#isAccessAllowed方法,判断是否已登录。如果未登录执行下面的onAccessDenied方法,自定义返回结果。
  8. 如果访问的接口有配置(注解)权限,会调用MyRealm#doGetAuthorizationInfo方法,组织权限列表和controller方法上注解的@RequiresPermissions("user")匹配,匹配失败时会抛出UnauthorizedException异常,这个由@ControllerAdvice统一处理并返回json数据。如果匹配权限通过则正常返回接口执行结果。
  9. 退出登录,调用此方法SecurityUtils.getSubject().logout()。

示例

文件列表或涉及配置文件
  1. shiroFilter 在web.xml配置过滤器,shiro的入口
  2. applicationContext-shiro.xml,shiro在spring中的配置信息,由spring引入。
  3. MyRealm 自定义登录的判断doGetAuthenticationInfo(登录时执行一次),以及权限列表的组装doGetAuthorizationInfo(每次访问需要权限的接口都会执行)。在applicationContext-shiro.xml中配置
  4. ShiroLoginFilter.java,是否已登录判断和未登录的处理,这里是返回json。
  5. Login2Controller 登录的接口#login,还有登出的接口#logout,以及当前登录信息的获取。
关键代码

WEB-INF/web.xml,其他不相关内容省略

<web-app>
  <filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <!-- 设置true由servlet容器控制filter的生命周期 -->
    <init-param>
      <param-name>targetFilterLifecycle</param-name>
      <param-value>true</param-value>
    </init-param>
    <!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean-->
    <init-param>
      <param-name>targetBeanName</param-name>
      <param-value>shiroFilter</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

applicationContext-shiro.xml,独立的文件引入到spring的配置中,可以在web.xml中引入也可以在总的applicationContext.xml import

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

    <!-- id属性值要对应 web.xml中shiro的filter对应的bean -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"></property>
        <property name="filters">
            <util:map>
                <entry key="authc" value-ref="shiroLoginFilter" />
            </util:map>
        </property>
        <!-- 未登录跳转页面,请求地址将由formAuthenticationFilter进行表单认证 -->
        <!-- 本项目通过ajax访问,由ShiroLoginFilter中处理返回json信息 -->
        <!--<property name="loginUrl" value="/notLogin"></property>-->

        <!-- 认证成功统一跳转到页面,建议不配置,shiro认证成功会默认跳转到上一个请求路径 -->
        <!-- 本项目通过ajax访问,loginController#loging中直接返回json信息 -->
        <!-- <property name="successUrl" value="/first.action"></property> -->

        <!-- 通过unauthorizedUrl指定没有权限操作时跳转页面,这个位置会拦截不到,下面有给出解决方法 -->
        <!-- 本项目通过ajax访问,由BaseController中@ExceptionHandler捕获异常处理 -->
        <!--<property name="unauthorizedUrl" value="/refuse"></property>-->

        <!-- 过滤器定义,从上到下执行,一般将/**放在最下面 -->
        <property name="filterChainDefinitions">
            <value>
                <!-- 对静态资源设置匿名访问 -->
                /assets/** = anon
                <!--开放登陆接口-->
                /api/sys/login = anon
                /api/sys/logout = anon
                /login.html = anon
                <!-- /**=authc 所有的url都必须通过认证才可以访问 -->
                /** = authc
                <!-- /**=anon 所有的url都可以匿名访问,不能配置在最后一排,不然所有的请求都不会拦截 -->
            </value>
        </property>
    </bean>
    <!--使用ajax访问,自定义未登录返回信息-->
    <bean id="shiroLoginFilter" class="com.hammer.acl.shiro.ShiroLoginFilter"></bean>

    <!-- 解决shiro配置的没有权限访问时,unauthorizedUrl不跳转到指定路径的问题 -->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.apache.shiro.authz.UnauthorizedException">/refuse</prop>
            </props>
        </property>
    </bean>

    <!-- securityManager安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
    </bean>

    <!-- 配置自定义Realm -->
    <bean id="myRealm" class="com.hammer.acl.shiro.MyRealm">
        <!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
        <property name="credentialsMatcher" ref="credentialsMatcher"></property>
    </bean>

    <!-- 凭证匹配器 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <!-- 加密算法 -->
        <property name="hashAlgorithmName" value="md5"></property>
        <!-- 迭代次数 -->
        <property name="hashIterations" value="1"></property>
    </bean>
</beans>

MyRealm.java

/**
 * 自定义的Realm
 */
public class MyRealm extends AuthorizingRealm {

  @Autowired
  private UserService userService;
  @Autowired
  private LoginService loginService;

  // 设置realm的名称
  @Override
  public void setName(String name) {
    super.setName("customRealm");
  }

  /**
   * 认证的方法,登录时执行
   *
   * @param token
   * @return
   * @throws AuthenticationException
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //System.out.println("————身份认证方法————");
    // token是用户输入的用户名和密码
    // 第一步从token中取出用户名
    final String loginId = (String) token.getPrincipal();
    String password = null;
    final Object credentials = token.getCredentials();
    if (credentials instanceof char[]) {
      password = new String((char[]) credentials);
    }

    // 第二步:根据用户输入从数据库查询用户信息
    User user = loginService.getUse4Login(loginId, password);
    if (user == null) {
      throw new UnknownAccountException("账号或密码错误");
    }

    // 从数据库查询到密码
    //配合shiro配置的mc5加密(应该可以配置为不加密)
    if (password != null) {
      password = DigestUtils.md5Hex(password);
    }
    //加密的盐
    //String salt = user.getSalt();

    final HashMap<String, Object> principal = new HashMap<>();
    principal.put("user", user);
    return new SimpleAuthenticationInfo(principal, password, this.getName());
  }

  /**
   * 授权的方法,每次访问需要权限的接口都会执行
   *
   * @param principals
   * @return
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    //System.out.println("————权限认证————");
    //从principals获取主身份信息
    //将getPrimaryPrincipal方法返回值转为真实身份类型(在上边doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中的身份类型)

    //以下方法等效SecurityUtils.getSubject().getPrincipal() principals.getPrimaryPrincipal()
    //Map principal = (Map) SecurityUtils.getSubject().getPrincipal();
    Map principal = (Map) principals.getPrimaryPrincipal();
    User user = (User) principal.get("user");
    List<String> permissions = (List<String>) principal.get("permissions");

    //查到权限数据,返回授权信息(要包括上边的permissions)
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    simpleAuthorizationInfo.addStringPermissions(permissions);//这里添加用户有的权限列表
    simpleAuthorizationInfo.addRole(user.getRoleId());//这里添加用户所拥有的角色

    return simpleAuthorizationInfo;
  }

}

ShiroLoginFilter.java

public class ShiroLoginFilter extends FormAuthenticationFilter {
  private static final Logger log = LoggerFactory.getLogger(ShiroLoginFilter.class);
  /**
   * 如果isAccessAllowed返回false 则执行onAccessDenied
   *
   * @param request
   * @param response
   * @param mappedValue
   * @return
   */
  @Override
  protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    boolean isAllowed = false;
    //前端(某些框架)测试接口(OPTIONS)直接放行
    if (request instanceof HttpServletRequest) {
      if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
        isAllowed = true;
      }
    }
    isAllowed = super.isAccessAllowed(request, response, mappedValue);
    if (isAllowed) {
		//登录状态,作一些日志记录
    }
    return isAllowed;
  }

  /**
   * 未登录时的处理
   *
   * @param request
   * @param response
   * @return true-继续往下执行,false-该filter过滤器已经处理,不继续执行其他过滤器
   * @throws Exception
   */
  @Override
  protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    log.info("用户未登录");
    final HttpServletRequest request2 = (HttpServletRequest) request;
    final HttpServletResponse response2 = (HttpServletResponse) response;

    //ajax访问接口返回数据结构
    if (WebUtil.isAjax(request2)) {// ajax接口
      //这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
      response2.setHeader("Access-Control-Allow-Origin", request2.getHeader("Origin"));
      response2.setHeader("Access-Control-Allow-Credentials", "true");
      response2.setCharacterEncoding("UTF-8");
      response2.setContentType("application/json");
	  
	  Map responseData = new HashMap();
      responseData.put("state", "unauthorized");
	  responseData.put("code", 401);
	  responseData.put("msg", "用户未登录");
     
	  String result = Json.toJson(responseData);
	  PrintWriter out;
      try {
        out = response2.getWriter();
        out.print(result.toString());
        out.flush();
      } catch (IOException e) {
        log.error("返回数据失败!", e);
      }
    } else {
      //其他情况
      //shiro处理
      super.onAccessDenied(request, response);

      //其他处理方式

      // 页面,直接跳转登录页面
      //redirect("login.html", request2, response2);

      //web.xml处理
      //response2.setStatus(401);// 客户试图未经授权访问受密码保护的页面。
    }
    return false;
  }
}

Login2Controller.java

/**
 * shiro登录
 */
@Slf4j
@RestController
@RequestMapping("/api/sys")
public class Login2Controller{
  @Autowired
  private LoginService service;

  @Autowired
  private LoginService loginService;


  /**
   * 登陆
   *
   * @param loginId  登录账号
   * @param password 密码
   */
  @RequestMapping(value = "/login")
  public RespObject login(String loginId, String password, HttpServletRequest req) {
    final String host = req.getRemoteHost();
    // 在认证提交前准备 token(令牌)
    UsernamePasswordToken token = new UsernamePasswordToken(loginId, password, host);
    try {
      // 从SecurityUtils里边创建一个 subject
      Subject subject = SecurityUtils.getSubject();
      // 执行认证登陆
      subject.login(token);
      //set session attribute
      final Map principal = (Map) subject.getPrincipal();
      User user = (User) principal.get("user");
	  // loginService.buildSessionAttr方法生成了包含 List<String> permissions,key为"permissions";
      final Map sessionAttrs = loginService.buildSessionAttr(user);
      principal.putAll(sessionAttrs);
    } catch (UnknownAccountException e) {
      final String message = e.getMessage();
      log.info(String.format("%s[%s/%s]", message, loginId, password));
      throw new FailException(message);
    } catch (AuthenticationException e) {
      throw new FailException(e);
    }
    return RespObject.success(null, "登录成功");
  }

  /**
   * 退出
   *
   * @return
   */
  @RequestMapping(value = "/logout", method = RequestMethod.GET)
  public RespObject logout() {
    Subject subject = SecurityUtils.getSubject();
    //注销
    subject.logout();
    return RespObject.success(null, "成功注销!");
  }


  /**
   * 当前session属性
   *
   * @param attr
   * @return
   */
  @RequestMapping(value = "/current")
  public RespObject getCurrentAttr(String attr) {
    Map sessionAttrs = service.readSessionAttr();
    if (Strings.isEmpty(attr)) {
      return RespObject.success(sessionAttrs);
    } else {
      return RespObject.success(sessionAttrs.get(attr));
    }
  }
其他代码

MyControllerAdvice.java,统一处理spring MVC异常,代码里有些调用别地的常用处理方法,根据实际情况修改。

/**
 * controller 增强器,应用到所有@RequestMapping注解方法
 */
@ControllerAdvice
public class MyControllerAdvice {
  private static final Logger log = LoggerFactory.getLogger(MyControllerAdvice.class);
  @ExceptionHandler
  @ResponseBody
  public Object errorHandler(HttpServletRequest request, Exception e, HttpServletResponse response) {
    //记录日志
    if (e instanceof UnauthorizedException) {
      //没有权限
      String uri = request.getServletPath();
      final String queryString = request.getQueryString();
      if (null != queryString && queryString.trim().length() > 0) {
        uri = uri + "?" + queryString;
      }
      log.info(String.format("%s, [uri = %s]", e.getMessage(), uri));
    } else {
      if (e instanceof BaseException) {
        log.error(e.getMessage());
      } else {
        log.error("异常错误", e);
      }
    }
    Throwable e2 = WebUtil.deepestException(e);
    try {
      // 是否ajax调用
      boolean isAjax = true;
      if (WebUtil.isAjax(request)) {
        RespObject respObject;
        if (e instanceof UnauthorizedException) {
          respObject = RespObject.forbidden();
        } else if (e instanceof FailException) {
          respObject = RespObject.fail(RespObject.getExceptionMessage(e2));
        } else if (e instanceof ErrorException) {
          respObject = RespObject.error(e2);
        } else {
          respObject = RespObject.exception(e2);
        }
        respObject.setExtra(e2.getMessage());
        return respObject;
      } else {
        // 添加自己的异常处理逻辑,如日志记录
        request.setAttribute("exceptionMessage", e.getMessage());
        return "common/error";
      }
    } catch (Exception e3) {
      log.error("返回数据失败!", e3);
    }
    return "common/error";
  }
}

UserController.java,测试接口调用

@RestController
@RequestMapping("/api/base/user")
public class UserController extends BaseController<User, String> {
  @RequiresPermissions(value = {"user"}, logical = Logical.OR)//执行此方法需要权限
  @RequestMapping(value = "/search")
  public RespObject search(PageParam pageParam, User bean) {
    return super.search(pageParam, bean);
  }
}

其他注意

  • 有一个坑是applicationContext-shiro.xml中,shiroFilter <property name="filters"> 配置的<entry key="authc" value-ref="shiroLoginFilter" />,这里的key="authc"不能随便乱写,有些文章是随便写的,会导致不会调shiroLoginFilter.java中的方法。

引入jar包或版本信息

spring相关的包怎么引的随便找,使用shiro这里aop,肯定要

<java.version>1.8</java.version>
<spring.version>5.1.7.RELEASE</spring.version>
<hibernate.version>5.4.2.Final</hibernate.version>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.4.1</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-ehcache</artifactId>
  <version>1.4.1</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-web</artifactId>
  <version>1.4.1</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.4.1</version>
</dependency>

转载于:https://my.oschina.net/u/2438634/blog/3058882

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值