一 Shrio与Spring集成
- Shiro的理论
- shiro是一个权限框架(身份认证,授权,会话,密码学)
- shiro(轻量级,粗粒度,其它涉及到细粒度自己代码完成)与Spring security(重量级,细粒度,有点麻烦)
- 身份认证(登录)authentication,授权authorization,密码学,会话管理
Subject(当前用户)—》SecurityMananger(所有功能都经过它)–》Realm(获取真实数据的一个组件)
二 2.2.shiro能干什么?
Shiro 开发团队称为“应用程序的四大基石” ——身份验证,授权,会话管理和加密作为其目标。
Authentication(身份认证):
有时也简称为“登录”,这是一个证明用户是他们所说的他们是谁的行为。
Authorization(授权):
访问控制的过程,也就是绝对“谁”去访问“什么”权限。
Session Management:管理用户特定的会话,即使在非 Web 或 EJB 应用程序。
Cryptography:通过使用加密算法保持数据安全同时易于使用。
也提供了额外的功能来支持和加强在不同环境下所关注的方面,尤其是以下这些:
Web Support: Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
Caching:缓存是 Apache Shiro 中的第一层公民,来确保安全操作快速而又高效。
Concurrency: Apache Shiro 利用它的并发特性来支持多线程应用程序。
Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
“Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
“Remember Me”:在会话中记住用户的身份,所以他们只需要在强制时候登录。
三 身份验证
Authentication(身份认证)
-
引入
在集成Spring的例子中,只有登录页面是不需要登录可以直接访问。其他的页面需要认证(即登录)后才能访问,那怎么来实现登录认证,又叫做身份认证呢? -
概念
做认证需要用户名和密码
认证流程:
详细步骤分析
要想实现登录,分前台和后台两部分:
前台:
实现一个login.jsp的页面,用来搜集登录信息(用户名和密码)!当点击登录时把登录信息提交到后台完成认证。
后台:
写一个Controller接收前台传入的登录信息,完成登录认证。
具体步骤如下:
1)创建LoginConroller,写一个方法接收前台登录请求并接受登录信息(用户名和密码)
2)获取当前的 Subject. 调用 SecurityUtils.getSubject();
3)测试当前的用户是否已经被认证. 即是否已经登录. 调用 Subject 的 isAuthenticated()
4)若没有被认证, 则把用户名和密码封装为 UsernamePasswordToken 对象
5)执行登录: 调用 Subject 的 login(AuthenticationToken) 方法.
6)自定义 Realm 的方法, 从数据库中获取对应的记录, 返回给 Shiro.
入门中使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。
实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类
实现 doGetAuthenticationInfo(AuthenticationToken) 方法.
7)由 shiro 完成对密码的比对.
四 Shiro入门
前提:创建一个普通Maven项目
- 导包
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
五 集成spring
- 为什么要集成Spring
我们的项目基本都是通过Spring来管理bean的,如果要想使用Shiro,那就要把shiro集成到Spring。集成Spring的核心就是把框架的核心类(SecurityManager,Subject,Realm)交给Spring管理! - 集成的准备
准备Spring-web项目
3.web.xml配置
<!-- Spring与shiro集成:需要定义一个shiro过滤器(这是一个代理过滤器,它会到spring的配置中找一个名称相同的真实过滤器) -->
<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>
- 在applicationContext.xml中引入applicationContext-shiro.xml让其交给spring管理
- applicationContext-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-3.0.xsd">
<!-- 1.配置apache的管理器 引用我们自己的realm 创建时自动注入 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 配置一个realm,到数据库中获取权限数据 -->
<property name="realm" ref="myRealm"/>
</bean>
<!--相当于自动创建核心管理器的时候注入我们自己的realm对象-->
<!-- 2.我们可以自定义一个realm【暂时未实现功能】这个必需实现org.apache.shiro.realm.Realm接口 -->
<bean id="myRealm" class="com.lirui.web.shiro.AiSellRealm">
</bean>
<!-- 3.lifecycleBeanPostProcessor:可以自动调用在Spring Ioc窗口中 Shiro bean的生成周期方法 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 4.启动ioc容器中使用 shiro的注解,但是必需配置在Spring Ioc容器中Shiro bean的生成周期方法 -->
<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的真实过滤器(注:这个名称必需和web.xml的代表过滤器【DelegatingFilterProxy】名称一样) -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- 登录的url,如果没有登录,访问的路径会跳到这个页面 -->
<property name="loginUrl" value="/s/login.jsp"/>
<!-- 登录成功的url,如果登录成功,会跳转到这个页面 -->
<property name="successUrl" value="/s/main.jsp"/>
<!-- 没有权限时跳转到这个位置 -->
<property name="unauthorizedUrl" value="/s/unauthorized.jsp"/>
<!--
配置哪些资源被保护,哪些资源需要权限
anon:不需要登录也可以访问相应的权限
authc:需要登陆才能访问
/** :所有文件及其子文件
-->
<property name="filterChainDefinitions">
<value>
<!-- /s/login.jsp = anon
/login = anon
/dept/index = perms[dept:index]
/employee/index = perms[employee:index]
/** = authc-->
</value>
</property>
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
</bean>
<!-- 这个bean是帮助咱们获取相应的值:它会到一个工厂bean中通过对应的方法拿到相应的值 -->
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapFactory" factory-method="builderFilterChainDefinitionMap"></bean>
<!-- 配置可以创建 -->
<bean id="filterChainDefinitionMapFactory" class="com.lirui.web.shiro.FilterChainDefinitionMapFactory">
</bean>
</beans>
- 注意
- 为什么要自定义Realm
- 配置真实过滤器的细节,路径需要
- 为什么要创建一个FilterChainDefinitionMapFactory类来配置获取url和权限的key和value
- 配置哪些资源被保护,哪些资源需要权限
anon:不需要登录也可以访问相应的权限
authc:需要登陆才能访问
/** :所有文件及其子文件
六 自定义Reaml
为了让需求自己控制,所以自定义Realm
- 获取数据的组件
- 登陆验证,授权
public class AiSellRealm extends AuthorizingRealm {
//获取到这个Realm的名称(随便取)
@Override
public String getName() {
return "MyRealm";
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//拿到主角。。登陆方法验证传过来的是什么 这里就是什么。。
String username = (String)principalCollection.getPrimaryPrincipal();
//应该从数据库中查出。。这里是假设
Set<String> roles = getRoles(username);
Set<String> permissions = getPermissions();
//核心管理器会调用doGetAuthorizationInfo方法得到这个用户的权限和角色
//设置到权限的对象并且返回
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(permissions);
//返回权限对象
return authorizationInfo;
}
public Set<String> getRoles(String username){
Set<String> set = new HashSet<String>();
//set.add("admin");
set.add("it");
return set;
}
public Set<String> getPermissions(){
Set<String> set = new HashSet<String>();
set.add("employee:index");
//set.add("dept:*");
return set;
}
//登陆验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//先拿用户传过来的令牌
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//拿到令牌的用户
String username = token.getUsername();
String password = getByName(username);
if (password==null){
return null;
}
//加盐验证
//要想使用,还需要在xml配置加密次数,和加密算法。。才可以正确解密验证
ByteSource salt = ByteSource.Util.bytes("itsource");
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,salt,getName());
return simpleAuthenticationInfo;
}
//模拟从数据库查出来的对应用户名的密码
public String getByName(String usernmae){
if ("root".equals(usernmae)){
return "admin";
}else if("lirui".equals(usernmae)) {
return "123456";
}
return null;
}
}
- 自定义需要继承extends AuthorizingRealm,覆写两个方法,登陆和授权的不同返回验证对象
- 登陆验证,用户传进来的token令牌,查询数据库密码,没有查到返回null说明用户名错误
//有用户和密码则 返回 验证对象
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,salt,getName());
- realm只是查询到数据,返回验证对象,真正的验证是核心管理器接管
七 几个测试的跳转页面
- /s/login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名: <input type="text" name="username" />
密码: <input type="text" name="password" />
<input type="submit" value="登陆">
</form>
</body>
</html>
- /s/main.jsp 登陆成功的页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
欢迎光临
</body>
</html>
- /s/unauthorized.jsp 用户已经登陆但是授权失败的页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
没有访问权限不好意思
</body>
</html>
八 controller层
@Controller
public class ShiroController {
@RequestMapping("/login")
public String login(String username,String password){
System.out.println("===========================jll");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if (!subject.isAuthenticated()){
try {
subject.login(token);
} catch (UnknownAccountException e) {
System.out.println("用户名不存在");
e.printStackTrace();
}
catch (IncorrectCredentialsException e) {
System.out.println("密码错误");
e.printStackTrace();
}
catch (AuthenticationException e) {
System.out.println("未知错误");
e.printStackTrace();
}
}
//如果登陆验证失败,要访问首页还是会被拦截下来
return "redirect:/s/main.jsp";
}
}
九 FilterChainDefinitionMapFactory类
创建这个类的原因是,如果有很多页面的权限管理,比如
/s/login.jsp = anon
/login = anon
/dept/index = perms[dept:index]
/employee/index = perms[employee:index]
不可能全写在xml配置中,应该写出来,放在对象中,所以新建一个类创建map
把需要过滤的路径和登陆授权的需要信息以key和value的形式存入
注意:因为过滤器的顺序是从上往下,所以map需要顺序,我们使用
LinkedHashMap
代码示例
public class FilterChainDefinitionMapFactory {
public LinkedHashMap<String,String> builderFilterChainDefinitionMap(){
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
/*
/s/login.jsp = anon
/login = anon
/dept/index = perms[dept:index]
/employee/index = perms[employee:index]
/** = authc
* */
//anon 不需要登陆就可以访问 authc需要登陆才可以访问 per[xx:xx]需要授权才可以
linkedHashMap.put("/s/login.jsp", "anon");
linkedHashMap.put("/login", "anon");
linkedHashMap.put("/dept/index", "perms[dept:index]");
linkedHashMap.put("/employee/index", "perms[employee:index]");
linkedHashMap.put("/**", "authc");
return linkedHashMap;
}
}
- 注意xml配置
<!-- 这个bean是帮助咱们获取相应的值:它会到一个工厂bean中通过对应的方法拿到相应的值 -->
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapFactory" factory-method="builderFilterChainDefinitionMap"></bean>
<!-- 配置可以创建 -->
<bean id="filterChainDefinitionMapFactory" class="com.lirui.web.shiro.FilterChainDefinitionMapFactory">
执行流程分析
- 发生了什么?
用户想要登陆或者访问其他页面了 - 我们后台怎么响应?
首先拦截下来,根据xml的配置信息,拦截并且跳转到登陆页面 - 输入密码用户信息点击登陆过后?
来到controller层,需要进行验证,spring创建shiro核心管理器,用户信息封装为UsernamePasswordToken,核心管理器拿到我们自定义的realm,根据token查询username的密码,如果没有查到,返回null,用户有错
如果查到了密码,将username,password,如果有加盐,一并封装为验证对象
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,salt,getName());
如果登陆成功
可以访问没有权限的页面,
如果登陆失败,返回登陆页面,跳转其他页面失败,还是会验证,因为没有登陆成功
若有权限的页面不能访问
需要访问时,核心管理器继续判断,通过realm的授权方法继续验证授权
并且将查询的授权和角色信息存入simpleAuthenticationInfo 对象返回出去。
如果验证成功,信息是存在与shiro的session中,该session远比http的session强大,什么系统都能用