Shiro使用笔记
0 本文主要涉及
shiro在基于Spring和SpringMVC的前后端分离的JavaWeb项目中认证和授权授权功能的使用
1 shiro简介
shiro是Apache提供的开源的基于Java实现的安全框架
官网:http://shiro.apache.org/index.html
优点:配套功能完善,接口易于使用
主要功能:身份验证,权限验证,会话管理、加密等等
基本架构:
Subject :实体,代表当前用户,方便交互,实际功能逻辑是由SecurityManager实现
SecurityManager : 安全管理器,负责所有与安全相关的操作,是Shiro的核心,负责与Shiro的其他组件进行交互
Realm : Shiro从Realm获取安全数据(如用户,角色,权限),数据源,需要我们自己实现并提供给框架
Authenticator : 负责身份验证,提供接口,需要我们自己实现并提供给框架
Authorizer :负责权限验证,提供接口,需要我们自己实现并提供给框架
SessionManager 会话管理器,不仅仅可以在Web环境中使用,也可以在普通javaSE中使用
SessionDAO:所有会话的CRUD功能
CacheManager:缓存控制器,来管理用户,角色,权限等的缓存
Cryptography : 密码模块,提供了一些常见的加密组件用于加密和解密
2 shiro配置集成
0 依赖配置
使用了Maven进行项目依赖管理,JavaWeb基础的依赖这里就不多说了,Shiro相关的如下
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-core</artifactId>
- <version>1.4.0</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-web</artifactId>
- <version>1.4.0</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-spring</artifactId>
- <version>1.4.0</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-ehcache</artifactId>
- <version>1.4.0</version>
- </dependency>
- <dependency>
- <groupId>de.svenkubiak</groupId>
- <artifactId>jBCrypt</artifactId>
- <version>0.4.1</version>
- </dependency>
- <dependency>
- <groupId>net.sf.ehcache</groupId>
- <artifactId>ehcache</artifactId>
- <version>2.10.2</version>
- </dependency>
1 继承实现一个Realm,实现认证和授权的接口
Shiro框架不会去维护用户、角色和权限,需要我们自己去设计/提供,然后通过相应的接口注入给Shiro使用。
具体的,首先设计用户 角色和权限的表(简单的就三个表,一个用户有一种角色,每种角色有多种权限,复杂的可以建中间表实现多对多的映射关系)
然后实现一个基本的认证和鉴权所需的DAO,大概需要根据用户名获取用户,根据用户名获取角色集合,根据用户名获取权限集合(不一定是这样的,可以根据自己Realm的实现所需提供对应的DAO方法)
继承AuthorizingRealm实现一个自己的Realm,在Realm中主要工作就是实现doGetAuthorizationInfo(认证),doGetAuthenticationInfo(鉴权)两个方法,代码如下(代码中使用了BCrypt一种更方便的加盐hash方案,还带有重复登录限制逻辑)
具体的,首先设计用户 角色和权限的表(简单的就三个表,一个用户有一种角色,每种角色有多种权限,复杂的可以建中间表实现多对多的映射关系)
然后实现一个基本的认证和鉴权所需的DAO,大概需要根据用户名获取用户,根据用户名获取角色集合,根据用户名获取权限集合(不一定是这样的,可以根据自己Realm的实现所需提供对应的DAO方法)
继承AuthorizingRealm实现一个自己的Realm,在Realm中主要工作就是实现doGetAuthorizationInfo(认证),doGetAuthenticationInfo(鉴权)两个方法,代码如下(代码中使用了BCrypt一种更方便的加盐hash方案,还带有重复登录限制逻辑)
- public class MyRealm extends AuthorizingRealm
- {
- @Autowired
- private AuthorityService authorityService;//底层调用了上面所说的DAO
- public MyRealm()
- {
- super();
- //BcryptPasswordMatcher
- setCredentialsMatcher(new CredentialsMatcher()
- {
- @Override
- public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info)
- {
- UsernamePasswordToken userToken = (UsernamePasswordToken) token;
- String name = userToken.getUsername();
- String password = new String(userToken.getPassword());
- String hashed = info.getCredentials().toString();
- //重复登陆限制
- Cache<Object, Object> passwordRetryCache = getCacheManager().getCache("passwordRetryCache");
- Integer count = (Integer) passwordRetryCache.get(name);
- if(count == null)
- {
- passwordRetryCache.put(name, 1);
- }
- else
- {
- passwordRetryCache.put(name, count++);
- if(count > 5)
- {
- throw new ExcessiveAttemptsException();
- }
- }
- boolean matches = BCrypt.checkpw(password, hashed);
- if(matches)
- {
- passwordRetryCache.remove(name);
- }
- return matches;
- }
- });
- }
- /**
- * 用于的权限的认证。
- *
- * @param principalCollection
- * @return
- */
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)
- {
- String username = principalCollection.getPrimaryPrincipal().toString();
- SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
- //用户角色(role)放入到Authorization里。
- Set<String> roleName = authorityService.getRoles(username);
- info.setRoles(roleName);
- //用户权限(permission)放入到Authorization里。
- Set<String> permissions = authorityService.getPermissions(username);
- info.setStringPermissions(permissions);
- return info;
- }
- /**
- * 首先执行这个登录验证
- *
- * @param authenticationToken
- * @return
- * @throws AuthenticationException
- */
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException
- {
- //UsernamePasswordToken对象用来存放提交的登录信息
- UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
- //获取用户账号
- String username = token.getUsername();
- //查出是否有此用户
- UserEntity user = authorityService.getUserByUsername(username);
- if(user != null)
- {
- //若存在,将用户账号和密码存放到 authenticationInfo用于后面的权限判断。第三个参数传入realName。
- return new SimpleAuthenticationInfo(user.getName(), user.getPassword(), user.getRealName());
- }
- else
- {
- // 用户名不存在抛出异常
- throw new UnknownAccountException();
- }
- }
- }
2 配置缓存
缓存通过EhCache实现
在缓存的SpringBean中写入shiroEncacheManager
1 配置自定义Realm
在缓存的SpringBean中写入shiroEncacheManager
<!--shiro缓存管理--> <bean id="shiroEncacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager" p:cache-manager-ref="ehcache"/>具体缓存申明如下
- <ehcache>
- <!-- 指定一个文件目录,当EhCache把数据写到硬盘上时,将把数据写到这个文件目录下 -->
- <diskStore path="java.io.tmpdir"/>
- <!--
- 缓存配置
- name:缓存名称。
- maxElementsInMemory:缓存最大个数。
- eternal:对象是否永久有效,一但设置了,timeout将不起作用。
- timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
- timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
- overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
- diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
- maxElementsOnDisk:硬盘最大缓存个数。
- diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
- diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
- memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
- clearOnFlush:内存数量最大时是否清除。
- maxBytesLocalHeap是用来限制缓存所能使用的堆内存的最大字节数的,其单位可以是K、M或G,不区分大小写。默认是0,表示不限制
- maxBytesLocalOffHeap是用来限制缓存所能使用的非堆内存的最大字节数,其单位也可以是K、M或G。默认是0,表示不限制。
- maxBytesLocalDisk是用来限制缓存所能使用的磁盘的最大字节数的,其单位可以是K、M或G。默认是0,表示不限制。只有在单机环境下才可以使用本地磁盘
- maxEntriesLocalHeap是用来限制当前缓存在堆内存上所能保存的最大元素数量的
- -->
- <defaultCache
- maxElementsInMemory="10000"
- eternal="false"
- timeToIdleSeconds="120"
- timeToLiveSeconds="120"
- overflowToDisk="true"/>
- <cache name="authorizationCache"
- maxElementsInMemory="2000"
- eternal="false"
- timeToIdleSeconds="3600"
- timeToLiveSeconds="0"
- overflowToDisk="false"
- statistics="true">
- </cache>
- <cache name="authenticationCache"
- maxElementsInMemory="2000"
- eternal="false"
- timeToIdleSeconds="3600"
- timeToLiveSeconds="0"
- overflowToDisk="false"
- statistics="true">
- </cache>
- <cache name="shiro-activeSessionCache"
- maxElementsInMemory="2000"
- eternal="false"
- timeToIdleSeconds="3600"
- timeToLiveSeconds="0"
- overflowToDisk="false"
- statistics="true">
- </cache>
- <!-- 登录记录缓存 锁定10分钟 -->
- <cache name="passwordRetryCache"
- maxElementsInMemory="2000"
- eternal="false"
- timeToIdleSeconds="3600"
- timeToLiveSeconds="0"
- overflowToDisk="false"
- statistics="true">
- </cache>
- </ehcache>
3 在web.xml中配置shiro的过滤器
- <!-- shiro过滤器定义 -->
- <filter>
- <filter-name>shiroFilter</filter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
- <init-param>
- <!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->
- <param-name>targetFilterLifecycle</param-name>
- <param-value>true</param-value>
- </init-param>
- </filter>
- <filter-mapping>
- <filter-name>shiroFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
4 定义Shiro的SpringBean
1 配置自定义Realm
- <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
- <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
- <!-- 配置自定义Realm -->
- <bean id="myRealm" class="com.cetiti.application.modules.authority.MyRealm">
- <property name="cachingEnabled" value="true"/>
- <property name="authenticationCachingEnabled" value="true"/><!-- 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false -->
- <property name="authenticationCacheName" value="authenticationCache"/><!-- 缓存AuthenticationInfo信息的缓存名称 -->
- <property name="authorizationCachingEnabled" value="true"/><!-- 启用授权缓存,即缓存AuthorizationInfo信息,默认false -->
- <property name="authorizationCacheName" value="authorizationCache"/><!-- 缓存AuthorizationInfo信息的缓存名称 -->
- </bean>
2 配置安全管理器
- <!-- rememberMe管理器相关 -->
- <!-- rememberMe Cookie模板-->
- <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
- <constructor-arg value="rememberMe"/><!--名称-->
- <property name="httpOnly" value="true"/>
- <property name="maxAge" value="2592000"/><!-- 30天(单位:秒) -->
- </bean>
- <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
- <!-- rememberMe cookie加密的** 建议每个项目都不一样 默认AES算法 **长度(128 256 512 位)-->
- <!-- cipherKey是加密rememberMe Cookie的**;默认AES算法 -->
- <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
- <property name="cookie" ref="rememberMeCookie"/>
- </bean>
- <!-- 会话管理器相关 -->
- <!-- 会话Cookie模板-->
- <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
- <constructor-arg value="sessionId"/>
- <property name="httpOnly" value="true"/>
- <property name="maxAge" value="-1"/><!--maxAge=-1表示浏览器关闭时失效此Cookie-->
- </bean>
- <!-- 会话ID生成器 -->
- <bean id="sessionIdGenerator"
- class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
- <!-- 会话DAO -->
- <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
- <!-- 设置Session缓存名字,默认就是shiro-activeSessionCache -->
- <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
- <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
- </bean>
- <!-- 会话验证调度器 -->
- <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
- <property name="sessionValidationInterval" value="1800000"/><!--设置调度时间间隔 半小时 默认值为1小时-->
- <property name="sessionManager" ref="sessionManager"/>
- </bean>
- <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
- <property name="globalSessionTimeout" value="1800000"/> <!-- 设置全局会话超时时间 半小时 -->
- <property name="deleteInvalidSessions" value="true"/> <!-- 删除过期的会话 -->
- <property name="sessionValidationSchedulerEnabled" value="true"/> <!-- 是否开启会话验证器 默认开启 -->
- <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <!-- 设置会话调度器 -->
- <property name="sessionDAO" ref="sessionDAO"/>
- <!-- 是否启用Session Id Cookie,默认是启动;如果是禁用则默认使用servlet容器的 JSESSIONID -->
- <property name="sessionIdCookieEnabled" value="true"/>
- <property name="sessionIdCookie" ref="sessionIdCookie"/>
- </bean>
- <!-- 配置安全管理器 -->
- <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
- <property name="realm" ref="myRealm"/>
- <property name="cacheManager" ref="shiroEncacheManager"/>
- <property name="rememberMeManager" ref="rememberMeManager"/>
- <property name="sessionManager" ref="sessionManager"/><!--会话管理-->
- </bean>
- <!-- 静态注入,相当于调用SecurityUtils.setSecurityManager(securityManager) -->
- <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
- <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
- <property name="arguments" ref="securityManager"/>
- </bean>
3 设置Shiro过滤器
注意:有顺序优先级,匿名的url配置要放在有登录或者用户限制的前面
- <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
- <!-- Shiro的核心安全接口,这个属性是必须的 -->
- <property name="securityManager" ref="securityManager"/>
- <!-- 身份认证失败,则跳转到登录页面的配置 -->
- <!--<property name="loginUrl" value="/jsp/login.jsp"/>-->
- <property name="loginUrl" value="/authority/notLogin"/>
- <!-- 权限认证失败,则跳转到指定页面 -->
- <!--<property name="unauthorizedUrl" value="/jsp/noAccess.jsp"/>-->
- <property name="unauthorizedUrl" value="/authority/noAccess"/>
- <!-- Shiro连接约束配置,即过滤链的定义 -->
- <property name="filterChainDefinitions">
- <!--过滤定义,从上而下,蒋匿名的anon放最下面,小心配置,否则容易出现页面重定向死循环-->
- <value>
- <!--anon 表示匿名访问,不需要认证以及授权-->
- <!--静态资源-->
- /js/**=anon
- /css/**=anon
- /img/**=anon
- /lib/**=anon
- <!--登录注册入口-->
- /authority/signup*=anon
- /jsp/login*=anon
- /authority/login*=anon
- /authority/notLogin*=anon
- /authority/noAccess*=anon
- <!--authc表示需要认证 没有进行身份认证是不能进行访问的-->
- <!--/** = authc-->
- <!--用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc-->
- /jsp/dict*=roles[admin]
- <!--/user/add=roles[manager]-->
- <!--/user/del/**=roles[admin]-->
- <!--/user/edit/**=roles[manager]-->
- <!--/user=perms[user:del]-->
- <!--/student=roles[teacher]-->
- <!--/teacher=perms["user:create"]-->
- /** = user
- </value>
- </property>
- </bean>
4 在springmvcbean中配置拦截器和开启注解功能
- <!-- 开启Shiro URL权限控制 注解 -->
- <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
- depends-on="lifecycleBeanPostProcessor">
- <property name="proxyTargetClass" value="true"/>
- </bean>
- <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
- <property name="securityManager" ref="securityManager"/>
- </bean>
3 shiro常用示例
1 登录
- Subject subject = SecurityUtils.getSubject();
- //err.println(LogUtils.format(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt())));
- UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword(), rememberMe);//设置是否保持登录
- try
- {
- subject.login(token);
- AuthorityHelper.setCurrentUserEntity(authorityService.getUserByUsername(SecurityUtils.getSubject().getPrincipal().toString()));//自定义工具方法保存当前用户DO
- //if(WebUtils.getSavedRequest(request) != null)
- //{
- // //配合前端实现登陆前页面跳转
- // return WebUtils.getAndClearSavedRequest(request).getRequestUrl();
- //}
- //else
- //{
- return "登录成功!";
- //}
- }catch(UnknownAccountException e)
- {
- e.printStackTrace();
- throw new RuntimeException("用户名不存在!");
- }catch(ExcessiveAttemptsException e)
- {
- e.printStackTrace();
- throw new RuntimeException("登录失败次数过多!10分钟后重试");
- }catch(IncorrectCredentialsException e)
- {
- e.printStackTrace();
- throw new RuntimeException("密码错误!");
- }catch(AuthenticationException e)
- {
- e.printStackTrace();
- throw new RuntimeException("登录出错!");
- }
2 Shiro注解使用
常用注解
一般Authentication或者User(这两个区别是是否通过通过“记住我”登录)会在过滤器的url中统一限制
- @RequiresAuthentication : 表示当前Subject已经通过Login进行身份验证
- @RequiresUser : 表示当前Subject已经身份验证或者通过记住我登录
- @RequiresGuest : 表示当前Subject已经没有身份验证或通过记住我登录,即是游客身份
- @RequiresRoles(value={"admin", "user"}, Logical.AND) : 表示当前Subject需要admin和user角色
- @RequiresPermissions(value={"user:a", "user:b"}) : 表示当前Subject需要权限user:a或user:b
一般Authentication或者User(这两个区别是是否通过通过“记住我”登录)会在过滤器的url中统一限制
关于如何限制权限,简单的直接通过RequiresRoles批量限制,如果是具体某个权限则通过RequiresPermissions来限制
理论上最好的方式是面向资源进行权限限制(即使用RequiresPermissions给不同资源设置权限)然后根据角色分配资源(设置权限),这样便于动态控制(用户表,角色表,资源权限表,角色资源权限表),参考http://globeeip.iteye.com/blog/1236167
3会话管理
- 当前会话
- Subject.getSession();
- //获取会话,默认为Subject.getSession(true)即当前没有创建Session则会创建,
- Subject.getSession(false)即当前没有Session返回NULL
- session.getId(); //获取当前会话的唯一标示
- session.getHost(); //获取当前Subject的主机地址
- session.getTimeout() & session.setTimeout(毫秒);
- //获取/设置当前Session的过期时间
- session.getStartTimestamp() & session.getListAccessTime();
- //获取会话的启动时间以及最后访问时间,如果是JavaSE应用需要手动定期调用session.touch
- 去更新最后访问时间, 如果是Web应用每次进入ShiroFIlter会自动调用session.touch()进行更新最后访问时间
- 所有会话DAO
- 在配置中配置过org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO即可在代码中获取使用
- enterpriseCacheSessionDAO.delete(enterpriseCacheSessionDAO.readSession(sessionId))
4 区分url访问和ajax请求
返回不同的登录失败或无授权结果(页面或者json数据)
需要真shiro 的springBean中注册过滤器
//身份验证过滤器,区分ajax请求(返回json数据)
<bean id="authenticationFilter" class="com.cetiti.common.web.MyShiroAuthenticationFilter"/>
并在shiro过滤器中配入
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="filters"> <util:map> <entry key="user" value-ref="authenticationFilter"/> </util:map> </property> <!--其他配置这里省略--> </bean>MyShiroAuthenticationFilter具体实现如下
主要重写了
onAccessDenied方法,增加了是否是ajax请求的判断逻辑并返回不同结果
- public class MyShiroAuthenticationFilter extends UserFilter
- {
- @Data
- @AllArgsConstructor
- class UnauthenticatedExceptionInfoType
- {
- //返回json数据的格式
- int statusCode;
- Date dateTime;
- String message;
- }
- private ObjectMapper objectMapper = new MyJacksonObjectMapper();
- @Override
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception
- {
- if(isAjax(request))
- {
- ((HttpServletResponse) response).setHeader("Content-Type", "application/json;charset=utf-8");
- response.getWriter().print(objectMapper.writeValueAsString(new UnauthenticatedExceptionInfoType(HttpStatus.UNAUTHORIZED.value(), new Date(), "未登录!")));
- }
- else
- {
- saveRequestAndRedirectToLogin(request, response);
- }
- return false;
- }
- public static boolean isAjax(ServletRequest request)
- {
- String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
- if("XMLHttpRequest".equalsIgnoreCase(header))
- {
- return Boolean.TRUE;
- }
- return Boolean.FALSE;
- }
- }