Apache Shiro 是一个安全认证框架,和 Spring Security 相比,在于他使用了比较简洁易懂的认证和授权方式。其提供的 native-session(即把用户认证后的授权信息保存在其自身提供Session 中)机制,这样就可以和 HttpSession、EJB Session Bean 的基于容器的 Session 脱耦,和客户端应用、Flex 应用、远程方法调用等都可以使用它来配置权限认证。在exit-web-framework 里的 vcs-admin 例子用到该框架,具体使用说明可以参考官方帮助文档。
在这里主要讲解如何与 Spring 结合、以及认证、授权。
Apache Shiro 结合 Spring Shiro 拥有对 Spring Web 应用程序的一流支持。在 Web 应用程序中,所有 Shiro 可访问的万恶不请求必须通过一个主要的 Shiro 过滤器。该过滤器本身是极为强大的,允许临时的自定义过滤器链基于任何 URL 路径表达式执行。在 Shiro 1.0 之前,你不得不在 Spring web 应用程序中使用一个混合的方式,来定义 Shiro 过滤器及所有它在 web.xml 中的配置属性,但在 Spring XML 中定义 SecurityManager。这有些令人沮丧,由于你不能把你的配置固定在一个地方,以及利用更为先进的 Spring 功能的配置能力,如PropertyPlaceholderConfigurer 或抽象 bean 来固定通用配置。现在在 Shiro 1.0 及以后版本中,所有 Shiro 配置都是在 Spring XML 中完成的,用来提供更为强健的 Spring 配置机制。 Shiro框架执行流程与原理详细图解如下图所示:
1. 导入 jar 包(Eclipse 或 IDEA 基于 Spring 创建 Maven 工程这里就不做演示):
<!-- apache shiro dependencies -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.2.3</version>
</dependency>
2. 以下是如何在基于 Spring web 应用程序中配置 Shiro 核心控制器(web.xml):
<!-- Shiro核心控制器,一定放在struts2过滤器之前 filter-name这个名字的值将来还会在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>
3. 产生代理类的方式:
下面这行代码放置在 applicationContext.xml 的事务管理器声明之前。
<!-- 告诉spring生成shiro代理子类时,采用cglib方式生成 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
4. Shiro 的 bean 配置文件:
<description>Shiro与Spring整合</description>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- Single realm app. If you have multiple realms, use the 'realms' property instead. -->
<property name="realm" ref="authRealm"/><!-- 引用自定义的realm -->
</bean>
<!-- 自定义Realm域的编写 -->
<bean id="authRealm" class="cn.itcast.shiro.AuthRealm">
<!-- 注入自定义的密码比较器 -->
<property name="credentialsMatcher" ref="customerCredentialsMatcher" ></property>
</bean>
<!-- 自定义的密码比较器 -->
<bean id="customerCredentialsMatcher" class="cn.itcast.shiro.CustomerCredentialsMatcher"></bean>
<!-- filter-name这个名字的值来自于web.xml中filter的名字 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--登录页面 -->
<property name="loginUrl" value="/index.jsp"></property>
<!-- 登录成功后 -->
<!-- <property name="successUrl" value="/home.action"></property> -->
<property name="filterChainDefinitions">
<!-- /**代表下面的多级目录也过滤 -->
<value>
/index.jsp* = anon
/home* = anon
/sysadmin/login/login.jsp* = anon
/sysadmin/login/loginAction_logout* = anon
/login* = anon
/logout* = anon
/components/** = anon
/css/** = anon
/img/** = anon
/js/** = anon
/plugins/** = anon
/images/** = anon
/js/** = anon
/make/** = anon
/skin/** = anon
/stat/** = anon
/ufiles/** = anon
/validator/** = anon
/resource/** = anon
/** = authc
/*.* = authc </value>
</property>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.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>
5. 在 applicationContext.xml 中加载 Shiro 配置文件:
<!--组装其它 配置文件-->
<import resource="classpath:spring/applicationContext-shiro.xml"/>
自定义 AuthRealm 类:
/**
*Realm域的类需要继承AuthorizingRealm
*/
public class AuthRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//授权
/**
* 这个方法在什么时候调用?当jsp页面上第一次出现shiro标签时就会自动调用
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
//1.从Shiro中取出用户对象
User user = (User)pc.fromRealm(this.getName()).iterator().next();
List<String> permission = new ArrayList<String>();
//2.通过对象关联查询,加载用户的角色列表
Set<Role> roles = user.getRoles();
//3.遍历角色列表,得到每个角色
for (Role role : roles) {
Set<Module> modules = role.getModules();
//对象导航查询,模块列表
for (Module module : modules) {
if(!permission.contains(module)){
permission.add(module.getName());
}
}
}
//生成一个返回值的对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permission);
return info;
}
//认证 token参数代表用户在界面上输入的用户名和密码
/**
* 如果返回值为null,shiro就会抛出异常
*
* 如果返回值不为null,程序就会自动调用密码比较器
*
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //1.向下转型
final UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2.调用业务方法,实现根据用户名进行查询
Specification<User> spec = new Specification<User>() {
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(root.get("userName").as(String.class), upToken.getUsername());
}
};
List<User> userList = userService.find(spec);
//3.判断
if(userList!=null && userList.size()>0){
User user = userList.get(0);
//三个参数,第一个参数Principal代表用户对象,第二个参数credentials 代表密码 第三个参数realmName代表realm域的名字
return new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
}
return null;
}
}
自定义 CustomerCredentialsMatcher 密码比较器类:
/**
* 密码比较器
*/
public class CustomerCredentialsMatcher extends SimpleCredentialsMatcher {
/**
* 执行密码比较
* 第一个参数:用户在界面上输入的用户名和密码
* 第二个参数info:代表用户的全部信息
*
* 返回值:
* true代表密码比较成功
* false代表密码比较失败,Shiro就会抛出异常
*/
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//1.向下转型
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2.调用Md5Hash算法 第一个参数:明文 第二个参数:盐 第三个参数:加盐的次数,代表轮换的次数
//String uipwdEncrypt = new Md5Hash(new String(upToken.getPassword()), upToken.getUsername(), 2).toString();
String uipwdEncrypt = Encrypt.md5(new String(upToken.getPassword()), upToken.getUsername());
//3.可以得到这个用户在数据库中的密文
String dbpwd = info.getCredentials().toString();
return equals(uipwdEncrypt, dbpwd);
}
}
LoginAction 登陆 Action 类:
/**
* @Description: 登录和退出类
*
* 继承BaseAction的作用
* 1.可以与struts2的API解藕合
* 2.还可以在BaseAction中提供公有的通用方法
*/
@Namespace("/")
@Results({
@Result(name="login",location="/WEB-INF/pages/sysadmin/login/login.jsp"),
@Result(name="success",location="/WEB-INF/pages/home/fmain.jsp"),
@Result(name="logout",location="/index.jsp")})
public class LoginAction extends BaseAction {
private static final long serialVersionUID = 1L;
private String username;
private String password;
@Action("loginAction_login")
public String login() throws Exception {
//SSH传统登录方式
//if(true){
// String msg = "登录错误,请重新填写用户名密码!";
// this.addActionError(msg);
// throw new Exception(msg);
//}
//User user = new User(username, password);
//User login = userService.login(user);
//if (login != null) {
// ActionContext.getContext().getValueStack().push(user);
// session.put(SysConstant.CURRENT_USER_INFO, login); //记录session
// return SUCCESS;
//}
//return "login";
if(UtilFuns.isEmpty(username)){
return "login";
}
try {
//1.得到Subject
Subject subject = SecurityUtils.getSubject();
//2.调用登录方法
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);//调用登录
//3.从Shiro中取出用户信息
User user = (User)subject.getPrincipal();
//4.将用户信息放入session域中
session.put(SysConstant.CURRENT_USER_INFO, user);
} catch (Exception e) {
e.printStackTrace();
request.put("errorInfo", "对不起,用户名或密码错误,登录失败!");
return "login";
}
return SUCCESS;
}
//退出
@Action("loginAction_logout") public String logout(){
session.remove(SysConstant.CURRENT_USER_INFO); //删除session
SecurityUtils.getSubject().logout(); //登出
return "logout";
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Shiro 框架与 Spring 整合效果如下图所示: