最近换的这个新公司比较闲,所以回家可以学点自己的东西(我是根据尚硅谷的视频学的,如果有侵权的行为,请联系我删除),本身一直是对Spring自己的安全框架感兴趣,但是实在是有点难度,所以退而求其次,先学学Shiro吧,shiro感觉入门比较简单,这里我们就不上概念了,直接上和Spring的整合,谈谈自己的理解,以后也好有个笔记。
截几张图大概说下吧:
Spring和Shiro的整合:
这里我们暂时使用最落后的整合spring的配置文件+shiro的整合,springboot的整合后续会更新。
- 首先我们来看下web.xml
<?xml version="1.0" encoding="UTF-8"?>
<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_2_5.xsd"
id="WebApp_ID" version="2.5">
<!-- needed for ContextLoaderListener -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- Bootstraps the root web application context before servlet initialization -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- The front controller of this Spring Web application, responsible for handling all application requests -->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Map all requests to the DispatcherServlet for handling -->
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- Shiro Filter is defined in the spring application context: -->
<!--
1. 配置 Shiro 的 shiroFilter.
2. DelegatingFilterProxy 实际上是 Filter 的一个代理对象. 默认情况下, Spring 会到 IOC 容器中查找和
<filter-name> 对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id.
-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<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>
</web-app>
看到上面的配置大家不难理解,这就是最基本的spring监听器和springMVC的核心控制器。
注意一点的是:
如果springMVC的配置没有指明配置文件路径,那么他的默认位置就应该在WEB-INF包下面,命名规则就是
< servlet-name>-servlet.xml
像上面的配置,我们的springMVC的配置文件路径就应该是:
对应的是web.xml中的 < servlet-name>spring< /servlet-name>
这里我们可能陌生的是shiro的过滤器ShiroFilter。
注意一点:
这里的shiroFilter的名字,即< filter-name>的值并不是随便写的,他对应着配置的applicationContext.xml中的配置的bean的id
applicationContext.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="sessionMode" value="native"/>
<property name="realms" ref="jdbcRealm"/>
<!-- <property name="rememberMeManager.cookie.maxAge" value="10"></property> -->
</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
3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
-->
<bean id="jdbcRealm" class="com.atguigu.shiro.realms.ShiroRealm">
</bean>
<!--
4. 配置 LifecycleBeanPostProcessor. 可以自动的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- Enable Shiro Annotations for Spring-configured beans. Only run after
the lifecycleBeanProcessor has run: -->
<!--
5. 启用 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>
<!--
6. 配置 ShiroFilter.
6.1 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="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!-- <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> -->
<!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/user.jsp = authc
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
</beans>
这里我们讲解下
< bean id=“shiroFilter” class=“org.apache.shiro.spring.web.ShiroFilterFactoryBean”>这个bean的定义,这里我们看到该bean的id对应着web.xml中定义的shiro过滤器的名字,我们在这里可以定义路由的过滤。
- [urls]部分的配置,其格式是:url=拦截器[参数] 拦截器[参数]
如:
/login.jsp = anon
/user.jsp = authc - 如果当前请求的url匹配[urls]部分的某个url模式,将会执行其配置的拦截器
- anno表示拦截器接受匿名访问,表示不需要登录就可以访问
- authc表示拦截器需要身份认证通过后才能访问
- logout表示登出的过滤器(访问指定的url,就登出)
- user:表示认证或者是记住我登陆的都可以访问
- 我们的优先级是上面的优先级高,接受url然后从上到下依次匹配,如果匹配成功则不会向下继续匹配
- 如果配置的是authc,但是我们未登录,则验证不通过,默认会跳转到 < property name=“loginUrl” value="/login.jsp"/>代表的页面中
shiro的认证过程(简单理解就是登陆验证用户名密码是否正确)
- 获取当前的Subject,调用SecurityUtils.getSubject();
- 测试当前的用户是否已经被认证,即是否已经登录,调用Subject的isAuthenticated()方法
- 若没有被验证,则把用户名和密码封装成UsernamePasswordToken对象
(1)创建一个表单页面
(2)把请求提交到SpringMVC的Handler
(3)获取用户名和密码 - 执行登录,调用Subject的login()方法,其实是调用AuthenticationToken的方法
- 自定义Realm的方法,从数据库中获取对应的记录,返回给Shiro
(1)实际上需要继承AuthenticatingRelam类
(2)实现doGetAuthenicationInfo() 方法 - 由shiro完成密码的对比
代码说明:
login.jsp
<body>
<h4>Login Page</h4>
<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>
</body>
shiroHandler
@RequestMapping("/shiro")
public class ShiroHandler {
@RequestMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
Subject currentUser = SecurityUtils.getSubject();
if(!currentUser.isAuthenticated()) {
// 把用户名和密码封装成UsernamePasswordToken对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 设置Remember me
token.setRememberMe(true);
// 执行登录
try {
// 登录,判断传入的用户名密码是否正确,依赖于shiro.ini文件的配置
currentUser.login(token);
}
// 未知的账户
catch (UnknownAccountException uae) {
System.out.println("----> There is no user with username of " + token.getPrincipal());
}
// 密码错误
catch (IncorrectCredentialsException ice) {
System.out.println("----> Password for account " + token.getPrincipal() + " was incorrect!");
}
// 用户被锁定
catch (LockedAccountException lae) {
System.out.println("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
// 其他异常(认证时候异常的父类)
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
return "redirect:/list.jsp";
}
}
其中currentUser.login(token)其中的token值传给了Realm中,与其继承AuthenticatingRealm的实现的方法中的参数是一个值
ShiroRealm
public class ShiroRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1. 把AuthenticationToken的token转换成UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 2. 从UsernamePasswordToken中来获取username
String username = upToken.getUsername();
// 3. 调用数据库的方法,从数据库中查询username对应的用户记录(这里省略不做)
System.out.println("从数据库中获取username:" + username + " 所对应的用户信息");
// 4. 若用户不存在,则可以抛出UnknowAccountException异常
if("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
// 5. 根据用户的信息情况,可以决定是否抛出其他的异常
if("monster".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;
}
}
具体的realm的代码的意思可以参考上面的注释,已经详细给出每个代码的意思。当我们的用户名是unknown和monster的时候,会抛出各自的异常,当是其他用户名的时候,且密码是123456的时候,会允许登录,登录成功,这里本身是要与数据库做交互的,这里我们偷懒暂时不做交互。
注意。通常我们前台传入的密码应该是加密的,不应该直接是明文,且应该是不可逆的。那么如何加密呢?
我们的加密用的是AuthenticatingRealm的credentialsMatcher属性来进行的密码匹对,因此我们只需要替换credentialsMatcher属性就行,这里shiro提供了很多的子类可以完成对密码的加密
(1)使用HashCredentialsMatcher对象,并且设置加密算法既可完成加密
在其配置文件中作出以下改变
(1) MD5加密
<bean id="jdbcRealm" class="com.atguigu.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 采用MD5加密 -->
<property name="hashAlgorithmName" value="MD5"></property>
<!-- 指定加密的次数 -->
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
- 通过上面的配置,可以自动将前台输入的密码转换成md5加密后的密码,从而进行匹对。
- 我们上面的配置不仅指定了我们要加密的方式,还指明了我们要加密的次数。
- 在比较的时候,我们也应该将数据库中的密码进行加密,不能只用传来的前端密码进行加密,而不对数据库中取到的正确密码进行加密,两者都加密之后,再用加密的进行匹对
// 指明加密的方式是MD5
String hashAlgorithmName = "MD5";
// 密码的明文
Object credentials = "123456";
// 加盐(一般就是一个随机字符串)
Object salt = null;
// 加密的次数
int hashIterations = 1024;
// 加密操作(result就是加密之后的密码)
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
(2) 加盐的加密
我们通常会对其加密再加盐,所以这里我们自己实现的Realm就应该改一下
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, sault, realmName);
上面的代码就是返回的值,其中不同的是多加了一个参数,盐(sault)
一般使用ByteSource.Util.bytes()来计算盐值,且盐值应该是唯一的,一般使用随机字符串或者userId
多Realm验证
- 在xml中配置多个Realm对象(配置多个bean)
- 将配置的多个bean依赖到ModularRealmAuthenticator类中
- 将上面的类的id依赖到securitymanager的类中
<?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 name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
<!-- <property name="sessionMode" value="native"/>
<property name="realms" ref="jdbcRealm"/> -->
<!-- <property name="rememberMeManager.cookie.maxAge" value="10"></property> -->
</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>
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<!-- 修改认证策略 -->
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>
<!--
3. 配置 Realm
3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
-->
<bean id="jdbcRealm" class="com.atguigu.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 采用MD5加密 -->
<property name="hashAlgorithmName" value="MD5"></property>
<!-- 指定加密的次数 -->
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
<bean id="secondRealm" class="com.atguigu.shiro.realms.SecondRealm">
<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>
<!--
4. 配置 LifecycleBeanPostProcessor. 可以自动的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!--
5. 启用 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>
<!--
6. 配置 ShiroFilter.
6.1 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="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!-- <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> -->
<!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/user.jsp = authc
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
<!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<!-- <bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder"></bean>
<bean id="shiroService"
class="com.atguigu.shiro.services.ShiroService"></bean> -->
</beans>
除了上述配置外,还需要自己实现jdbcRealm(MD5加密)和secondRealm(SHA1加密)的类.
- 认证策略,上面我们动态的指出了认证策略
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<!-- 修改认证策略 -->
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>
我们来简单的介绍下shiro的认证策略
AuthenticationStrategy:
AuthenticationStrategy接口的默认实现:
- FirstSuccessfulStrategy
只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略。 - AtLeastOneSuccessfulStrategy:
只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份验证成功的认证信息。 - AllSuccessfulStrategy:
所有的Realm验证成功才算失败,且返回所有的Realm沈岑验证成功的认证信息,如果一个失败就失败了。
ModularRealmAuthenticator默认是AtLeastOneSuccessfulStrategy策略
授权方式:
shiro支持三种方式的授权:
- 编程式:通过写if/else授权代码块完成
- 注解式:通过在执行的java方法上放置相应的注解完成,没有权限将抛出相应的异常
- JSP/GSP标签:在JSP/GSP页面通过相应的标签完成
<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="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!-- <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> -->
<!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/user.jsp = authc
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
如上xml中的配置可以看到。roles是一个角色过滤器,当角色是user的时候可以访问user.jsp,角色是admin的时候可以访问admin.jsp。
那么我们如何实现授权的操作呢?
- 授权需要继承 AuthorizingRealm类,并实现其doGetAuthorizationInfo方法
- AuthorizingRealm类继承自AuthenticatingRealm,但没有实现AuthenticatingRealm中的dogetAuthenticationInfo方法
- 认证和授权只需要继承AuthorizingRealm就可以,同时实现他的两个抽象方法
如果是多realm授权用的是ModularRealmAuthorizer类的方法(不需要继承他,还是继承AuthorizingRealm类即可),只要有一个授权通过就会return true;
并且注意一点,多Realm的获取用户信息的顺序是与xml中配置的顺序一致的,因为我们在java中存储的时候用的是LinkedHashMap来存储的。
授权代码:
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[FirstRealm] doGetAuthenticationInfo");
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
if("monster".equals(username)){
throw new LockedAccountException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//以下信息是从数据库中获取的.
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
Object principal = username;
//2). credentials: 密码.
Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
if("admin".equals(username)){
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
}else if("user".equals(username)){
credentials = "098d2c478e9c11555ce2823231e02ec1";
}
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值.
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
public static void main(String[] args) {
String hashAlgorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("user");;
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
/**
* 授权的方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
// 1. 从PrincipalCollection中获取登录用户的信息
// 注意:这里获取的用户信息顺序与配置文件中配置的多Realm的顺序是一致的,用的是LinkedHashMap来存储的
Object principal = principals.getPrimaryPrincipal();
// 2. 利用登录的用户的信息来为当前用户的角色赋值权限(可能需要查询数据库,这里我们就不必要查询,测试直接赋值权限)
// 如果是admin用户赋值admin和user权限,如果是user用户赋值user权限
Set<String> roles = new HashSet<>();
roles.add("user");
if("admin".equals(principal)) {
roles.add("admin");
}
// 3. 创建SimpleAuthorizationInfo,并设置其roles属性
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
return info;
}
}
shiro标签
- Shiro提供了JSTL标签用于在JSP页面上进行权限控制,如根据登录用户显示相应的页面按钮。
- guest标签:用户没有身份验证时显示相应信息,即游客访问信息:
<shiro:guest>
欢迎游客访问,< a href="login.jsp">登录< /a>
</shiro:guest>
- user标签:用户已经经过认证/记住我登录后显示相应的信息。
<shiro:user>
欢迎【<shiro:principal>】登录,<a href="logout">退出</a>
</shiro:user>
还有很多的标签这里不做详细的介绍,因为现在这种方式已经不常用了,基本都是前后端分离,没有用jsp的了
权限注解
- @RequiresAuthentication
当前Subject已经通过login进行身份验证;即Subject.isAuthenticated()返回true - @RequiresUser
表示当前的Subject已经身份验证或者通过记住我登录的,和上面的注解的区别就是多了通过记住我登录的。 - @RequiresGuest
表示当前的Subject没有身份验证或者通过记住我登陆过的,即是游客身份。 - @RequiresRoles(value={“admin”, “user”}),logical=Logical.AND)
表示当前Subject需要角色admin和user - @RequuresPermissions(value={“user:a”, “user.b”}, logical=Logical.OR)
表示当前Subject需要权限user:a或user:b,user用户的a权限或者b权限
注意:
我们通常会在Service层上加上@Transactional注解,开启事务,事务的原理是AOP生成代理对象进行控制,因此我们不能讲Shiro的注解加到service层上,否则就是代理对象的代理对象 ,这明显是不合理的,因此通常我们会将Shiro的注解加到Controller层上去
我们上面讲的页面的权限配置都是将其直接在xml中配置,哪一个需要配置什么权限,哪一个需要登录,如:
<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>
这种方式通常是不合理的,我们应该将其权限配置在数据库中,然后通过数据库的访问,将其配置出来。
那么如何做呢?
我们只需要构建一个Map并且将其配置成FilterChainDefinitionMap的属性就可以了
eg. xml:
<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="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/>
</bean>
<!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder"></bean>
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");
map.put("/**", "authc");
return map;
}
}
Shiro的会话管理
- Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器的Tomcat),不管JavaSE还是JavaEE环境都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关集群,失效/过期支持,对web的透明支持,SSO单点登录的支持等特性
因为我们在Service层中无法使用session,所以有了Shiro的session,这里的session和HttpSession是一个,当我们在HttpSession中赋值的时候,我们可以从shiro的session中取值。这样我们就可以从service层中获得session
认证和记住我
- Shiro提供了记住我的功能,比如访问一些网站的时候,关闭了浏览器,下次打开的时候还是能记住你是谁,下次访问时,无需再次登录即可访问,基本流程如下:
(1)首先在登录页面中选中RememberMe然后登录成功,如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来;
(2)关闭浏览器再重新打开,发现浏览器还是记住我的
(3)访问一般的网页服务器端还是知道你是谁,而且能正常访问
(4)但是一般安全性的页面还是需要进行身份验证的。
注意:认证和记住我是互斥的,二者只能选其一,在登录的时候,你要么是认证过的,要么是记住我通过的,所以要么subject.isAuthenticated()==true,要么subject.isRememberer()==true
如何实现呢?
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表示账户通过记住我或者认证通过的都可以访问该页面
map.put("/list.jsp", "user");
map.put("/**", "authc");
return map;
}
}
上面是配置了那个页面可以通过记住我登陆
设置记住我的方法就是调用
- setRememberMe(true)
表示的是记住我
我们也可以设置其配置文件设置最大记住我的时间
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
</bean>
上面的设置表示10s后过期