Shrio第二天——认证、授权与其它特性

时间:2022-01-29 01:07:19

一、认证——Authentication

  (即登陆),简单分析之前的HelloWorld的认证:

1. 获取当前的 Subject. 调用 SecurityUtils.getSubject();
2. 测试当前的用户是否已经被认证. 即是否已经登录. 调用 Subject 的 isAuthenticated()
3. 若没有被认证, 则把用户名和密码封装为 UsernamePasswordToken 对象
1). 创建一个表单页面
2). 把请求提交到 SpringMVC 的 Handler
3). 获取用户名和密码.
4. 执行登录: 调用 Subject 的 login(AuthenticationToken) 方法.
5. 自定义 Realm 的方法, 从数据库中获取对应的记录, 返回给 Shiro.
1). 实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类
2). 实现 doGetAuthenticationInfo(AuthenticationToken) 方法.
6. 由 shiro 完成对密码的比对.

  在 shiro 中,用户需要提供 principals (身份)和 credentials(证 明)给 shiro,从而应用能验证用户身份

  接下来我们来实现一下分析的认证流程:

    第3步的创建表单页面:login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>Hello login!</h3>
<form action="shiroLogin" method="post">
username:<input type="text" name="username"/>
<br/>
password:<input type="password" name="password"/>
<br/>
<input type="submit" value="提交"/>
</form>
</body>
</html>

    创建相应的handler(controller):

package cn.shiro.handler;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; @Controller
public class ShrioHandler { /**
* 获取登陆的表单数据
* @return
*/
@RequestMapping("/shiroLogin")
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("1. " + 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());
}
}
//执行完成后重定向到list页面
return "redirect:/list.jsp";
}
}

    将前面创建的Realm改造为继承而不是实现接口的形式来实现自定义Realm:

package cn.shiro.realms;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection; public class ShiroRealm extends AuthorizingRealm { @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
// TODO Auto-generated method stub
return null;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException {
System.out.println("token:"+arg0.hashCode());
return null;
} }

    当然,我们得在配置文件中配置controller的匿名访问才可以访问:

   <property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiroLogin = anon
# everything else requires authentication:
/** = authc
</value>
</property>

    最后我们访问Login页面,可以看到结果:

Shrio第二天——认证、授权与其它特性

//证明我们的reaml拿到了用户名与密码(token)

    可以过来以后我们进行认证的完善:

  完善ShiroRealm的对比过程:

package cn.shiro.realms;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource; public class ShiroRealm extends AuthorizingRealm { @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { return null;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//System.out.println("token:"+arg0.hashCode()); //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 = "123";
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName);
//返回信息
return info;
} }

  输入unknown的测试结果(其他暂不列举)

Shrio第二天——认证、授权与其它特性

  //成功登陆(即认证)后即可访问其他的需要认证的页面

  我们来大致捋一下认证的流程:

从表单填写信息——>提交到handler,获取表单的信息——>调用login方法,把用户名密码封装为token对象继续往后传——>在Reaml中调用doGetAuthenticationInfo方法——>由token的用户名去数据库中获取用户信息进行对比,我们把密码封装成SimpleAuthenticationInfo(具体的源码分析:例如何时进行密码比对,如何比对待补充)

  接下来进行密码的加密,因为密码不能是明文,并且不能是可逆的,这里介绍常用的MD5,另外的SHA1待补充:

  1. 如何把一个字符串加密为 MD5
  2. 替换当前 Realm 的 credentialsMatcher 属性. 直接使用 HashedCredentialsMatcher 对象, 并设置加密算法即可.

我们先完成第二步的配置,在配置文件中Realm的配置处配置一个内部的bean:(配置了自定义Realm,当然得让shiro知道我们要使用自定义的Realm)

    <!--
3. 配置 Realm
3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
-->
<bean id="jdbcRealm" class="cn.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
</bean>
</property>
</bean>

  指定了凭证匹配器后会自动的把前台的明文密码加密为MD5,当然,我们可以指定加密次数:

    <property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>

  当然,即使指定了加密次数,如果两个人的密码一样,那么加密后的密码也是一样的,我们还需要改进,于是我们加入 盐值

盐值是一个ByteSource接口,一般我们使用唯一的字符串作为盐,比如不可重复的用户名,示例如下:

@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;
}

  盐值加密小结如下:

1. 为什么使用 MD5 盐值加密:
2. 如何做到:
1). 在 doGetAuthenticationInfo 方法返回值创建 SimpleAuthenticationInfo 对象的时候, 需要使用
SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 构造器
2). 使用 ByteSource.Util.bytes() 来计算盐值.
3). 盐值需要唯一: 一般使用随机字符串或 user id
4). 使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 来计算盐值加密后的密码的值.

  加密的部分请参见开涛的博客:http://jinnianshilongnian.iteye.com/blog/2021439

  接下来我们再介绍多Realm验证

  先建立一个SHA1加密的Realm:

package com.atguigu.shiro.realms;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource; public class SecondRealm extends AuthenticatingRealm { @Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[SecondReaml] 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 = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06";
}else if("user".equals(username)){
credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718";
} //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("secondRealmName", credentials, credentialsSalt, realmName);
return info;
} public static void main(String[] args) {
String hashAlgorithmName = "SHA1";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("admin");;
int hashIterations = 1024; Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
}

  在spring配置文件中配置第二个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>

  再把管家的多个Realm进行配置:

 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"></property> <property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property> <property name="rememberMeManager.cookie.maxAge" value="10"></property>
</bean>

  测试暂不举例(认证策略暂不展开):

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

 退出: 不用我们去实现退出,只要去访问一个退出的url(该 url是可以不存在),由LogoutFilter拦截住,清除session。

  在applicationContext-shiro.xml配置LogoutFilter:

    <!-- 请求 logout.action地址,shiro去清除session-->
/logout.action = logout

二、授权——Authorization

  授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作 等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限 (Permission)、角色(Role)。

  授权基础流程:

  Shrio第二天——认证、授权与其它特性

 测试授权的具体流程:

1、在applicationContext-shiro.xml中配置filter规则
<!--商品查询需要商品查询权限 -->
/items/queryItems.action = perms[item:query]
2、用户在认证通过后,请求/items/queryItems.action
3、被PermissionsAuthorizationFilter拦截,发现需要“item:query”权限
4、PermissionsAuthorizationFilter调用realm中的doGetAuthorizationInfo获取数据库中正确的权限
5、PermissionsAuthorizationFilter对item:query 和从realm中获取权限进行对比,如果“item:query”在realm返回的权限列表中,授权通过。

  相关的对象解释:

• 主体(Subject):访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权
后才允许访问相应的资源。
• 资源(Resource):在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些
数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
• 权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户
有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用
户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控
)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允
许。
• Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,
即实例级别的)
• 角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有
一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等
都是角色,不同的角色拥有一组不同的权限。
  Shiro支持的授权方式:
Shrio第二天——认证、授权与其它特性
 

 //当然编码与注解我们还是倾向于使用注解的形式

规则即:“用户名=密码,角色1,角色2”,如果需要在应用中判断用户是否有相应角色,就需要在相应的Realm中返回角色信息,也就是说Shiro不负责维护用户-角色信息,需要应用提供,Shiro只是提供相应的接口方便验证

  Shiro相关的拦截器参见day01

  1.Shiro 的 Permissions  //quickstart的shiro.ini中也有相关的介绍。

  规则:资源标识符:操作:对象实例 ID 即对哪个资源的哪个实例可以进行什么操作.

  • 实例级访问控制 – 这种情况通常会使用三个部件:域、操作、被付诸实施的实例。如:user:edit:manager,对资源user的manager实例拥有edit操作

    – 也可以使用通配符来定义,如:user:edit:*、user:*:*、 user:*:manager

    – 部分省略通配符:缺少的部件意味着用户可以访问所 有与之匹配的值,比如:user:edit 等价于 user:edit :*、 user 等价于 user:*:*

    – 注意:通配符只能从字符串的结尾处省略部件,也就 是说 user:edit 并不等价于 user:*:edit

  2.授权流程简要分析如下:

• 流程如下:
• 1、首先调用 Subject.isPermitted*/hasRole* 接口,其会委托给
SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
• 2、Authorizer是真正的授权者,如果调用如
isPermitted(“user:view”),其首先会通过
• PermissionResolver 把字符串转换成相应的 Permission 实例;
• 3、在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角
色/权限用于匹配传入的角色/权限;
• 4、Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果
有多个Realm,会委托给 ModularRealmAuthorizer 进行循环判断,
如果匹配如 isPermitted*/hasRole* 会返回true,否则返回false表示
授权失败。

  如何实现授权:

//授权会被 shiro 回调的方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
Object principal = principals.getPrimaryPrincipal(); //2. 利用登录的用户的信息来用户当前用户的角色或权限(可能需要查询数据库)
Set<String> roles = new HashSet<>();
roles.add("user");
if("admin".equals(principal)){
roles.add("admin");
} //3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); //4. 返回 SimpleAuthorizationInfo 对象.
return info;
}

 1.编码实现举例:

  ini配置文件:(后期应该改造为自定义realm,在数据库存储)

shiro-permission.ini里边的内容相当于在数据库。

#用户
[users]
#用户zhang的密码是123,此用户具有role1和role2两个角色
zhang=,role1,role2
wang=,role2 #权限
[roles]
#角色role1对资源user拥有create、update权限
role1=user:create,user:update
#角色role2对资源user拥有create、delete权限
role2=user:create,user:delete
#角色role3对资源user拥有create权限
role3=user:create

Shrio第二天——认证、授权与其它特性

 2.一般情况下,我们都需要通过自定义Realm来进行授权(也就是前面自定义Realm中另外一个未实现的方法):

    在之前的customerRealm中完成另外一个授权方法:

    // 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) { //从 principals获取主身份信息
//将getPrimaryPrincipal方法返回值转为真实身份类型(在上边的doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中身份类型),
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal(); //根据身份信息获取权限信息
//从数据库获取到权限数据
List<SysPermission> permissionList = null;
try {
permissionList = sysService.findPermissionListByUserId(activeUser.getUserid());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//单独定一个集合对象
List<String> permissions = new ArrayList<String>();
if(permissionList!=null){
for(SysPermission sysPermission:permissionList){
//将数据库中的权限标签 符放入集合
permissions.add(sysPermission.getPercode());
}
} /* List<String> permissions = new ArrayList<String>();
permissions.add("user:create");//用户的创建
permissions.add("item:query");//商品查询权限
permissions.add("item:add");//商品添加权限
permissions.add("item:edit");//商品修改权限
*/ //.... //查到权限数据,返回授权信息(要包括 上边的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//将上边查询到授权信息填充到simpleAuthorizationInfo对象中
simpleAuthorizationInfo.addStringPermissions(permissions); return simpleAuthorizationInfo;
}

     测试方法:

// 自定义realm进行资源授权测试
@Test
public void testAuthorizationCustomRealm() { // 创建SecurityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-realm.ini"); // 创建SecurityManager
SecurityManager securityManager = factory.getInstance(); // 将SecurityManager设置到系统运行环境,和spring后将SecurityManager配置spring容器中,一般单例管理
SecurityUtils.setSecurityManager(securityManager); // 创建subject
Subject subject = SecurityUtils.getSubject(); // 创建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
"111111"); // 执行认证
try {
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} System.out.println("认证状态:" + subject.isAuthenticated());
// 认证通过后执行授权 // 基于资源的授权,调用isPermitted方法会调用CustomRealm从数据库查询正确权限数据
// isPermitted传入权限标识符,判断user:create:1是否在CustomRealm查询到权限数据之内
boolean isPermitted = subject.isPermitted("user:create:1");
System.out.println("单个权限判断" + isPermitted); boolean isPermittedAll = subject.isPermittedAll("user:create:1",
"user:create");
System.out.println("多个权限判断" + isPermittedAll); // 使用check方法进行授权,如果授权不通过会抛出异常
subject.checkPermission("items:add:1"); }

      方法流程(上文已有一个版本):

1、对subject进行授权,调用方法isPermitted("permission串")
2、SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
3、ModularRealmAuthorizer执行realm(自定义的CustomRealm)从数据库查询权限数据
调用realm的授权方法:doGetAuthorizationInfo 4、realm从数据库查询权限数据,返回ModularRealmAuthorizer
5、ModularRealmAuthorizer调用PermissionResolver进行权限串比对
6、如果比对后,isPermitted中"permission串"在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。

  3.Shiro标签:

    标签的介绍参见http://jinnianshilongnian.iteye.com/blog/2026398

  4.shiro注解 

  对系统中类的方法给用户授权,建议在controller层进行方法授权。(下文有进一步原因解释)

   在springMVC中配置注解的AOP式支持:

   <!-- 开启aop,对类代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 开启shiro注解支持 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>

    

  这样就可以在controller方法上(强烈建议controller方法上,而不是其它层)使用 @RequiresPermissions("order:query") ——执行此方法需要此权限,示例需要此角色类同式注解了:

@RequiresRoles("admin")
@RequestMapping("/hello2")
public String hello2() {
return "success";
}

  当调用controller的一个方法,由于该 方法加了@RequiresPermissions("item:query") ,shiro调用realm获取数据库中的权限信息,看"item:query"是否在权限数据中存在,如果不存在就拒绝访问,如果存在就授权通过。

  当展示一个jsp页面(真身是servlet)时,页面中如果遇到<shiro:hasPermission name="item:update">,shiro调用realm获取数据库中的权限信息,看item:update是否在权限数据中存在,如果不存在就拒绝访问,如果存在就授权通过。

问题:只要遇到注解或jsp标签的授权,都会调用realm方法查询数据库,需要使用缓存解决此问题。

    jeesite中的配置(待补充):

<!-- AOP式方法级权限检查  -->
<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>

  完整配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd"
default-lazy-init="true"> <description>Shiro Configuration</description> <!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:jeesite.properties" /> <!-- Shiro权限过滤过滤器定义 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/cas = cas
${adminPath}/login = authc
${adminPath}/logout = logout
${adminPath}/** = user
/act/editor/** = user
/ReportServer/** = user
</value>
</constructor-arg>
</bean> <!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" /><!--
<property name="loginUrl" value="${cas.server.url}?service=${cas.project.url}${adminPath}/cas" /> -->
<property name="loginUrl" value="${adminPath}/login" />
<property name="successUrl" value="${adminPath}?login" />
<property name="filters">
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean> <!-- CAS认证过滤器 -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<property name="failureUrl" value="${adminPath}/login"/>
</bean> <!-- 定义Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="systemAuthorizingRealm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean> <!-- 自定义会话管理配置 -->
<bean id="sessionManager" class="com.thinkgem.jeesite.common.security.shiro.session.SessionManager">
<property name="sessionDAO" ref="sessionDAO"/> <!-- 会话超时时间,单位:毫秒 -->
<property name="globalSessionTimeout" value="${session.sessionTimeout}"/> <!-- 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话 -->
<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!-- <property name="sessionValidationSchedulerEnabled" value="false"/> -->
<property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/>
<property name="sessionIdCookieEnabled" value="true"/>
</bean> <!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="jeesite.session.id"/>
</bean> <!-- 自定义Session存储容器 -->
<!-- <bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.JedisSessionDAO"> -->
<!-- <property name="sessionIdGenerator" ref="idGen" /> -->
<!-- <property name="sessionKeyPrefix" value="${redis.keyPrefix}_session_" /> -->
<!-- </bean> -->
<bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.CacheSessionDAO">
<property name="sessionIdGenerator" ref="idGen" />
<property name="activeSessionsCacheName" value="activeSessionsCache" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean> <!-- 自定义系统缓存管理器-->
<!-- <bean id="shiroCacheManager" class="com.thinkgem.jeesite.common.security.shiro.cache.JedisCacheManager"> -->
<!-- <property name="cacheKeyPrefix" value="${redis.keyPrefix}_cache_" /> -->
<!-- </bean> -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="cacheManager"/>
</bean> <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- AOP式方法级权限检查 -->
<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> </beans>

  注解的实现请参见:http://www.cnblogs.com/lzlblogs/p/5973535.html

  【注意】:

  在Service方法上使用注解 @Transactional 即在方法开始的时候会有事务,这个时候这个Service已经是一个代理对象,

  这个是有把 权限注解加到 Service上是不好用的,会发生类型转换异常。需要加到Controller上,因为不能够让Service是代理的代理。

  并且,controller的入口只有一个,而service是可以被其它层公开调用了,就失去了授权的意义。

  4.之前我们配置的受保护的资源和对应关系是配置在applicationContext.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>

 我们可以改进为将数据存入数据库中,然后通过SQL方式进行读取,这样便于管理。

    我们把 filterChainDefinitions 配置给ShiroFilter:

 <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>

    创建相应的Map,存放资源权限数据:

package com.atguigu.shiro.factory;

import java.util.LinkedHashMap;

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("/user.jsp", "authc,roles[user]");
map.put("/admin.jsp", "authc,roles[admin]");
map.put("/list.jsp", "user"); map.put("/**", "authc"); return map;
} }

  //实际操作的时候,从数据库取出数据(一定是linkedHashMap,而且注意顺序)

  再补全配置文件:

  <!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean> <bean id="filterChainDefinitionMapBuilder"
class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder"></bean>

 通过自定义拦截器可以扩展功能,例如:动态url-角色/权 限访问控制的实现、根据 Subject 身份信息获取用户信息 绑定到 Request(即设置通用数据)、验证码验证、在线 用户信息的保存等

三、会话管理

 Shiro 提供了完整的企业级会话管理功能,不依赖于底层容 器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境 都可以使用,提供了会话管理、会话事件监听、会话存储/ 持久化、容器无关的集群、失效/过期支持、对Web 的透明 支持、SSO 单点登录的支持等特性。

  相关的API这里暂不展开。

  1.这里需要提的是Shiro的会话管理可以进入Service,这样就可以在开发时在service层访问session

package com.atguigu.shiro.services;

import java.util.Date;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session; public class ShiroService { @RequiresRoles({"admin"})
public void testMethod(){
System.out.println("testMethod, time: " + new Date()); Session session = SecurityUtils.getSubject().getSession();
Object val = session.getAttribute("key"); System.out.println("Service SessionVal: " + val);
} }

  2.SessionDao:

AbstractSessionDAO 提供了 SessionDAO 的基础实现,
如生成会话ID等
• CachingSessionDAO 提供了对开发者透明的会话缓存的
功能,需要设置相应的 CacheManager
• MemorySessionDAO 直接在内存中进行会话维护
• EnterpriseCacheSessionDAO 提供了缓存功能的会话维
护,默认情况下使用 MapCache 实现,内部使用
ConcurrentHashMap 保存缓存的会话。

示例配置:

Shrio第二天——认证、授权与其它特性

Shrio第二天——认证、授权与其它特性

    3.会话验证

  • Shiro 提供了会话验证调度器,用于定期的验证会话是否 已过期,如果过期将停止会话 • 出于性能考虑,一般情况下都是获取会话时来验证会话是 否过期并停止会话的;但是如在 web 环境中,如果用户不 主动退出是不知道会话是否过期的,

  因此需要定期的检测 会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler • Shiro 也提供了使用Quartz会话验证调度器: QuartzSessionValidationScheduler

【更新】:shiro的缓存

  针对上边授权频繁查询数据库,需要使用shiro缓存。

  shiro中提供了对认证信息和授权信息的缓存。shiro默认是关闭认证信息缓存的,对于授权信息的缓存shiro默认开启的。主要研究授权信息缓存,因为授权的数据量大。

  用户认证通过。

  该 用户第一次授权:调用realm查询数据库

  该 用户第二次授权:不调用realm查询数据库,直接从缓存中取出授权信息(权限标识符:就是一堆字符串)。

  缓存的配置步骤:

    导包:

      Shrio第二天——认证、授权与其它特性

    配置缓存管理器(cacheManager):由于缓存管理器也是归安全管理器securityManager这个大管家管理的,所以在其属性处进行配置即可:

Shrio第二天——认证、授权与其它特性

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!--diskStore:缓存数据持久化的目录 地址 -->
<diskStore path="F:\develop\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>

【更新】:验证码的引入:

  思路

  shiro使用FormAuthenticationFilter进行表单认证,验证校验的功能应该加在FormAuthenticationFilter中,在认证之前进行验证码校验。

  需要写FormAuthenticationFilter的子类,继承FormAuthenticationFilter,改写它的认证方法,在认证之前进行验证码校验。

public class CustomFormAuthenticationFilter extends FormAuthenticationFilter {

    //原FormAuthenticationFilter的认证方法
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
//在这里进行验证码的校验 //从session获取正确验证码
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpSession session =httpServletRequest.getSession();
//取出session的验证码(正确的验证码)
String validateCode = (String) session.getAttribute("validateCode"); //取出页面的验证码
//输入的验证和session中的验证进行对比
String randomcode = httpServletRequest.getParameter("randomcode");
if(randomcode!=null && validateCode!=null && !randomcode.equals(validateCode)){
//如果校验失败,将验证码错误失败信息,通过shiroLoginFailure设置到request中
httpServletRequest.setAttribute("shiroLoginFailure", "randomCodeError");
//拒绝访问,不再校验账号和密码
return true;
}
return super.onAccessDenied(request, response);
}
<!-- 自定义filter配置 -->
<property name="filters">
<map>
<!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中-->
<entry key="authc" value-ref="formAuthenticationFilter" />
</map>
</property>
<!-- 自定义form认证过虑器 -->
<!-- 基于Form表单的身份验证过滤器,不配置将也会注册此过虑器,表单中的用户账号、密码及loginurl将采用默认值,建议配置 -->
<bean id="formAuthenticationFilter"
class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter ">
<!-- 表单中账号的input名称 -->
<property name="usernameParam" value="username" />
<!-- 表单中密码的input名称 -->
<property name="passwordParam" value="password" />
<!-- 记住我input的名称 -->
<property name="rememberMeParam" value="rememberMe"/>
</bean>

四、认证与记住我

   • Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝 等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁, 下次访问时无需再登录即可访问,基本流程如下:

  • 1、首先在登录页面选中 RememberMe 然后登录成功;如果是 浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并 保存下来;——用户信息实现序列化借口,方便进行保存与反序列化。

  • 2、关闭浏览器再重新打开;会发现浏览器还是记住你的;

  • 3、访问一般的网页服务器端还是知道你是谁,且能正常访问;

  • 4、但是比如我们访问淘宝时,如果要查看我的订单或进行支付 时,此时还是需要再进行身份认证的,以确保当前用户还是你。

   如何配置:

Shrio第二天——认证、授权与其它特性

  前台页面:

               <tr>
<TD></TD>
<td><input type="checkbox" name="rememberMe" />自动登陆</td>
</tr>

//当然,name是可以配置的,详见配置文件applicationContext-shiro.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.2.xsd "> <!-- web.xml中shiro的filter对应的bean -->
<!-- Shiro 的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 -->
<property name="loginUrl" value="/login.action" />
<!-- 认证成功统一跳转到first.action,建议不配置,shiro认证成功自动到上一个请求路径 -->
<property name="successUrl" value="/first.action"/>
<!-- 通过unauthorizedUrl指定没有权限操作时跳转页面-->
<property name="unauthorizedUrl" value="/refuse.jsp" />
<!-- 自定义filter配置 -->
<property name="filters">
<map>
<!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中-->
<entry key="authc" value-ref="formAuthenticationFilter" />
</map>
</property> <!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 -->
<property name="filterChainDefinitions">
<value>
<!-- 对静态资源设置匿名访问 -->
/images/** = anon
/js/** = anon
/styles/** = anon
<!-- 验证码,可匿名访问 -->
/validatecode.jsp = anon <!-- 请求 logout.action地址,shiro去清除session-->
/logout.action = logout
<!--商品查询需要商品查询权限 ,取消url拦截配置,使用注解授权方式 -->
<!-- /items/queryItems.action = perms[item:query]
/items/editItems.action = perms[item:edit] -->
<!-- 配置记住我或认证通过可以访问的地址 -->
/index.jsp = user
/first.action = user
/welcome.jsp = user
<!-- /** = authc 所有url都必须认证通过才可以访问-->
/** = authc
<!-- /** = anon所有url都可以匿名访问 --> </value>
</property>
</bean> <!-- securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm" />
<!-- 注入缓存管理器 -->
<property name="cacheManager" ref="cacheManager"/>
<!-- 注入session管理器 -->
<property name="sessionManager" ref="sessionManager" />
<!-- 记住我 -->
<property name="rememberMeManager" ref="rememberMeManager"/> </bean> <!-- realm -->
<bean id="customRealm" class="cn.itcast.ssm.shiro.CustomRealm">
<!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean> <!-- 凭证匹配器 -->
<bean id="credentialsMatcher"
class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="1" />
</bean> <!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean> <!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session的失效时长,单位毫秒 -->
<property name="globalSessionTimeout" value="600000"/>
<!-- 删除失效的session -->
<property name="deleteInvalidSessions" value="true"/>
</bean> <!-- 自定义form认证过虑器 -->
<!-- 基于Form表单的身份验证过滤器,不配置将也会注册此过虑器,表单中的用户账号、密码及loginurl将采用默认值,建议配置 -->
<bean id="formAuthenticationFilter"
class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter ">
<!-- 表单中账号的input名称 -->
<property name="usernameParam" value="username" />
<!-- 表单中密码的input名称 -->
<property name="passwordParam" value="password" />
<!-- 记住我input的名称 -->
<property name="rememberMeParam" value="rememberMe"/>
</bean> <!-- rememberMeManager管理器,写cookie,取出cookie生成用户信息 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie" />
</bean>
<!-- 记住我cookie -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- rememberMe是cookie的名字 -->
<constructor-arg value="rememberMe" />
<!-- 记住我cookie生效时间30天 -->
<property name="maxAge" value="2592000" />
</bean> </beans>

  配置某些页面可以使用记住我访问:

Shrio第二天——认证、授权与其它特性

   认证与记住我的区别:

  subject.isAuthenticated() 表示用户进行了身份验证登录的, 即使有 Subject.login 进行了登录;

  • subject.isRemembered():表示用户是通过记住我登录的, 此时可能并不是真正的你(如你的朋友使用你的电脑,或者 你的cookie 被窃取)在访问的

  • 两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样。

        // rememberme
token.setRememberMe(true);

  //一般为若单选框的进行了勾选则设置记住我,还可以进一步设置记住我的时间。

 记住我请参阅:http://jinnianshilongnian.iteye.com/blog/2031823