博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
springMVC整合shiro权限框架示例与实践
阅读量:6569 次
发布时间:2019-06-24

本文共 13435 字,大约阅读时间需要 44 分钟。

hot3.png

为什么写这篇文章

看过那么多框架、教程,大部分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,其他不相关内容省略

shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
targetBeanName
shiroFilter
shiroFilter
/*

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

/assets/** = anon
/api/sys/login = anon /api/sys/logout = anon /login.html = anon
/** = authc
/refuse

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
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
permissions = (List
) 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
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注解方法 */@ControllerAdvicepublic 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
{ @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,肯定要

1.8
5.1.7.RELEASE
5.4.2.Final
org.apache.shiro
shiro-core
1.4.1
org.apache.shiro
shiro-ehcache
1.4.1
org.apache.shiro
shiro-web
1.4.1
org.apache.shiro
shiro-spring
1.4.1

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

你可能感兴趣的文章
Python学习第一天
查看>>
mysql中利用游标遍历表中的数据时未正常获取数据
查看>>
Iptables模块recent应用
查看>>
Linux Ext3、Ext4误删文件恢复 extundelete
查看>>
AIX添加硬盘后做rootvg镜像及取消
查看>>
Windows性能计数器
查看>>
NX支持的环境变量(转)
查看>>
基于javascript的Ajax
查看>>
ORACLE中序列问题
查看>>
我的友情链接
查看>>
[实践]Sonar Xcode8兼容
查看>>
数字金额到汉字的转换
查看>>
PostgreSQL 9.4版本的物化视图更新
查看>>
userdel命令
查看>>
linux /etc/inittab 的六个运行级别简单理解
查看>>
Ubuntu Server 14.04 ftp
查看>>
linux系统wc命令详解
查看>>
Windows7 下边查看80端口被哪个服务占用了
查看>>
进度条(HUD) 第三方库
查看>>
CentOS 5.5升级内核到2.6.28并支持XFS/L7
查看>>