Apache Shiro
强大易用的 Java 安全框架,身份验证、授权、密码和会话管理。不跟任何框架或者容器绑定,可以独立运行。
组件
作用
Subject(用户)
认证主体,相当于当前操作用户。它可以是任何事物,比如程序等 Subject currentUser = SecurityUtils.getSubject();
类似 Employee user = SessionUtil.getAttribute(Employee.class);
一旦获得 Subject,就可以立即使用 Shrio 为当前用户做事,比如登录、登出、访问会话、执行授权等
SecurityManager 安全管理器
它是 Shiro 功能实现的核心,负责与其他组件(认证器/授权器/缓存控制器)进行交互,实现 Subject 委托的各种功能。类似于 SpringMVC 中的 DispatcherServlet 前端控制器
Realms 数据源
Realm 是 Shiro 与应用数据之间的桥梁,可以把 Realm 看成连接池 DataSource,执行认证(登录)和授权(访问控制)时,Shiro 会从应用配置的 Realm 中查找相关的比对数据,确认用户是否合法,操作是否合理
Authenticator 认证
协调一个或者多个 Realm,从 Realm 指定的数据源取得数据之后执行具体的认证
Authorizer 授权
决定用户是否拥有执行指定操作的权限
SessionManager
Shiro 支持会话管理,在安全框架中是独一无二的功能,即便不存在 web 容器环境,Shiro 都可以使用自己的会话管理机制,提供相同的会话 API
CacheManager 缓存组件
缓存认证信息
Cryptography 加密组件
Shiro 提供了加密解密的命令行工具 jar 包,需要单独下载使用
Shiro 架构的核心(Subject,SecurityManager,Realms)
Shiro 在应用中最常用的两个功能:用户登录认证和访问授权。
基于 ini 的 Shiro 认证
Shiro 认证概述,认证的过程为用户的身份确认过程。
上图表明了用户访问的认证流程,是否需要认证和是否已经通过认证,最关键的是如何认证。
准备工作:导入 jar 包
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > commons-logging</groupId > <artifactId > commons-logging</artifactId > <version > 1.1.3</version > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-core</artifactId > <version > 1.4.1</version > </dependency >
编写 ini 配置文件: shiro-permission.ini
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [users] zhang =123 ,role1,role2wang =123 ,role3[roles] role1 =user:create,user:updaterole2 =user:create,user:deleterole3 =user:createuser:create:01 表示对用户资源的01实例进行create操作。 user:create:表示对用户资源进行create操作,相当于user:create:*,对所有用户资源实例进行create操作。 user:*:01 表示对用户资源实例01进行所有操作。
shiro-permi.ini
1 2 3 4 5 6 7 8 [users] zhang =123 lisi =555 crmRealm =shiro.CRMRealmsecurityManager.realms =$crmRealm
使用 Shiro 相关的 API 完成身份认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @Test public void testShiro () { Factory<SecurityManager> factory = new IniSecurityManagerFactory( "classpath:shiro-permission.ini" ); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wang" , "123" ); try { subject.login(token); } catch (AuthenticationException e) { e.printStackTrace(); } System.out.println("认证状态:" + subject.isAuthenticated()); boolean ishasRole = subject.hasRole("role1" ); System.out.println("单个角色判断" + ishasRole); boolean hasAllRoles = subject.hasAllRoles(Arrays.asList("role1" , "role2" , "role3" )); System.out.println("多个角色判断" + hasAllRoles); boolean isPermitted = subject.isPermitted("user:create:1" ); System.out.println("单个权限判断" + isPermitted); boolean isPermittedAll = subject.isPermittedAll("user:create:1" , "user:delete" ); System.out.println("多个权限判断" + isPermittedAll); subject.checkPermission("items:create:1" ); } @Test public void test2 () { IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro-permi.ini" ); SecurityManager manager = factory.getInstance(); SecurityUtils.setSecurityManager(manager); Subject subject = SecurityUtils.getSubject(); System.out.println("登录前的用户认证状态" + subject.isAuthenticated()); UsernamePasswordToken token = new UsernamePasswordToken("zhang" , "123" ); subject.login(token); System.out.println("登录后用户认证状态" + subject.isAuthenticated()); }
Shiro 认证流程分析:
自定义 Realm
实际开发中,需要的账户信息来自于数据库或者程序中,而不是 ini 配置文件。 上面的例子中,通过源码可以看到,使用的是 SimpleAccountRealm 类来处理账户的认证和授权的。其实现为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; SimpleAccount account = getUser(upToken.getUsername()); if (account != null ) { if (account.isLocked()) { throw new LockedAccountException("Account [" + account + "] is locked." ); } if (account.isCredentialsExpired()) { String msg = "The credentials for account [" + account + "] are expired" ; throw new ExpiredCredentialsException(msg); } } return account; } protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) { String username = getUsername(principals); USERS_LOCK.readLock().lock(); try { return this .users.get(username); } finally { USERS_LOCK.readLock().unlock(); } }
在 AuthenticatingRealm 中调用该方法判断账号是否正确,如果返回的 account 不为空,说明账号存在,然后进行密码校验。
覆写 doGetAuthenticationInfo() 方法,在该方法中实现账号的校验,返回 AuthenticationInfo 对象给上层调用者。
上图为 Realm 的继承体系。我们需要继承 AuthorizingRealm,因为我们会使用缓存、认证和授权的所有功能。
创建自己的 Realm 文件,继承 AuthorizingRealm。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class CRMRealm extends AuthorizingRealm { public AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { String username = "111" ; String password = "555" ; if (username.equals(token.getPrincipal())) { return new SimpleAuthenticationInfo(username, password, this .getName()); } return null ; } public AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) { return null ; } }
然后需要告诉 Shiro 认证或者授权时,去找哪个类完成功能。修改 SecurityManager 中的默认 Realm 的使用
shiro.ini
1 2 3 4 crmRealm =shiro.CRMRealmsecurityManager.realms =$crmRealm
运行与之前测试结果一致,但现在是我们自己自定义的 Realm 完成了账号的校验。
Spring 与 Shiro 整合
导入 jar 包,shiro-web、shiro-spring、shiro-code
shiro 通过 filter 进行拦截,拦截后将操作权交给 spring 中配置的 filterChain 进行过滤。
web.xml
1 2 3 4 5 6 7 8 9 10 11 <filter > <filter-name > shiroFilter</filter-name > <filter-class > org.springframework.web.filter.DelegatingFilterProxy </filter-class > </filter > <filter-mapping > <filter-name > shiroFilter</filter-name > <url-pattern > /*</url-pattern > </filter-mapping >
shiro过滤器,DelegatingFilterProxy 会从 spring 容器中找 shiroFilter,过滤器生命周期交给 Spring 容器管理。 Shiro 中定义了多个过滤器完成不同的预处理操作:
所有的过滤器导入的包为 org.apache.shiro.web.filter.authc.
过滤器名称
过滤器
理解
anon
AnonymousFilter
匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 /static/**=anon
authc
FormAuthenticationFilter
表示需要认证(登录)才能使用;示例 /**=authc
主要属性下面有说明
authcBasic
BasicHttpAuthenticationFilter
Basic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application)
roles
RolesAuthorizationFilter
角色授权拦截器,验证用户是否拥有资源角色;示例 /admin/=roles[admin]
perms
PermissionsAuthorizationFilter
权限授权拦截器,验证用户是否拥有资源权限;示例 /employee/input=perms["user:update"]
user
UserFilter
用户拦截器,用户已经身份验证/记住 我登录的都可;示例/index=user
logout
LogoutFilter
注销拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例 /logout=logout
port
PortFilter
端口拦截器,主要属性 port(80),可以通过的端口;示例 /test= port[80]
如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
rest
HttpMethodPermissionFilter
rest风格拦截器,自动根据请求方法构建权限字符串
ssl
SslFilter
ssl:SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443)其他和port拦截器一样
HttpMethodPermissionFilter : rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read,POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串,示例 /users=rest[user]
,会自动拼出 user:read,user:create,user:update,user:delete
权限字符串进行权限匹配(所有都得匹配isPermittedAll)
CRMRealm.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component ("crmRealm" )public class CRMRealm extends AuthorizingRealm { public AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { String username = "zhang" ; String password = "123" ; if (username.equals(token.getPrincipal())) { return new SimpleAuthenticationInfo(username, password, this .getName()); } return null ; } public AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) { return null ; } }
shiro.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <context:component-scan base-package ="cn.lizhaoloveit.crm.realm" /> <bean id ="securityManager" class ="org.apache.shiro.web.mgt.DefaultWebSecurityManager" > <property name ="realm" ref ="crmRealm" /> </bean > <bean id ="shiroFilter" class ="org.apache.shiro.spring.web.ShiroFilterFactoryBean" > <property name ="securityManager" ref ="securityManager" /> <property name ="loginUrl" value ="/login.html" /> <property name ="successUrl" value ="/first.action" /> <property name ="unauthorizedUrl" value ="/refuse.jsp" /> <property name ="filters" > <map > <entry key ="authc" value-ref ="crmFormAuthenticationFilter" /> </map > </property > <property name ="filterChainDefinitions" > <value > /css/** = anon /js/** = anon /images/** = anon /uploads/** = anon /** = authc </value > </property > </bean >
步骤:
在 web.xml 文件中配置 shiro 的过滤器
在对应的 spring 配置文件中配置与 web.xml 中对应的 filter
保证静态资源不被拦截 /css/** = anon、/js/** = anon、/images/** = anon
匿名可以访问
配置安全管理器,注入自定义的 realm
配置自定义的 realm
交验完,告知前端页面校验结果
有了上面的配置,在访问达到具体资源前,会通过制定的过滤器做预处理,允许通过后才能继续访问。在 JavaEE 环境中,使用过的安全管理器是 DefaultWebSecurityManager
,并且在该安全管理器中指定我们自定义的 Realm。需要将自定义的 realm 交给 spring 容器管理,之后 shiro 就会通过自定义的 realm 进行账号认证和授权了。
如果你使用 Dispatcher 配置路径 url-pattern 为 /,且 mvc 中配置了 <mvc:default-servlet-handler/>
就一定记住,动态资源访问名与静态资源访问名不能同名。
默认情况下,会调用 FormAuthenticationFilter 中的 onLoginFailure 和 onloginSuccess 对成功和失败的结果进行处理,而 shiro 不知道失败或者成功或者权限被拒后要做什么,需要我们去告诉它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component ("crmFormAuthenticationFilter" )public class CRMFormAuthenticationFilter extends FormAuthenticationFilter { @Override protected boolean onLoginSuccess (AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { response.setContentType("application/json;charset=UTF-8" ); JsonResult result = new JsonResult(); response.getWriter().print(JSON.toJSONString(result)); return false ; } @Override protected boolean onLoginFailure (AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { response.setContentType("application/json;charset=UTF-8" ); JsonResult result = new JsonResult(); try { result.mark("用户名或者密码错误" ); response.getWriter().print(JSON.toJSONString(result)); } catch (IOException e1) { e1.printStackTrace(); } return false ; } }
最后在 Realm 中实现真实的查询即可。
CRMReam.java
1 2 3 4 5 6 7 8 9 10 11 12 public class CRMRealm extends AuthorizingRealm {... public AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { ... String username = (String) token.getPrincipal(); Employee employee = mapper.selectByName(username); if (employee != null ) { return new SimpleAuthenticationInfo(username, employee.getPassword(), this .getName()); } } }
Shiro 制定化认证登录
从 Shiro 的认证体系可以看出来,FormAuthenticationFilter 中通过认证参数获取的几个参数是固定的,封装的认证数据类型为 UsernamePasswordToken ,UsernamePasswordToken 中只有 username,password,rememberMe,这显然不能满足需求,因此我们在覆写 FormAuthenticationFilter.java 时,也要扩展 Token 类,以提供需要的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class CRMToken extends UsernamePasswordToken { private static final long serialVersionUID = -2804050723838289739L ; private String captchaCode; public CRMToken (String username, String password, String captchaCode) { super (username, password, false , "" ); this .captchaCode = captchaCode; } public String getCaptchaCode () { return captchaCode; } }
必须在覆写的 FormAuthenticationFilter 中,重写 createToken 方法来创建我们自己的 Token 对象,否则不会有扩展。
CRMFormAuthenticationFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class CRMFormAuthenticationFilter extends FormAuthenticationFilter { @Override protected AuthenticationToken createToken (String username, String password, ServletRequest request, ServletResponse response) { String captchaCode = request.getParameter("captchaCode" ); return new CRMToken(username, password, captchaCode); } ... }
然后使用自己的数据模块来进行认证,CRMRealm 继承 org.apache.shiro.realm.AuthorizingRealm.java 类,代码如下: CRMRealm.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Component ("crmRealm" )public class CRMRealm extends AuthorizingRealm { @Autowired private EmployeeMapper mapper; public AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { CRMToken crmToken = (CRMToken) token; String loginCaptcha = crmToken.getCaptchaCode(); if (StringUtil.isEmptyStr(loginCaptcha)) { throw new CaptchaEmptyException(); } Subject subject = SecurityUtils.getSubject(); String sessionCaptcha = (String) subject .getSession().getAttribute("RANDOMCODE_IN_SESSION" ); if (!loginCaptcha.equals(sessionCaptcha)) { throw new CaptchaErrorException(); } String username = (String) token.getPrincipal(); Employee employee = mapper.selectByName(username); if (employee != null ) { return new SimpleAuthenticationInfo(employee, employee.getPassword(), this .getName()); } return null ; } public AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) { return null ; } }
CRMRealm 做的是用户名认证,会将用户名密码传递给下一个认证链,如果抛出异常(AuthenticationException类型),就会返回,并调用 FormAuthenticationFilter 的 onLoginSuccess
和 onLoginFailure
方法将结果响应给前端浏览器。所以我们也可以抛出自己自定义的异常类型,来精准判断错误。AuthenticationException 类型的异常会被 onLoginFailure
捕捉。
1 2 3 4 public class CaptchaEmptyException extends AuthenticationException {} public class CaptchaErrorException extends AuthenticationException {}
最终根据程序执行流程响应结果
CRMFormAuthenticationFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Component ("crmFormAuthenticationFilter" )public class CRMFormAuthenticationFilter extends FormAuthenticationFilter { @Override protected AuthenticationToken createToken (String username, String password, ServletRequest request, ServletResponse response) { String captchaCode = request.getParameter("captchaCode" ); return new CRMToken(username, password, captchaCode); } @Override protected boolean onLoginSuccess (AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { response.setContentType("application/json;charset=UTF-8" ); JsonResult result = new JsonResult(); response.getWriter().print(JSON.toJSONString(result)); return false ; } @Override protected boolean onLoginFailure (AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { response.setContentType("application/json;charset=UTF-8" ); JsonResult result = new JsonResult(); if (e instanceof CaptchaEmptyException) { result.mark("验证码为空" ); } else if (e instanceof CaptchaErrorException) { result.mark("验证码错误" ); } else { result.mark("用户名或者密码错误" ); } try { response.getWriter().print(JSON.toJSONString(result)); } catch (IOException e1) { e1.printStackTrace(); } return false ; } }
Shiro RememberMe 流程原理
Shiro 授权概述
基于 ini 的授权,详情见上面基于 ini 的 Shiro 认证代码。
规则,”资源标识符:操作:资源实例标识符”,意思是对哪个资源的哪个实例具有什么操作,:是资源/操作/实例的分割付,权限字符串也可以用 * 通配符。
exp:用户创建权限:(user:create),或者 user:create.* 用户修改实例 001 的权限:user:update001,用户实例001的所有权限:user:*:001
方法:
isPermitted(String permission) 是否拥有当前指定的一个权限
isPermitted(String permission,…) 是否拥有当前指定的一个或者多个权限,以数组形式返回每个判断结果
hasRole(String role) 是否拥有当前指定的一个角色
hasRoles(List roles) 是否拥有当前指定的多个角色,以数组形式返回每个判断的结果
hasAllRoles(Collection roles) 是否拥有指定的多个角色,只有当都有的时候才返回 true,反之返回 false
web 下 Realm 的授权
对 subject 进行授权,调用方法 isPermitted(“permission 串”)
SecurityManager 执行授权,通过 ModularRealmAuthorizer 执行授权
ModularRealmAuthorizer 执行自定义的 Realm 中的方法 doGetAuthorizationInfo
从数据库查询权限数据调用 realm 的授权方法。
realm 从数据库查询权限数据,返回 ModularRealmAuthorizer
如果对比后,isPermitted 中 “permission串”在realm 查询到权限数据中,说明用户访问 permission 传有权限,否则没有权限,抛出异常。
在权限管理模块中,采用自定义权限注解,贴在需要授权的方法上,获取对应的注解,生成权限表达式。但这个注解是我们自己定义的,Shiro 并不知道,所以需要使用 Shiro 自身提供的注解来完成。
在 Controller 的方法上贴上 Shiro 提供的权限注解(@RequiresPermissions)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RequestMapping ("/list" )@RequiresPermissions (value = {"Employee:list" , "员工列表" }, logical = Logical.OR)public String list (Model model, @ModelAttribute("qe" ) QueryEmployee qe) { if (qe == null || qe.getPageNum() == null ) { qe = QueryEmployee.EMPTYQUERY; model.addAttribute("qe" , qe); } qe.setDeptId(Optional.ofNullable(qe.getDeptId()) .filter(s -> s.intValue() > 0 ).orElse(null )); PageInfo pageInfoEmp = employeeService.gets(qe); model.addAttribute("pageInfo" , pageInfoEmp); PageInfo pageInfoDept = departmentService.gets(null ); model.addAttribute("pageInfoDept" , pageInfoDept); return "employee/list" ; }
value 属性可以传递 String[] ,同时设置权限表达式和权限名称, logical Shiro 会将 value 中的数据都作为权限表达式使用,校验的时候都要有,或者拥有其中一个,对应的值是 Logical.AND 和 Logical.OR
开启 Shiro 注解扫描器,当扫描到 Controller 中有使用 @RequiresPermissions 注解时,会使用动态代理为当前 Controller 生成代理对象,增强对应方法的权限校验功能。
1 2 3 4 <aop:config proxy-target-class ="true" > </aop:config > <bean class ="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor" > <property name ="securityManager" ref ="securityManager" /> </bean >
上面的增强类需要依赖 aop 组件,并且要注明是用 CGLib 创建动态代理
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > ${aspectj.version}</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjrt</artifactId > <version > ${aspectj.version}</version > </dependency >
PermissionService.reload()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override public void reload () { List<Permission> permissions = permissionMapper.selectAll(); Map<String, Object> beans = context.getBeansWithAnnotation(Controller.class); Collection<Object> controllers = beans.values(); for (Object controller : controllers) { Method[] methods = controller.getClass().getSuperclass().getDeclaredMethods(); for (Method method : methods) { boolean isPermissionMethod = method.isAnnotationPresent(RequiresPermissions.class); if (!isPermissionMethod) continue ; RequiresPermissions reqPerAnn = method.getDeclaredAnnotation(RequiresPermissions.class); if (permissions.contains(reqPerAnn.value()[0 ])) continue ; Permission permission = new Permission(); permission.setName(reqPerAnn.value()[1 ]); permission.setExpression(reqPerAnn.value()[0 ]); permissionMapper.insert(permission); } } }
注意:
从 Spring 容器中获取的 Controller 对象是代理对象,该对象的类型继承原 Controller,而注解是在原 Controller 类方法上的,所以需要获取到 Controller 代理对象的父类字节码,然后获取方法
权限表达式在注解中直接指定。
在 CRMRealm.java 中获取当前登录用户的授权信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Employee employee = (Employee) principals.getPrimaryPrincipal(); if (employee.isAdmin()) { info.addRole("admin" ); info.addStringPermission("*:*" ); return info; } List<String> roleSns = roleMapper.selectRoleSnsWithEmpId(employee.getId()); List<String> expressions = permissionMapper.selectExpressionsByEmpId(employee.getId()); info.addRoles(roleSns); info.addStringPermissions(expressions); return info; }
*:*
表示所有权限,如果用户是超管,可以访问所有权限,不是超管,就分配给用户响应的权限。doGetAuthorizationInfo
方法配置角色,基于角色获取对象是否有访问权限,或者基于角色判断页面是否需要展示页面。这里 subject.hasRole(String sn),sn 为上面我们在授权时,添加的角色名。
1 2 3 4 5 6 7 8 Subject subject = SecurityUtils.getSubject(); Employee employee = (Employee) subject.getPrincipal(); if (!subject.hasRole(Role.ROLE_ADMIN) && !subject.hasRole(Role.ROLE_MARKETMASTER)) qc.setSellerId(employee.getId()); else { model.addAttribute("sellers" , employeeService.getsSellers()); }
前端页面中的写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!-- 如果是管理员或者销售经理才会出现这个 --> <@shiro.hasAnyRoles name="admin,marketMaster"> <div class="form-group"> <label for="seller">市场专员:</label> <select class="form-control" id="seller" name="sellerId"> <option value="-1">全部</option> <#list sellers.list as e> <option value="${e.id}">${e.name}</option> </#list> </select> <script> $("#seller option[value='${qc.sellerId!'-1'}']").prop("selected", true); </script> </div> </@shiro.hasAnyRoles>
如果访问没有权限的资源会抛出异常:org.apache.shiro.authz.UnauthorizedException
,只要捕获到该异常,然后进行处理。使用 SpringMVC 的统一异常处理。
mvc.xml
1 2 3 4 5 6 7 8 9 10 <bean class ="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver" > <property name ="defaultErrorView" value ="common/error" /> <property name ="exceptionMappings" > <value > org.apache.shiro.authz.UnauthorizedException=common/nopermission </value > </property > </bean >
在 exceptionMappings 中键值对的格式设置,是如果异常类名是 org.apache.shiro.authz.UnauthorizedException,则跳转的错误页面资源路径为 common/nopermission
异步请求没有权限异常处理 异步请求需要拦截 org.apache.shiro.authz.UnauthorizedException 异常,并判断是否是异步请求,然后做相应处理,而拦截访问资源的异常,最好使用 aop 对 Controller 进行增强,就可以拦截器方法抛出的异常了。
Spring 已经提供了一个增强器 @ControllerAdvice,贴上改标签的类被认为是 Controller 的增强类型。通常和 @ExceptionHandler、@InitBinder、@ModelAttribute 注解配合使用,最常用的是 @ExceptionHandler。对Controller 中的异常做特定处理。
@ExceptionHandler 贴了该注解的方法,会在Controller 抛出异常时,进入方法执行逻辑,参数如下:
异常参数,包括一般的异常或者自定义异常,如果注解没有指定异常类,会默认进行映射。
请求或者响应对象,(HttpServletRequest、ServletRequest、HttpServletResponse、ServletResponse)
HttpSession
WebRequest 或者 NativeWebRequest
Locale
InputStream/Reader
OutputStream/Writer
Model
UnauthorizedExceptionUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @ControllerAdvice public class UnauthorizedExceptionUtil { @ExceptionHandler (UnauthorizedException.class) public void handler (HttpServletResponse response, HandlerMethod method, UnauthorizedException e) throws IOException { if (method.getMethod().isAnnotationPresent(ResponseBody.class)) { response.setContentType("text/json;charset=UTF-8" ); JsonResult jsonResult = new JsonResult(); jsonResult.mark("对不起,您没有权限执行该操作" ); response.getWriter().print(JSON.toJSONString(jsonResult)); } else { throw e; } } }
Shiro标签控制权限
准备工作 前端页面的权限处理,根据权限控制前端页面。比如:用户拥有删除员工的权限,则在页面上把删除按钮显示出来。 引入 jar 包,shiro-freemarker-tags
并且默认 freemarker 是不支持 shiro 标签的,需要扩展 freemarker 功能。
MyFreeMarkerConfig.java
1 2 3 4 5 6 7 8 public class MyFreeMarkerConfig extends FreeMarkerConfigurer { @Override public void afterPropertiesSet () throws IOException, TemplateException { super .afterPropertiesSet(); Configuration cfg = this .getConfiguration(); cfg.setSharedVariable("shiro" , new ShiroTags()); } }
mvc.xml
1 2 3 4 5 6 <bean class ="cn.lizhaoloveit.crm.util.MyFreeMarkerConfig" > <property name ="defaultEncoding" value ="UTF-8" /> <property name ="templateLoaderPath" value ="/WEB-INF/view" /> </bean >
覆写 FreemarkerConfigerer 类,加入 shiro 标签,然后再 mvc.xml 中用自己的 FreemarkerConfig 类来覆盖原来的 bean。
常用标签
authenticated:已经认证通过的用户 <@shiro.authenticated></@shiro.authenticated>
notAuthenticated
principal 输出当前用户信息,通常为登录账号信息 <@shiro.principal property="name"/>
,就是 Employee 对象,也就是我们在验证的时候传入的对象。相当于Employee principal = (Employee) SecurityUtils.getSubject().getPrincipal();
hasRole 验证当前用户是否属于该角色 <@shiro.hasRole name="admin">Hello admin!</@shiro.hasRole>
hasAnyRoles 验证当前用户是否属于这些角色中的任何一个,角色之间用逗号分隔 <@shiro.hasAnyRoles name="admin,user,operator">Hello admin!</@shiro.hasAnyRoles>
hasPermission 验证当前用户是否拥有该权限 <@shiro.hasPermission name="/order:*">订单</@shiro.hasPermission>
1 2 3 4 5 <@shiro.hasPermission name="employee:delete"> <a href="#" class="btn btn-danger btn-xs btn-delete" data-href="/employee/delete?id=${entity.id}"> <span class="glyphicon glyphicon-trash"></span> 删除 </a> </@shiro.hasPermission>
如果当前登录的用户没有员工删除权限,就不在页面中显示超链接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <shiro:authenticated > <shiro:principal /> 已经经过身份验证<br > <br > </shiro:authenticated > <shiro:notAuthenticated > 用户没有进行身份验证,记住我自动登录的属于没有进行身份验证<br > <br > </shiro:notAuthenticated > <shiro:guest > <a href ="login.jsp" > 登录</a > <br > <br > </shiro:guest > <shiro:hasAnyRoles name ="admin,user,manager" > <shiro:principal > </shiro:principal > 拥有admin/user/manager中的角色<br > <br > </shiro:hasAnyRoles > <shiro:hasPermission name ="customer:delete" > <shiro:principal /> 拥有customer:delete权限<br > <br > </shiro:hasPermission > <shiro:lacksPermission name ="customer:delete" > 没有权限customer:delete<br > <br > </shiro:lacksPermission > <shiro:lacksRole name ="manager" > <shiro:principal > </shiro:principal > 没有manager角色<br > <br > </shiro:lacksRole > <shiro:user > <a href ="logout" > 登出</a > <br > <br > </shiro:user > <shiro:hasRole name ="admin" > <a href ="admin.jsp" > Admin Page</a > <br > <br > </shiro:hasRole >
MD5 加密
登录的时候,虽然 POST 请求,仍然要对用户数据进行加密。而加密后的数据需要告诉 shiro 是何种加密算法,不然 shiro 不能对用户进行认证,需要在 shiro.xml 中配置加密器
1 2 3 4 5 6 7 <bean class ="org.apache.shiro.authc.credential.HashedCredentialsMatcher" > <property name ="hashAlgorithmName" value ="MD5" /> </bean >
首先,在保存用户名密码时,应该加密保存。 EmployeeServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public void save (Employee e, Long[] roleIds) throws RuntimeException { Md5Hash password = new Md5Hash(e.getPassword(), e.getName(), 1 ); e.setPassword(password.toString()); try { if (e.getId() == null ) { employeeMapper.insert(e); } else { employeeMapper.update(e); } if (roleIds == null ) return ; employeeMapper.deleteFromEmpRoleWithEmpId(e.getId()); for (Long roleId : roleIds) { employeeMapper.insertEmpRole(e.getId(), roleId); } } catch (Exception e1) { throw new RuntimeException("用户名已存在" ); } }
下面模拟整个登录认证,用户输入用户名密码登录,shiro 的 realm 拿到用户名密码后,调用 doGetAuthenticationInfo
方法。在调用 SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName)
时,如果用了 md5 加密算法,则要将 salt 告诉 shiro,在 ByteSource credentialsSalt。
1 2 3 4 5 return new SimpleAuthenticationInfo( employee, employee.getPassword(), ByteSource.Util.bytes(employee.getName()), this .getName());
但此时还是不能让 shiro 来进行认证,因为我们虽然在 spring 容器中存放了加密器 HashedCredentialsMatcher
,但是并没有将这个加密器设置给 realm,所以在 realm 中要设置 HashedCredentialsMatcher 为自定义的加密器。
1 2 3 4 @Autowired public void setCredentialsMatcher (CredentialsMatcher credentialsMatcher) { super .setCredentialsMatcher(credentialsMatcher); }
realm 接受一个 CredentialsMatcher 参数,但是 HashedCredentialsMatcher 继承了 SimpleCredentialsMatcher 实现了 CredentialsMatcher。
此时,shiro 已经可以为我们进行认证了。
集成 EhCache
shiro 中的缓存。在请求中,一旦进行权限控制,例如 @RequiresPermissions(“employee:list”) 或者 <@shiro:hasPermission name="employee:delete">
都会去 Realm 的 doGetAuthorizationInfo 方法中进行授权,而且每次都要查数据库,用到就查是非常麻烦的。而且用户登录以后,授权信息一般很少变动,所以在第一次授权后,就把授权信息缓存。
shiro 中没有实现自己的缓存机制,只提供了一个可以支持具体缓存的实现的抽象 API 接口,允许 shiro 根据自己的需求灵活的选择具体的 CacheManager,(Hazelcast, Ehcache, OSCache, Terracotta, Coherence, GigaSpaces, JBossCache等),这里选择使用 EHCache。
实现步骤:
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-ehcache</artifactId > <version > ${shiro.version}</version > </dependency > <dependency > <groupId > net.sf.ehcache</groupId > <artifactId > ehcache-core</artifactId > <version > 2.6.11</version > </dependency >
shiro.xml 中配置缓存管理器,并引用缓存
1 2 3 4 5 6 7 8 9 10 <bean id ="cacheManager" class ="org.apache.shiro.cache.ehcache.EhCacheManager" > <property name ="cacheManagerConfigFile" value ="classpath:shiro-ehcache.xml" /> </bean > <bean id ="securityManager" class ="org.apache.shiro.web.mgt.DefaultWebSecurityManager" > <property name ="realm" ref ="crmRealm" /> <property name ="cacheManager" ref ="cacheManager" /> </bean >
shiro-ehcache.xml
1 2 3 4 5 6 7 8 <ehcache > <defaultCache maxElementsInMemory ="1000" eternal ="false" timeToIdleSeconds ="120" timeToLiveSeconds ="120" memoryStoreEvictionPolicy ="LRU" > </defaultCache > </ehcache >
Ehcache 配置属性名:
name:缓存名称
maxElementsInmemory 缓存最大个数,
eternal 对象是否永久有效,一旦设置,timeout 将不起作用。
timeToLiveSeconds 设置对象在失效前允许存活时间(单位:s)最大时间介于创建时间和失效时间之间。当 eternal=false 时使用,默认是0,也就是对象存活时间无穷大。
overflowToDisk 当内存中对象数量达到 maxElementsInMemory 时,Ehcache 会将对象写到磁盘中,
diskSpoolBufferSizeMB 这个参数设置 DiskStore 磁盘缓存区域大小。默认 30 MB。每个 Cache 都有自己的一个缓冲区
maxElementsOnDisk 硬盘最大缓存个数,
diskPersistent 是否缓存虚拟机重启期数据。默认值是 false
diskExpireThreadIntervalSeconds 磁盘失效线程运行时间间隔,默认120秒
memoryStoreEvictionPolicy 当达到 maxElementsInMemory 限制时,Ehcache 会根据指定的策略去清理内存。默认策略是 LRU (最近最少使用),可以设置为 FIFO(先进先出) 或者 LFU(较少使用)。
clearOnFlush 内存数量最大时是否清楚。
配置结束,登录之后,多次访问权限资源时,不会在反复查询权限数据,而是直接使用缓存汇总的权限数据。
清空缓存
用户正常退出,缓存自动清空
如果用户非正常退出,缓存自动清空
如果修改了用户的权限,而用户不退出系统,修改的权限无法立即生效
当用户权限修改以后,用户再次登录 shiro 会自动调用 realm 从数据库获取权限,修改权限后想立即清除缓存的话,可以调用 realm 的 clearCache 方法清除缓存。
1 2 3 4 public void clearCache () { PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals(); super .clearCache(principals); }
在角色权限 Service 中,delete、update 方法区调用 realm 的清除缓存方法。 测试时,可以用命令对数据库中的密码加密:
1 2 3 4 UPDATE employee SET password = 1 ;UPDATE employee SET password =MD5 (concat (name , password ));
上述方法只能清空自己的授权缓存
如果想要清空所有用户的缓存
1 cacheManager.getCacheManager().clearAll();
总结
Shiro 做了哪些事情
实现了用户的身份认证
实现了系统级别的注销登录
实现了访问权限的控制
用户信息的加密
权限数据的缓存
Spring Security
目前最流行的一个安全权限管理框架,它与 Spring 结合紧密。如果项目使用 Spring 可以使用。