目录
传统Spring中使用Shiro(XML)
传统Spring中使用Shiro(XML)
1.导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<!-- 引入ehcache的依赖,给shiro做缓存权限用的 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version>
</dependency>
2.在web.xml配置filter
filter要放在DispchterServlet过滤器(filter-mapping)之前
通常希望在任何其他“filter-mapping”声明之前定义“ShiroFilter filter-mapping”,以确保Shiro也可以在这些过滤器中起作用
Shiro过滤器是标准的Servlet过滤器,根据Servlet规范,其默认编码为ISO-8859-1 。但是,客户端可以选择使用标头的charset属性以不同的编码发送身份验证数据Content-Type
EnvironmentLoaderListener监听器
在EnvironmentLoaderListener监听器初始化Shiro WebEnvironment实例(包含一切Shiro需要的操作,其中包括SecurityManager),并使其在可接入ServletContext。如果需要随时获取可此WebEnvironment实例,可以使用WebUtils.getRequiredWebEnvironment(servletContext)
在ShiroFilter将使用这个WebEnvironment来执行所有必要的安全操作的任何过滤的要求。最后,该filter-mapping定义确保ShiroFilter为大多数Web应用程序推荐使用过滤所有请求,以确保可以保护任何请求
DelegatingFilterProxy过滤器
拦截所有的请求。根据filter-name也就是shiroFilter转发到applicationContext-shiro.xml中的一个名字也为shiroFilter的ShiroFilterFactoryBean,并且将所有Filter的操作都委托给他管理。这就要求在Spring配置中必须注入一个这样的Bean
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<!-- ...省略其他配置 -->
<!-- 配置shiro权限管理filter -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 将生命周期交给Spring代理 -->
<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>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
3.配置applicationContext-shiro.xml
此处bean的id和web.xml中Shiro过滤器的名称<filter-name>必须是相同的,否则Shiro会找不到这个Bean
XML核心处就是Shiro的Web过滤器的配置,当然因为Shiro的所有涉及安全的操作都要经过DefaultWebSecurityManager安全管理器,所以shiroFilter首先就要将其交给SecurityManager管理
<!-- filter-name这个名字的值来自于web.xml中filter的名字 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!--登录页面 如果没有登录 访问项目的方法或页面 直接跳转到这个页面 -->
<property name="loginUrl" value="/login.jsp" />
<!--登录后 在访问没有经过授权的方法或页面时 直接跳转到这个页面 -->
<property name="unauthorizedUrl" value="/unauthorized.jsp" />
<property name="filterChainDefinitions">
<!-- /**代表下面的多级目录也过滤 -->
<value>
/login.jsp = anon
/css/** = anon
/img/** = anon
/plugins/** = anon
/make/** = anon
/login.do = anon
/** = authc
</value>
</property>
</bean>
<!-- 引用自定义的realm -->
<bean id="saasRealm" class="com.my.realm.SaasRealm" />
<!-- 配置Shiro的核心组件:securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 配置域realm:用户名、密码、角色都保存在域里 -->
<property name="realm" ref="saasRealm" />
<!-- 配置缓存 -->
<property name="cacheManager" ref="cacheManager" />
</bean>
<!-- 缓存管理器:配置ehcache缓存bean,导入ehcache并新建配置文件 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml" />
<property name="shared" value="true"></property>
</bean>
<!-- 激活spring 缓存注解 -->
<cache:annotation-driven cache-manager="springCacheManager" />
<!-- 安全管理器:注入securityManager -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!-- 开启shiro框架注解支持 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<!-- 生成代理,通过代理进行控制 -->
<!-- 必须使用cglib方式为Action对象创建代理对象 -->
<property name="proxyTargetClass" value="true" />
</bean>
<aop:aspectj-autoproxy proxy-target-class="true" />
内置Realm(领域)
<!-- 使用Shiro自带的JdbcRealm类 指定密码匹配所需要用到的加密对象 指定存储用户、角色、权限许可的数据源及相关查询语句 -->
<bean id="jdbcRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"></property>
<property name="permissionsLookupEnabled" value="true"></property>
<property name="dataSource" ref="dataSource"></property>
<property name="authenticationQuery"
value="SELECT password FROM sec_user WHERE user_name = ?"></property>
<property name="userRolesQuery"
value="SELECT role_name from sec_user_role left join sec_role using(role_id) left join sec_user using(user_id) WHERE user_name = ?"></property>
<property name="permissionsQuery"
value="SELECT permission_name FROM sec_role_permission left join sec_role using(role_id) left join sec_permission using(permission_id) WHERE role_name = ?"></property>
</bean>
4.配置缓存(ehcache-shiro.xml)
Shiro的缓存是被Shiro的缓存管理器所管理(CacheManage)
Shiro的用户认证是没有提供缓冲机制的,因为每次登陆一次查询一次数据库比对一下用户名密码,做缓存的必要几乎是没有的
Shiro授权将会是大量的数据,Shiro授权缓存是默认开启的
Shiro缓冲使用EhCache来管理,之后授权时只有用户第一次访问系统的时候会走realm查数据库,之后就会走缓存
注意:用户正常退出或者非正常退出时都会清空缓冲
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd">
<!--diskStore:缓存数据持久化的目录地址 -->
<diskStore path="E:\shiroCache" />
<defaultCache maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
defaultCache 必须属性:
属性 | 描述 |
---|---|
name | 设置缓存的名称,用于标志缓存,唯一 |
maxElementsInMemory | 在内存中最大的对象数量 |
maxElementsOnDisk | 在DiskStore中的最大对象数量,如为0,则没有限制 |
eternal | 设置元素是否永久的,如果为永久,则timeout忽略 |
overflowToDisk | 是否当memory中的数量达到限制后,保存到Disk |
defaultCache 可选的属性:
属性 | 描述 |
---|---|
timeToIdleSeconds | 设置元素过期前的空闲时间 |
timeToLiveSeconds | 设置元素过期前的活动时间 |
diskPersistent | 是否disk store在虚拟机启动时持久化。默认为false |
diskExpiryThreadIntervalSeconds | 运行disk终结线程的时间,默认为120秒 |
memoryStoreEvictionPolicy | 策略关于Eviction |
defaultCache 缓存子元素:
属性 | 描述 |
---|---|
cacheEventListenerFactory | 注册相应的的缓存监听类,用于处理缓存事件。如put、remove、update和expire |
bootstrapCacheLoaderFactory | 指定相应的BootstrapCacheLoader,用于在初始化缓存,以及自动设置 |
5.Mapper
<mapper namespace="com.crossoverJie.dao.T_userDao">
<resultMap id="baseResultMap"
type="com.my.User">
<result property="id" column="id" />
<result property="userName" column="userName" />
<result property="password" column="password" />
<result property="roleId" column="roleId" />
</resultMap>
<sql id="baseColumn">
id, username, password, roleId
</sql>
<select id="findUserByUsername" parameterType="String" resultMap="baseResultMap">
select
<include refid="baseColumn" />
from user
where user_name = #{userName}
</select>
<select id="listRoles" parameterType="String" resultType="String">
select
r.role_name
from user u,role r
where u.rolei_d = r.id and u.user_name = #{userName}
</select>
<select id="listMenus" parameterType="String" resultType="String">
select
p.menu_name
from user u,role r,menu m
where u.role_id = r.id and m.role_id = r.id and u.user_name = #{userName}
</select>
</mapper>
6.自定义Realm(继承AuthorizingRealm)
创建一个类继承一个父类AuthorizingRealm,实现父类的两个方法:一个关于认证,一个关于授权的,这个类就是自定义的Realm
Shiro从Realm中获取安全数据,可以自定义多个Realm实现,但都要在SecurityManager中定义。一般自定义实现的Realm继承AuthorizingRealm(授权)即可,它继承了AuthenticatingRealm(身份验证);所以自定义Realm一般存在两个最主要的功能:
1.身份验证
2.权限校验
在用户登录后,Controller会接收到用户输入的用户名和密码,并调用subject.login(token)进行登录,实际上SecurityManager会委托Authenticator调用自定义的Realm进行身份验证。要知道,调用Realm传入的并不直接是用户名和密码,而是在Controller中绑定了用户名和密码的Token对象
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用,负责在应用程序中决定用户的访问控制的方法
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = principals.getPrimaryPrincipal().toString();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> roleName = userService.listRoles(username);
Set<String> permissions = userService.listMenus(username);
info.setRoles(roleName);
info.setStringPermissions(permissions);
return info;
}
/**
* 认证回调函数,登录信息和用户验证信息验证
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
// UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
// String username = usernamePasswordToken.getUsername();
String username = (String) token.getPrincipal();
// 根据用户名查询密码,由安全管理器负责对比查询出的数据库中的密码和页面输入的密码是否一致
User user = userService.findByName(username);
if (user == null) {
throw new UnknownAccountException(); // 没有找到账号
}
if (Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); // 账号锁定
}
// 这里盐值可以自定义:username + salt(加密)
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername + user.getSalt());
// 交给AuthenticationRealm使用CredentialsMatcher进行密码匹配
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), // 用户名
user.getPassword(), // 密码
credentialsSalt, // salt=username+salt
getName() // realm name
);
return authenticationInfo;
}
}
从token对象中调用token.getPrincipal()获取用户名,然后调用Service层方法根据这个用户名查询数据库中是否存在一个密码与其对应,根据返回的User对象,最后通过Shiro提供的SimpleAuthenticationInfo进行密码匹配
盐值
(1)在doGetAuthenticationInfo()返回值创建SimpleAuthenticationInfo对象的时候,需要使SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName)构造器
(2)使用ByteSource.Util.bytes()来计算盐值
(3)盐值需要唯一:一般使用随机字符串、userId、username+salt
(4)使用new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);来计算盐值加密后的密码的值
7.Controller控制器
登录验证的时候调用Subject的login(),调用login()时Shiro会自动调用自定义的MyRealm类中的doGetAuthenticationInfo()进行验证
验证逻辑:先根据用户名查询用户,如果查询到的话再将查询到的用户名和密码放到SimpleAuthenticationInfo对象中,Shiro会自动根据用户输入的密码和查询到的密码进行匹配。如果匹配不上就可能抛出一系列异常(登录失败),匹配就会执行doGetAuthorizationInfo()进行相应的权限验证
doGetAuthorizationInfo()处理逻辑:根据用户名获取到他所拥有的角色以及权限,然后赋值到SimpleAuthorizationInfo对象中即可,Shiro就会按照配置的角色对应权限来进行判断
在ShiroFilterFactoryBean中配置successUrl:如果登录成功Shiro会自动跳转到登录前访问的地址,如果找不到登录前访问的地址,就会跳转到successUrl中配置的地址
@Controller
public class LoginController {
@RequestMapping("/login")
public String login(@RequestParam(value = "username", required = false) String username,
@RequestParam(value = "password", required = false) String password, Model model) {
String error = null;
if (username != null && password != null) {
// 初始化:获取主题
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
// 登录,即身份校验
//由通过Spring注入的MyRealm会自动校验输入的用户名和密码在数据库中是否有对应的值
subject.login(token);
return "redirect:index.do";
} catch (Exception e) {
e.printStackTrace();
error = "未知错误,错误信息:" + e.getMessage();
}
} else {
error = "请输入用户名和密码";
}
// 登录失败,跳转到login页面,这里不做登录成功的处理,由
model.addAttribute("error", error);
return "login";
}
}
8.注销(退出)
注销登录就简单很多了,在以前都是手动写一个请求映射方法,当用户调用这个请求的时候,手动清空Session;但是在Shiro中,这些步骤都省略了,只需要在配置文件Shiro的过滤器shiroFilter中过滤器链filterChainDefinitions中的<value>标签中配置这一行即可:
/logout = logout
Shiro会根据这个配置生成一个虚拟的请求映射路径,当用户请求localhost:8080/logout这个接口的时候,Shiro会自动清空Session,并跳转到loginUrl指定的地址