轻量级权限管理框架之Shiro

作者 Gubaidan 日期 2018-03-23
web
轻量级权限管理框架之Shiro

关于Shiro

Shiro是Apache公司旗下产品,与Apache项目结合起来稳定可靠,使用灵活,可以独立于任何项目独立存在,粒度比较大,对于普通的web项目足够满足需求。与Shiro相对应的是Spring-security,Spring-security对Spring结合较好,如果项目用的springmvc,使用起来很方便。但是 如果项目中没有用到spring,那就不要考虑它了,而且粒度很细不适用于小型项目。

Shiro的总体架构:

shiro-frame.png

Shiro主要组件包括:Subject,SecurityManager,Authenticator,Authorizer,SessionManager,CacheManager,Cryptography,Realms

  • Subject表示与系统交互的对象,一般是登录系统的操作用户,也可能是另外一个软件系统
  • SecurityManager是Shiro架构最核心的组件。实际上,SecurityManager就是Shiro框架的控制器,协调其他组件一起完成认证和授权
  • Authenticator用于认证,协调一个或者多个Realm,从Realm指定的数据源取得数据之后进行执行具体的认证# Maven项目中Shiro的使用
  • Authorizer用户访问控制授权,决定用户是否拥有执行指定操作的权限
  • Shiro与生俱来就支持会话管理,这在安全类框架中都是独一无二的功能。即便不存在web容器环境,shiro都可以使用自己的会话管理机制,提供相同的会话API
  • Shiro提供了一个加解密的命令行工具jar包,需要单独下载使用。
    详见 https://shiro.apache.org/download.html

一个常见的登陆过程:

login

1)使用用户的登录信息创建令牌

Subject subject = SecurityUtils.getSubject(); //获得操作主题
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassward());

token可以理解为用户令牌,登录的过程被抽象为Shiro验证令牌是否具有合法身份以及相关权限。

2)执行登陆操作

subject.login(token);

Shiro的核心部分是SecurityManager,它负责安全认证与授权。Shiro本身已经实现了所有的细节,用户可以完全把它当做一个黑盒来使用。SecurityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。Subject是初学者比较难于理解的对象,很多人以为它可以等同于User,其实不然。Subject中文翻译:项目,而正确的理解也恰恰如此。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(token)与项目(subject)的登陆(login)关系,Shiro保证了项目整体的安全。

3)检查角色

subject.checkRole("admin"); //此处admin是通过数据库得来

4)检查权限

subject.checkPermission("user:delete"); //此处的权限信息也来自数据库

Maven项目中使用Shiro

在pom.xml中配置依赖包

<!--Shiro核心依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>

<!--Shiro与spring整合包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

<!--Shiro web依赖包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>

在Spring.xml中配置相关bean

    <!--shiro自带过滤器-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="login.html"/>
<property name="unauthorizedUrl" value="403.html"/>
<property name="filterChainDefinitions">
<value>
/login.html = anon
/subLogin = anon
/* = authc
</value>
</property>
</bean>

<!--自定义过滤器-->
<bean class="filter.RolesOrFilter" id="rolesOrFilter"/>
<!--创建SecurityMananger对象-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--设置自定义Realm-->
<property name="realm" ref="realm"/>
</bean>

<!--定义自定义的Realm-->
<bean id="realm" class="realm.CustomRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>

<!--设置加密的算法-->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"
id="credentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="1"/>
</bean>

需要注意filterChainDefinitions过滤器中对于路径的配置是有顺序的,当找到匹配的条目之后容器不会再继续寻找。因此带有通配符的路径要放在后面。三条配置的含义是: /authc/admin需要用户有用admin权限、/authc/用户必须登录才能访问、/其他所有路径任何人都可以访问

spring-mvc.xml配置

 <!--配置基于注解的shiro-->
<aop:config proxy-target-class="true"/>
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor" >
<property name="securityManager" ref="securityManager"/>
</bean>

缓存机制ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache">
<diskStore path="java.io.tmpdir" />
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>

timeToLiveSeconds为缓存的最大生存时间,timeToIdleSeconds为缓存的最大空闲时间,当eternal为false时ttl和tti才可以生效

散列算法与加密算法

    <!--设置加密的算法-->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"
id="credentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="1"/>
</bean>

web.xml

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<!-将Shiro的配置文件交给Spring监听器初始化->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

<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>

<!-- The front controller of this Spring Web application, responsible for handling all application requests -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<!-- Map all requests to the DispatcherServlet for handling -->
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

基于JDBC的自定义reaml

public class CustomRealm extends AuthorizingRealm {

@Resource
private UserDao userDao;

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获得认证数据中的用户名
String username = (String) principals.getPrimaryPrincipal();
Set<String> permissions = new HashSet<>();
Set<String> roles = getUserRolesByName(username);
for(String s:roles){
Set<String> tem = getUserPermissionByName(s);
permissions.addAll(tem);
}
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setStringPermissions(permissions);
authorizationInfo.setRoles(roles);
return authorizationInfo;
}

private Set<String> getUserPermissionByName(String username) {
List<String> list = userDao.getPermissionByName(username);
Set<String> permission = new HashSet<String>(list);
return permission;
}

private Set<String> getUserRolesByName(String username) {
List<String> list = userDao.getRolesByName(username);
Set<String> roles = new HashSet<String>(list);
return roles;
}

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获得认证数据中的用户名
String username = (String) token.getPrincipal();

// 通过用户名获取数据库密码
String passwd = getUserPasswdByName(username);
if(passwd == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,passwd,"custom");
//设置盐为用户名 authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return authenticationInfo;
}

private String getUserPasswdByName(String name) {

User user = userDao.getUserByName(name);
if(user != null){
return user.getPassward();
}
return null;
}

//生成MD5 并加盐
public static void main(String[] args) {
Md5Hash md5Hash = new Md5Hash("what","what");
System.out.println(md5Hash.toString());
}
}

根据Shiro的设计思路,用户与角色之前的关系为多对多,角色与权限之间的关系也是多对多。在数据库中需要因此建立5张表,分别是用户表(存储用户名,密码,盐等)、角色表(角色名称,相关描述等)、权限表(权限名称,相关描述等)、用户-角色对应中间表(以用户ID和角色ID作为联合主键)、角色-权限对应中间表(以角色ID和权限ID作为联合主键)。具体dao与service的实现本文不提供。总之结论就是,Shiro需要根据用户名和密码首先判断登录的用户是否合法,然后再对合法用户授权。而这个过程就是Realm的实现过程。

Shiro会话管理

shiro实现了简单的会话管理,由于自带的会话管理需要多次查询数据库,在实际项目中对性能影响过大,所以结合redis 实现会话管理时比较好的选择,当然也可以实现一个二级缓存进一步提升性能。在shiro中配置和实现如下:

  • spring配置文件中配置自定义sessionManagement(可以用注解方式实现)
   <!-自定义SessionDAO->
<bean class="session.RedisSessionDao" id="redisSessionDao"/>

<!-自定义SessionManager->
<bean class="session.CustomSessionManager" id="sessionManager">
<property name="sessionDAO" ref="redisSessionDao"/>
</bean>
  • 在securityManager中添加如下属性

securitityManagement

自定义Management

/**
* 如果用默认的DefaultWebSessionManager ,会多次从Redis中读取数据 造成资源浪费 ,所有重写retrieveSession()方法
* retrieveSession该方法为读取Session方法
* 参数SessionKey 中包含request对象,所有在第一次请求时从Redis中读取,之后都从request读取
*/
public class CustomSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey){
request = ((WebSessionKey)sessionKey).getServletRequest();
}
if (request != null && sessionId != null){
Session session = (Session) request.getAttribute(sessionId.toString());
if (session != null){
return session;
}
}
Session session = super.retrieveSession(sessionKey);
if(request != null && sessionId != null){
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}

自定义Cache实现权限和角色数据缓存

虽然角色缓存和权限下缓存包含的数据可能并不多,但是有效的减少了对数据库的访问次数,而且速度更快。

实现Shiro 自带CacheManager 接口

/**
* 缓存角色和权限数据
*/
@Component
public class RedisCacheManager<K, V> implements CacheManager {
@Resource
private RedisCache redisCache;


@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return redisCache;
}
}

spring 中配置如下:

<!--自定义cacheManager-->
<bean class="cache.RedisCacheManager" id="cacheManager"/>

一个简单的DEMO

基于Spring的实现DemoDemo