阅读 648
安全权限框架 —— Shiro(一)

安全权限框架 —— Shiro(一)

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

1.简介

  • Apache Shiro 是Java的一个安全(权限)框架;

  • Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境;

  • 主要功能:认证(Authentication)、授权(Authorization)、加密、回话管理、与Web集成、缓存等;

Shiro 架构

  • Subject应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject 的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者;
  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色
  • Realm:Shiro从Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource

2.与Spring整合开发

2.1 Shiro认证

2.1.1 配置文件

web.xml

<!-- 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>
复制代码

applicationContext.xml

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

    <!--2.配置CacheManager
        2.1 需要加入ehcache 的jar包,及配置文件
    -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <!-- 3.自定义Realm
     -->
    <bean id="myRealm" class="com.xiaojian.shiro.realms.MyRealm"/>


    <!-- 4.保证实现了Shiro内部lifecycle函数的bean执行
        可以来调用IOC容器中 shiro bean 生命周期方法
     -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 5.开启Shiro注解,必须配置了lifecycleBeanPostProcessor -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

    <!-- 5.Shiro过滤器
        id 必须和web.xml文件中配置DelegatingFilterProxy的<filter-name>一致
        因为Shiro会在 IOC容器中查询和 <filter-name> 名字对应的 filter bean
     -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全接口,这个属性是必须的 -->
        <property name="securityManager" ref="securityManager"/>
        <!-- 身份认证失败,则跳转到登录页面的配置 -->
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="success.jsp"/>
        <property name="unauthorizedUrl" value="unauth.jsp"/>

        <!-- Shiro连接约束配置,即过滤链的定义 -->
        <property name="filterChainDefinitions">
            <value>
                /login.jsp=anon
                /**=authc
            </value>
        </property>
    </bean>
复制代码

MyRealm.java

public class MyRealm extends AuthorizingRealm{
	
	@Resource
	private BloggerService bloggerService;
	
	/**
	 * AuthorizationInfo:角色的权限集合
	 * 获取授权信息
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * AuthenticationInfo:用户的角色集合
	 * 登录验证
	 * token: 令牌,基于用户名密码的名牌
	 */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行认证逻辑");

        // 假设用户名、密码
        String username = "xiaojian";

        // 1.转换类型,获取Token
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        // 2.对比用户名
        if(!token.getUsername().equals(username)){
            // 用户名不存在
            return null;    //// Shiro底层会抛出 UnknownAccountException 异常
        }
        // 3.根据用户情况,构建AuthenticationInfo对象返回。通常使用实现类:SimpleAuthenticationInfo
        // 3.1 principal:认证的实体信息,可以是username,也可以是对应的实体类对象
        Object principal = null;
        // 3.2 hashedCredentials:密码
        Object hashedCredentials = "1234"; //a20b8e682a72eeac0049847855cecb86
        // 3.3 realName:当前realm对象的name,调用父类的getName()方法
        String realmName = getName();

        SimpleAuthenticationInfo simpleAuthenticationInfo = null;
       simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,hashedCredentials,realmName);

        return simpleAuthenticationInfo;
    }
}
复制代码

2.1.2 Shiro中默认的过滤器

过滤器类过滤器名称例子
anon无参,匿名可访问/login.jsp=anon
authc无参,需认证(登录)才能访问/admin/**=authc
user无参,表示必须存在用户
perms可有多个参数,多个时必须加上引号,参数间用逗号分隔。当有多个参数时必须每个参数都通过,相当于isPermitedAll()方法/admin/*=perms[user:add] /admin/**=perms["user:add,user:update"]
roles角色过滤器,判断当前用户是否指定角色。规则同上。相当于hasAllRoles()方法/admin/**=roles["admin,guest"]
logout注销登录时,完成一定的功能:任何现有的session都将会失效,而且任何身份都将失去关联(web程序中,RememberMe cookie也将会被删除)

2.1.3 URL匹配模式

使用Ant风格模式,Ant路径通配符支持 ?、*、**,通配符匹配不包括目录分隔符 "/"

  • ? : 匹配一个字符,如:/admin?,匹配:/admin1;不匹配:/admin123, /admin/
  • *:匹配零个或多个字符串或一个路径
  • **:匹配路径中的零个或多个路径

2.1.4 URL匹配顺序

​ 第一次匹配优先的方式。所以一般 /* 的路径访问都放在后面。

2.1.5 Shiro加密

Shiro 认证中密码比对:使用AuthorizingRealm中的 credentialsMatcher 进行的密码比对。

1、md5加密(不可逆的)

(1).如何把一个字符串加密为MD5;

(2).替换当前 Realm的credentialsMatcher 属性,直接使用HashedCredentialsMather对象,并设置加密算法。

applicationContext.xml

    <!-- 3.自定义Realm
        实现了 Realm 接口的bean
     -->
    <bean id="myRealm" class="com.xiaojian.shiro.realms.MyRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <!--加密方式-->
                <property name="hashAlgorithmName" value="MD5"/>
                <!--加密的次数-->
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>
复制代码

这时候服务器会自动将浏览器传来的密码使用 MD5加密,加密1024次。

2、md5盐值加密

applicationContext.xml不变

(1). doGetAuthenticationInfo方法返回值创建SimpleAuthenticationInfo,使用构造器:

​ SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, realmName);

(2). 使用ByteSource.Util.bytes(username);加密盐值

(3). 盐值需唯一,一般使用随机字符串、user id

(4). 可以使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 计算盐值加密后的值。

   /**
     * 执行认证逻辑
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行认证逻辑");

        // 1.转换类型,获取Token
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String username = token.getUsername();

        //3.根据用户情况,构建AuthenticationInfo对象返回,通常使用实现类 SimpleAuthenticationInfo
// 3.1 principal:认证的实体信息,可以是username,也可以是对应的实体类对象
        Object principal = username;
// 3.2 hashedCredentials:密码
        Object hashedCredentials = null; //a20b8e682a72eeac0049847855cecb86
        // 2.对比用户名、密码
        if("user".equals(username)){
            hashedCredentials = "3e042e1e3801c502c05e13c3ebb495c9";
        } else if("admin".equals(username)){
            hashedCredentials = "c34af346c89b8b03438e27a32863c9b5";
        }
// 3.3 credentialsSalt:加密盐值
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
// 3.4 realName:当前realm对象的name,调用父类的getName()方法
        String realmName = getName();

        SimpleAuthenticationInfo simpleAuthenticationInfo =  new SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, realmName);

        return simpleAuthenticationInfo;
    }
复制代码

MD5Test.java

public class MD5Test {
    public static void main(String[] args) {
        String hashAlgorithmName = "MD5";
        Object credentials = "1234";
        Object salt = "admin";
        int hashIterations = 1024;

        Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }
}
复制代码

2.1.6 多Reaml

为什么使用多 Realm ?

​ 根据不同的登录需求,需要做不同的验证。如:手机号登录、邮箱登录。。。

使用 ModularRealmAuthenticator类,属性Collection realms;注入 Realm集合

SecondRealm.java 同MyRealm类,将验证密码改为 SHA1加密后的值。

applicationContext.xml

    <bean id="secondRealm" class="com.xiaojian.shiro.realms.SecondRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <!--加密方式-->
                <property name="hashAlgorithmName" value="SHA1"/>
                <!--加密的次数-->
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>
复制代码
认证策略 (AuthenticationStrategy)

AuthenticationStrategy 接口的默认实现:

  • FirstSuccessfulStrategy: 还要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
  • AtLeastOneSuccessfulStrategy: 只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有 Realm 身份验证成功的认证信息;
  • AllSuccessfulStrategy: 所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。
  • ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy 策略

配置:

<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<!--        <property name="realms">-->
<!--            <list>-->
<!--                <ref bean="myRealm"/>-->
<!--                <ref bean="secondRealm"/>-->
<!--            </list>-->
<!--        </property>-->
    <property name="authenticationStrategy">
        <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
    </property>
</bean>
复制代码

使用多 Realm 后,可以把 authenticator 配置给 SecurityManager

通常将所有的 Realm 配置给 安全管理器SecurityManager

之后
<!-- 1.安全管理器SecurityManager -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="authenticator" ref="authenticator"/>
        
        <property name="realms">
            <list>
                <ref bean="myRealm"/>
                <ref bean="secondRealm"/>
            </list>
        </property>        
    </bean>
复制代码

2.2 Shiro 授权

多 Realm 实现授权时,有一个通过,都是授权通过。

Shiro过滤器添加角色拦截    
/user.jsp = roles[user]
/admin.jsp = roles[admin]
复制代码

MyRealm.java

/**
 * 执行授权逻辑
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("MyRealm 执行授权逻辑");
    // 1. 从 principalCollection 获取用户信息 (是用户名还是用户对象,取决于你认证时 principal参数 放入的是啥)
    Object principal = principalCollection.getPrimaryPrincipal();
    // 2. 利用登录用户的信息来获取当前用户的角色或权限
    Set<String> roles = new HashSet<>();
    roles.add("user");
    if("admin".equals(principal)){
        roles.add("admin");
    }
    // 3. 创建 SimpleAuthorizationInfo,并设置其 roles 属性
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    authorizationInfo.setRoles(roles);

    // 4. 返回 SimpleAuthorizationInfo
    return authorizationInfo;
}
复制代码

2.3 Shiro 标签

Shiro 提供了 JSTL 标签用于在 JSP 页面进行权限控制,如根据登录用户显示相应的页面按钮。

标签描述示例
guest用户没有身份验证时显示相应信息,即游客信息image-20200307234952932
user用户已经经过认证/记住我登录后显示相应的信息image-20200308000528385
authenticated用户已经身份验证通过,即 Subject.login登录成功,不是记住我登录的image-20200308000656696
notAuthenticated用户未进行身份验证,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证。image-20200308000937683
pincipal显示用户身份信息,默认调用image-20200308001012588
hasRole如果当前Subject 有角色将显示body 体内容image-20200308001040891
hasAnyRoles如果当前Subject有任意一个角色(或的关系)将显示body体内容。image-20200308001147878
lacksRole如果当前Subject 没有角色将显示body 体内容image-20200308001256323
hasPermission如果当前Subject 有权限将显示body 体内容image-20200308001314089
lacksPermission如果当前Subject没有权限将显示body体内容image-20200308001326991
文章分类
后端
文章标签