安全框架——Shiro使用教程

1. 下载

  • 官网地址
  • 选择Source Code Distribution,下载,解压

2. 测试

实际开发使用注解的方式,搭建web开发环境,不会在ini配置文件进行硬编码的操作

  • 测试样例参考shiro解压目录\samples\quickstart
  • 需要的jar包
shiro-all.jar
log4j.jar
slf4j-api.jar
slf4j-log4j12.jar
  • 官方shiro.ini文件(截取部分文件)
[users]
# 用户 'root' 密码 'secret' 角色 'admin' 
root = secret, admin
# 用户 'guest' 密码 'guest' 角色 'guest' 
guest = guest, guest
# 用户 'darkhelmet' 密码 'ludicrousspeed' 角色 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz

[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# 用户名 = 类型-用户:操作-删除:实例-张三--->goodgay 可以删除张三的用户信息
# 主:谓:宾
goodguy = user:delete:zhangsan
  • 官方Quickstart.java:\samples\quickstart\src\main\java\Quickstart.java
public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

    public static void main(String[] args) {
    	// 使用ini配置文件配置realms, users, roles and permissions,创建Shiro SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
                
        /**
         * 如何操作shiro 模拟用户、密码、角色、权限都在ini配置文件里
         */
        // 1. 获取当前的 Subject. 调用 SecurityUtils.getSubject();
        Subject currentUser = SecurityUtils.getSubject();
 
        // 获取 Session: Subject#getSession()
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("---> Retrieved the correct value! [" + value + "]");
        }

        // 2. 测试当前的用户是否已经被认证. 即是否已经登录. 
        // 调动 Subject 的 isAuthenticated() 
        if (!currentUser.isAuthenticated()) {
        	// 把用户名和密码封装为 UsernamePasswordToken 对象
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            // rememberme
            token.setRememberMe(true);
            try {
            	// 执行登录. 
                currentUser.login(token);
            } 
            // 若没有指定的账户, 则 shiro 将会抛出 UnknownAccountException 异常. 
            catch (UnknownAccountException uae) {
                log.info("----> There is no user with username of " + token.getPrincipal());
                return; 
            } 
            // 若账户存在, 但密码不匹配, 则 shiro 会抛出 IncorrectCredentialsException 异常。 
            catch (IncorrectCredentialsException ice) {
                log.info("----> Password for account " + token.getPrincipal() + " was incorrect!");
                return; 
            } 
            // 用户被锁定的异常 LockedAccountException
            catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // 所有认证时异常的父类. 
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("----> User [" + currentUser.getPrincipal() + "] logged in successfully.");

        // 测试是否有某一个角色. 调用 Subject 的 hasRole 方法. 
        if (currentUser.hasRole("schwartz")) {
            log.info("----> May the Schwartz be with you!");
        } else {
            log.info("----> Hello, mere mortal.");
            return; 
        }

        // 测试用户是否具备某一个行为. 调用 Subject 的 isPermitted() 方法。 
        if (currentUser.isPermitted("lightsaber:weild")) {
            log.info("----> You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        // 测试用户是否具备某一个行为. 
        if (currentUser.isPermitted("user:delete:zhangsan")) {
            log.info("----> You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        // 执行登出. 调用 Subject 的 Logout() 方法. 通过测试用户是否被认证来查看用户是否登陆退出
        System.out.println("---->" + currentUser.isAuthenticated());
        
        currentUser.logout();
        
        System.out.println("---->" + currentUser.isAuthenticated());

        System.exit(0);
    }
}

3. SSM整合教程

3.1 基本认证流程

  1. 获取当前的 Subject,调用 SecurityUtils.getSubject()
  2. 测试当前的用户是否已经被认证. 即是否已经登录,调用 Subject 的 isAuthenticated() 方法
  3. 如果没有被认证,则把用户名和密码(通过将表单信息提交到SpringMVC中)封装为 UsernamePasswordToken 对象
  4. 执行登录,调用 Subject 的 login(token) 方法
  5. 自定义Realm,继承 AuthenticatingRealm,实现 doGetAuthenticationInfo(),通过数据库获取对应记录,返回给shiro
  6. 通过shiro完成密码比对:前台输入的 UsernamePasswordToken 与 数据库查询的 SimpleAuthenticationInfo 信息进行比对

3.2 基本配置

  • 依赖管理:修改pom.xml
<!-- 添加shiro支持 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<version>1.2.4</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-web</artifactId>
	<version>1.2.4</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.2.4</version>
</dependency>
  • 修改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>
  • 新建spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
	http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- 1.配置 SecurityManager -->     
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager" />        
        <property name="realm" ref bean="jdbcRealm" />        
        <property name="rememberMeManager.cookie.maxAge" value="10"></property>
    </bean>

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

    <!-- 1.2配置 Realm 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean -->     
    <bean id="jdbcRealm" class="com.cc.shiro.realms.ShiroRealm"></bean>
	
	
	<!-- 2.启用shiro注解 -->
    <!-- 2.1配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法 -->       
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    <!-- 2.2启用 IOC 容器中使用 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>


	<!-- 3.配置 ShiroFilter -->
    <!-- 
    	id 必须和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
    	若不一致, 则会抛出: NoSuchBeanDefinitionException. 因为 Shiro 会来 IOC 容器中查找和 <filter-name> 名字对应的 filter bean.
    -->     
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="/success.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        
<!--         <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> -->
        
        <!--  
        	配置哪些页面需要受保护. /** 使用通配符表示其他所有页面
        	以及访问这些页面需要的权限. 
        	1). anon 可以被匿名访问
        	2). authc 必须认证(即登录)后才可能访问的页面. 
        	3). logout 登出.
        	4). roles 角色过滤器
        	
        	格式:url=拦截器
        	
        	通配符:?表示一个字符,*表示0个或多个字符,**表示0个或多个路径
        -->
          
        <property name="filterChainDefinitions">
            <value>
            	<!-- 允许匿名访问 -->
                /login.jsp = anon
                /shiro/login = anon
                /shiro/logout = logout
                
                # everything else requires authentication:
                /** = authc
            </value>
        </property>
        
    </bean>


</beans>
  • 在spring.xml文件中导入spring-shiro.xml
<import resource="spring-shiro.xml"/>
  • 自定义Realm
public class ShiroRealm extends AuthenticatingRealm{
	
	/**
	 * token:是SpringMVC从请求中获取的
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("token.hashCode() = " + token.hashCode());
		//1. 把 AuthenticationToken 转换为 UsernamePasswordToken 
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		
		//2. 从 UsernamePasswordToken 中来获取 username
		String username = upToken.getUsername();
		
		//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
		System.out.println("用户名:"+username);
		
		//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
		if("unknown".equals(username)){
			throw new UnknownAccountException("用户不存在!");
		}
		
		//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常 
		if("locked".equals(username)){
			throw new LockedAccountException("用户被锁定!");
		}
		
		//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
		//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
		Object principal = username;
		//2). credentials: 从数据库中获取密码,以下是在本地模拟
		Object credentials = "123456"; 
		//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
		String realmName = getName();
		
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName);
		return info;
	}
}
  • login.jsp
<form action="shiro/login" method="POST">

	username: <input type="text" name="username"/>
	<br><br>
	
	password: <input type="password" name="password"/>
	<br><br>
	
	<input type="submit" value="Submit"/>
	
</form>
  • success.jsp
<a href="shiro/logout">Logout</a>
  • ShiroController
@Controller
@RequestMapping("/shiro")
public class ShiroController {

	@RequestMapping("/login")
	public String login(@RequestParam("username") String username, 
			@RequestParam("password") String password){
		Subject currentUser = SecurityUtils.getSubject();
		
		if (!currentUser.isAuthenticated()) {
			// 把用户名和密码封装为 UsernamePasswordToken 对象
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // rememberme
            token.setRememberMe(true);
            try {
            	System.out.println("token.hashCode() = " + token.hashCode());
            	// 执行登录
                currentUser.login(token);
            } 
            // ... catch more exceptions here (maybe custom ones specific to your application?
            // 所有认证时异常的父类
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            	System.out.println("登录失败: " + ae.getMessage());
            }
        }
		
		return "redirect:/success.jsp";
	}
	
}
  • 测试效果
    • 测试访问其他页面:无法访问,返回login.jsp
    • 测试用户名输入:unknown,打印:登录失败: 用户不存在!
    • 测试输入用户名:aa、密码:123456(即定义的Realm中的密码),登陆成功

3.3 密码加密配置

  • 将前台获取的密码进行加密:修改 spring-shiro.xml 配置的 Realm
<bean id="jdbcRealm" class="com.cc.shiro.realms.ShiroRealm">
	<property name="credentialsMatcher">
  		<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
  			<!-- 指定加密算法 -->
  			<property name="hashAlgorithmName" value="MD5"></property>
  			<!-- 指定加密次数 -->
  			<property name="hashIterations" value="1024"></property>
  		</bean>
  	</property>
</bean>
  • 修改自定义Realm
public class ShiroRealm extends AuthenticatingRealm{
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		//前面不变..
		
		//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
		//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
		Object principal = username;
		//2). credentials: 密码
		Object credentials = "038bdaf98f2037b31f1e75b5b4c9b26e"; //123456采用MD5加密后的结构 
		//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
		String realmName = getName();
		//4). 盐值
		ByteSource credentialsSalt = ByteSource.Util.bytes(username);
		
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
		return info;
	}
}
  • 原理
//shiro-core.jar包下
package org.apache.shiro.authc;
public class UsernamePasswordToken {
	public char[] getPassword() {
        return password; // 打断点
    }
}

//不断下一步,会来到equals方法,进行密码比对
package org.apache.shiro.authc.credential;
public class HashedCredentialsMatcher extends SimpleCredentialsMatcher {
	@Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = hashProvidedCredentials(token, info);
        Object accountCredentials = getCredentials(info);
        return equals(tokenHashedCredentials, accountCredentials);
    }
}

3.4 配置多个Realm

  • 增加新的自定义的Realm,使用SHA1加密,其他不变
public class ShiroRealm2 extends AuthenticatingRealm{
	
	/**
	 * token:是SpringMVC从请求中获取的
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("token.hashCode() = " + token.hashCode());
		System.out.println("ShiroRealm2");
		//1. 把 AuthenticationToken 转换为 UsernamePasswordToken 
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		
		//2. 从 UsernamePasswordToken 中来获取 username
		String username = upToken.getUsername();
		
		//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
		System.out.println("用户名:"+username);
		
		//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
		if("unknown".equals(username)){
			throw new UnknownAccountException("用户不存在!");
		}
		
		//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常 
		if("locked".equals(username)){
			throw new LockedAccountException("用户被锁定!");
		}
		
		//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
		//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
		Object principal = username;
		//2). credentials: 密码
		Object credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06"; //123456
		//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
		String realmName = getName();
		//4). 盐值
		ByteSource credentialsSalt = ByteSource.Util.bytes(username);
		
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
		return info;
	}	
}
  • 修改spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
	http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- 1.配置 SecurityManager -->     
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"></property>
        <property name="rememberMeManager.cookie.maxAge" value="10"></property>
		<!-- 尽管可以放在配置认证策略中,但很多时候需要直接从securityManager获取realms -->
        <property name="realms">
    		<list>
    			<ref bean="firstRealm"/>
    			<ref bean="secondRealm"/>
    		</list>
    	</property>
    </bean>

    <!-- 1.1配置缓存管理器,需要加入 ehcache 的 jar 包及配置文件  -->     
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> 
    </bean>
    
    <!-- 1.2配置认证策略(针对多个Realm) -->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    	<property name="authenticationStrategy">
    		<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
    	</property>
    </bean>  
    
    <!-- 1.3配置 Realm 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean -->   
    <bean id="firstRealm" class="com.cc.shiro.realms.ShiroRealm">
    	<property name="credentialsMatcher">
    		<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
    			<!-- 指定加密算法 -->
    			<property name="hashAlgorithmName" value="MD5"></property>
    			<!-- 指定加密次数 -->
    			<property name="hashIterations" value="1024"></property>
    		</bean>
    	</property>
    </bean>
    <bean id="secondRealm" class="com.cc.shiro.realms.ShiroRealm2">
    	<property name="credentialsMatcher">
    		<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
    			<!-- 指定加密算法 -->
    			<property name="hashAlgorithmName" value="SHA1"></property>
    			<!-- 指定加密次数 -->
    			<property name="hashIterations" value="1024"></property>
    		</bean>
    	</property>
    </bean>
    

	<!--其余部分未修改 -->    

</beans>
  • 原理
//controller
currentUser.login(token);

//调用Subject接口的login方法
public interface Subject {
	void login(AuthenticationToken token) throws AuthenticationException;
}

//去找login的实现方法,这里调用securityManager.login()方法
public class DelegatingSubject implements Subject {
	public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        Subject subject = securityManager.login(this, token);
        //...
}

//来到SecurityManager接口
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
	Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
}

//找对应实现类,调用authenticate()方法
public class DefaultSecurityManager extends SessionsSecurityManager {
	public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } 
        //...
    }
}

//继续进入
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
	public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }
}

//找authenticate()方法实现类,这里执行doAuthenticate()方法
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
	public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
		//...
        AuthenticationInfo info;
        try {
            info = doAuthenticate(token);
            //...
    }
}

//找doAuthenticate()实现方法
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
	protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token)
            throws AuthenticationException;
}

//ModularRealmAuthenticator.class中getRealms()获取到的是Realm的集合
public class ModularRealmAuthenticator extends AbstractAuthenticator {
	//263行
	protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
        	//单个Realms认证
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
        	//多个Realms认证
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }
	//198行
	protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
		//选择认证策略
        AuthenticationStrategy strategy = getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        //...
}

3.5 授权

  • 增加user.jsp
  • 增加admin.jsp
  • 修改success.jsp
<body>
	<h1>成功!</h1>
	
	<a href="shiro/logout">Logout</a>
	
	<a href="user.jsp">User Page</a>
	
	<a href="admin.jsp">Admin page</a>
</body>
  • 修改自定义Realm:模拟分别让user、admin登陆系统,密码都是123456
public class ShiroRealm extends AuthenticatingRealm{
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		//...
		Object credentials = null;
		if("admin".equals(username)){
			credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";//123456加盐:admin、加密:MD5
		}else if("user".equals(username)){
			credentials = "098d2c478e9c11555ce2823231e02ec1";//123456加盐:user、加密:MD5
		}
		//...
	}
}
  • 测试:登陆(用户名:user、密码:123456),登陆成功,可以访问user.jsp、admin.jsp
  • 修改spring-shiro.xml,设置user.jsp、admin.jsp的访问权限
		<property name="filterChainDefinitions">
            <value>
            	<!-- 允许匿名访问 -->
                /login.jsp = anon
                /shiro/login = anon
                /shiro/logout = logout
                
                /user.jsp = roles[user]
                /admin.jsp = roles[admin]
                
                # everything else requires authentication:
                /** = authc
            </value>
        </property>
	</bean>
</beans>
  • 测试:登陆(用户名:user、密码:123456),登陆成功,不可以访问user.jsp、admin.jsp,会跳转到unauthorized.jsp
  • 通过以下原理分析,得出自定义Realm需要实现AuthorizingRealm,之前实现的是AuthenticatingRealm,所以修改ShiroRealm.java
public class ShiroRealm extends AuthorizingRealm{
	
	/**
	 * 1.认证方法
	 * token:是SpringMVC从请求中获取的
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("token.hashCode() = " + token.hashCode());
		//1. 把 AuthenticationToken 转换为 UsernamePasswordToken 
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		
		//2. 从 UsernamePasswordToken 中来获取 username
		String username = upToken.getUsername();
		
		//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
		System.out.println("用户名:"+username);
		
		//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
		if("unknown".equals(username)){
			throw new UnknownAccountException("用户不存在!");
		}
		
		//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常 
		if("locked".equals(username)){
			throw new LockedAccountException("用户被锁定!");
		}
		
		//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
		//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
		Object principal = username;
		//2). credentials: 从数据库中获取密码,以下是在本地模拟
		Object credentials = null;
		if("admin".equals(username)){
			credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";//123456加盐:admin、加密:MD5
		}else if("user".equals(username)){
			credentials = "098d2c478e9c11555ce2823231e02ec1";//123456加盐:user、加密:MD5
		}
		//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
		String realmName = getName();
		//4). 盐值
		ByteSource credentialsSalt = ByteSource.Util.bytes(username);
		
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
		return info;
	}
	
	
	/**
	 * 2.授权会被shiro回调的方法
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		//1. 从 PrincipalCollection 中来获取登录用户的信息
		Object principal = principals.getPrimaryPrincipal();
		
		//2. 利用登录用户的信息获取当前用户的角色或权限(可能需要查询数据库)
		Set<String> roles = new HashSet<>();
		roles.add("user"); // 模拟:所有用户都被赋予user角色
		if("admin".equals(principal)){
			roles.add("admin"); // 模拟:只有admin登陆后才有admin角色
		}
		
		//3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性.
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
		
		//4. 返回 SimpleAuthorizationInfo 对象. 
		return info;
	}

}
  • 测试
    • 登陆【user、123456】,只能访问user.jsp
    • 登陆【admin、123456】,能访问user.jsp、admin.jsp
  • 原理(分析得出,自定义Realm需要实现AuthorizingRealm的doGetAuthorizationInfo方法)
//判断是否有权限:调用Subject.class下的hasRole()方法
public interface Subject {
	boolean hasRole(String roleIdentifier);
}

//hasRole()的实现方法:调用securityManager的hasRole()方法
public class DelegatingSubject implements Subject {
	public boolean hasRole(String roleIdentifier) {
        return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
    }
}

//执行AuthorizingSecurityManager.class下的hasRole()方法
public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager {
	public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        return this.authorizer.hasRole(principals, roleIdentifier);
    }
}

//来到ModularRealmAuthorizer.class下的hasRole()方法
public class ModularRealmAuthorizer implements Authorizer, PermissionResolverAware, RolePermissionResolverAware {
	public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        assertRealmsConfigured();//验证realms不为空
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)) continue;
            if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
                return true;
            }
        }
        return false;
    }
}

//这里将realm强转为Authorizer,去调用Authorizer接口下的hasRole()方法
public interface Authorizer {    
	boolean hasRole(PrincipalCollection subjectPrincipal, String roleIdentifier);
}

//hasRole()方法的具体实现方法为AuthorizingRealm.class下的hasRole()方法
public abstract class AuthorizingRealm extends AuthenticatingRealm
        implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
    //572行,hasRole()方法执行getAuthorizationInfo
	public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
        AuthorizationInfo info = getAuthorizationInfo(principal);
        return hasRole(roleIdentifier, info);
    }
    //310行,执行doGetAuthorizationInfo
	protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
		//...
		if (info == null) {
            info = doGetAuthorizationInfo(principals);            
            //...
        }
	}
	//399行,该方法是一个抽象方法,需要被实现【重要!】
	protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

	//577行,执行hasRole
	protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
        return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
    }
}

3.6 权限注解

@RequiresAuthentication

表示当前Subject已经通过login 进行了身份验证;即Subject. isAuthenticated()返回true。

@RequiresUser

表示当前Subject已经身份验证或者通过记住我登录的。

@RequiresGuest

表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。

@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
@RequiresRoles(value={“admin”})   
@RequiresRoles({“admin“})

表示当前Subject需要角色admin 和user。

@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)

表示当前Subject需要权限user:a或user:b。

3.6.1 不使用@Service注解

public class ShiroService {	
	@RequiresRoles({"admin"})
	public void testMethod(){
		System.out.println("testMethod, time: " + new Date());
	}	
}

spring.xml中将ShiroService添加到IOC容器

<bean id="shiroService" class="com.cc.shiro.service.ShiroService"></bean>

测试注解使用:必须角色admin才能执行testMethod();如果以【user、123456】登陆,无法执行testMethod(),会报错Subject does not have role [admin];以【admin、123456】登陆,可以执行testMethod()

3.6.2 使用@Service注解

坑:shiro注解不生效原因:官方说明shiro和spring集成时,spring-shiro.xml需要被spring.xml引用,但是集成springmvc时,spring-shiro.xml中关于开启注解的部分需要声明在spring-mvc.xml中

<!-- 2.启用shiro注解:将这部分放在spring-mvc.xml下 -->
<!-- 2.1配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法 -->       
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 2.2启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用 -->     
<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>
@Service
public class ShiroService {	
	@RequiresRoles({"admin"})
	public void testMethod(){
		System.out.println("testMethod, time: " + new Date());
	}	
}

3.6.3 使用Spring声明式异常处理没有权限的异常

ExceptionHandler
ControllerAdvice

3.7 从数据库中获取资源和权限

之前采用的方式:在spring-shiro.xml中配置

	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		//...
		<property name="filterChainDefinitions">
            <value>
            	<!-- 允许匿名访问 -->
                /login.jsp = anon
                /shiro/login = anon
                /shiro/logout = logout
                
                /user.jsp = roles[user]
                /admin.jsp = roles[admin]
                
                # everything else requires authentication:
                /** = authc
            </value>
        </property>
	</bean>
</beans>

实际上是调用了ShiroFilterFactoryBean.class的setFilterChainDefinitions(String definitions)方法,而经过debug,发现setFilterChainDefinitionMap()方法传入的是LinkedHashMap,即将上述filterChainDefinitions中的values封装到Map中。

public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
	public void setFilterChainDefinitions(String definitions) {
        Ini ini = new Ini();
        ini.load(definitions);
        //did they explicitly state a 'urls' section?  Not necessary, but just in case:
        Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
        if (CollectionUtils.isEmpty(section)) {
            //no urls section.  Since this _is_ a urls chain definition property, just assume the
            //default section contains only the definitions:
            section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        }
        setFilterChainDefinitionMap(section);
    }
    public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
        this.filterChainDefinitionMap = filterChainDefinitionMap;
    }
}

所以优化spring-shiro.xml:

<beana>
	<!-- 3.配置 ShiroFilter -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		//...        
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
	</bean>
	<!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
    <bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
    <bean id="filterChainDefinitionMapBuilder"
    	class="com.cc.shiro.factory.FilterChainDefinitionMapBuilder"></bean>
</beans>        

新建FilterChainDefinitionMapBuilder.java

public class FilterChainDefinitionMapBuilder {
	public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){
		LinkedHashMap<String, String> map = new LinkedHashMap<>();
		
		map.put("/login.jsp", "anon");
		map.put("/shiro/login", "anon");
		map.put("/shiro/logout", "logout");
		//访问user.jsp,需要经过认证authc,并且还具有user权限
		map.put("/user.jsp", "authc,roles[user]");
		map.put("/admin.jsp", "authc,roles[admin]");
		//如果开启rememberme,只需要有user角色就可以访问,无需认证
		map.put("/success.jsp", "user");
		
		map.put("/**", "authc");
		
		return map;
	}	
}

3.8 会话管理

3.9 缓存管理

3.10 Remember Me

controller:设置token.setRememberMe(true);

@Controller
@RequestMapping("/shiro")
public class ShiroController {

	@RequestMapping("/login")
	public String login(@RequestParam("username") String username, 
			@RequestParam("password") String password){
		Subject currentUser = SecurityUtils.getSubject();		
		if (!currentUser.isAuthenticated()) {
			// 把用户名和密码封装为 UsernamePasswordToken 对象
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // rememberme
            token.setRememberMe(true);
            //...
	}	
}

spring-shiro.xml:增加设置rememberme失效时间

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">

    <!-- 设置rememberme失效时间,单位s -->
    <property name="rememberMeManager.cookie.maxAge" value="10"></property>  
</bean>

4. SpringBoot整合教程

附录1:新建Spring工程

附录2:新建SpringBoot工程

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值