上一篇我们剖析了Shiro的整个认证思路,这次来动手实现一个简单的Web登录认证程序。
首先在MyEclipse中创建一个Web Project:
然后在lib中加入Shiro/Spring/SpringMVC以及ehcache和日志相关jar:
然后在src下创建Spring配置文件applicationContext.xml以及缓存ehcache.xml文件,
在WEB-INF下创建SpringMVC配置文件,具体配置如下:
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="realm" ref="shiroRealm"/>
</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="shiroRealm" class="com.test.shiro.realms.ShiroRealm"></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="/index.jsp"/>
<!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
</beans>
ehcache.xml:
<ehcache>
<diskStore path="java.io.tmpdir"/>
<cache name="authorizationCache"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<cache name="sampleCache1"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<cache name="sampleCache2"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/>
</ehcache>
spring-servlet.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"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.test.shiro"></context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler/>
</beans>
然后在web.xml中配置Spring加载器,SpringMVC前端控制器,以及Shiro的shiroFilter:
<?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_3_0.xsd"
id="WebApp_ID" version="3.0">
<!-- Spring加载器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- SpringMVC 前端控制器 -->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--
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>
工程结构如下:
然后在WebRoot下创建一个名为login.jsp的页面:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>登录</title>
</head>
<body>
<form action="userAuth/login" method="post">
账号:<input type="text" name="username"><br/><br/>
密码:<input type="password" name="password"><br/><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
可以看到登录请求的名称为“userAuth/login”,由于我们使用的是SpringMVC框架,所以需要编写相应的Controller方法(Handler类)来响应该请求。
在src中创建名为ShiroLoginController的类,并编写相关的请求响应方法:
package com.test.shiro.controller;
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;
@Controller
@RequestMapping("userAuth")
public class ShiroLoginController {
@RequestMapping("login")
public String login(String username,String password){
//获取当前的Subject
Subject currentUser = SecurityUtils.getSubject();
//测试当前用户是否已经被认证(即是否已经登录)
if (!currentUser.isAuthenticated()) {
//将用户名与密码封装为UsernamePasswordToken对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);//记录用户
try {
currentUser.login(token);//调用Subject的login方法执行登录
} catch (AuthenticationException e) {//所有认证时异常的父类
System.out.println("登录失败:"+e.getMessage());
}
}
return "redirect:/list.jsp";
}
}
然后在WebRoot下创建一个list.jsp作为的登录成功后的响应界面:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>首页</title>
</head>
<body>
登录成功!欢迎访问首页O(∩_∩)O
<a href="userAuth/logout">登出</a>
</body>
</html>
修改index.jsp,因为在applicationContext.xml中设置未授权时默认跳转至index.jsp,所以在
该页面要提示用户没有权限:
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>提示</title>
</head>
<body>
抱歉,没有权限访问该资源!<br>
</body>
</html>
别忘记在applicationContext.xml的shiroFilter对应的url过滤机制中对userAuth域下登入登出请求
放行:
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/userAuth/login = anon
/userAuth/logout = logout
# everything else requires authentication:
/** = authc
</value>
</property>
其中anon(anonymous)拦截器表示匿名访问(既不需要登录即可访问)。
其中logout表示该Subject对应的用户登出(登出操作由Shiro来执行,无需自定义方法)。
如前面一篇总结所讲,Shiro认证时需要一个Realm。而在上面的applicationContext.xml中已经为securityManager配置了一个realm:
<bean id="shiroRealm" class="com.test.shiro.realms.ShiroRealm"></bean>
所以要在src下创建该realm类:
package com.test.shiro.realms;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.realm.AuthenticatingRealm;
public class ShiroRealm extends AuthenticatingRealm{
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// TODO Auto-generated method stub
return null;
}
}
注意,前面我们说过,如果仅仅需要校验的话,无需实现Realm接口,只需要继承
AuthenticatingRealm类,以及实现doGetAuthenticationInfo方法即可。因为在之前剖析
Subject的login方法源码的时候知道,其login方法最终调用的是AuthenticatingRealm实现
类的doGetAuthenticationInfo方法,而其参数token就是封装的UsernamePasswordToken(可以
通过在两个方法中分别调用token.hashCode()来观察是否是一个对象)。
在doGetAuthenticationInfo方法中添加具体校验的逻辑:
package com.test.shiro.realms;
import java.util.HashMap;
import java.util.Map;
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.realm.AuthenticatingRealm;
import com.test.shiro.po.User;
public class ShiroRealm extends AuthenticatingRealm{
private static Map<String,User> userMap = new HashMap<String,User>();
static{
//使用Map模拟数据库获取User表信息
userMap.put("jack", new User("jack","aaa123",false));
userMap.put("tom", new User("tom","bbb321",false));
userMap.put("jean", new User("jean","ccc213",true));
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//1.把AuthenticationToken转换为UsernamePasswordToken
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//2.从UsernamePasswordToken中获取username
String username = userToken.getUsername();
//3.调用数据库的方法,从数据库中查询Username对应的用户记录
System.out.println("从数据看中获取UserName为"+username+"所对应的信息。");
//Map模拟数据库取数据
User u = userMap.get(username);
//4.若用户不行存在,可以抛出UnknownAccountException
if(u==null){
throw new UnknownAccountException("用户不存在");
}
//5.若用户被锁定,可以抛出LockedAccountException
if(u.isLocked()){
throw new LockedAccountException("用户被锁定");
}
//6.根据用户的情况,来构建AuthenticationInfo对象,通常使用的实现类为SimpleAuthenticationInfo
//以下信息是从数据库中获取的
//1)principal:认证的实体信息,可以是username,也可以是数据库表对应的用户的实体对象
Object principal = u.getUsername();
//2)credentials:密码
Object credentials = u.getPassword();
//3)realmName:当前realm对象的name,调用父类的getName()方法即可
String realmName = getName();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,realmName);
return info;
}
}
其中User类:
package com.test.shiro.po;
public class User {
private String username;
private String password;
private boolean Locked;
public User(String username, String password, boolean locked) {
super();
this.username = username;
this.password = password;
Locked = locked;
}
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;
}
public boolean isLocked() {
return Locked;
}
public void setLocked(boolean locked) {
Locked = locked;
}
}
上面的验证的主要核心步骤总结起来为3步:
1)把AuthenticationToken转换为UsernamePasswordToken
2)从UsernamePasswordToken中获取username
3)预校验(账户存在性,锁定等业务校验)后,将账户信息封装至AuthenticationInfo的某种
实现类,返回出去。
所以我们在doGetAuthenticationInfo中做的事情,就是将正确的用户信息封装至AuthenticationInfo
类中,返回给Shiro,具体的密码比对验证工作交由Shiro来完成。
我们下面来测试一下认证功能是否成功。首先将工程部署至tomcat中:
启动tomcat,在浏览器中输入“http://localhost:8080/Shiro3/login.jsp”,
可以看到登录界面:
然后输入jack对应的账号密码,点击登录,发现登录成功:
点击“登出”之后,会回到登录界面,然后输入不存在的账号:
可以在后台看到用户不存在的信息:
然后输入正确的账号和错误的密码,在后台可以看到:
其中报错信息中提到了作为密码表标识的credentials是与数据库不匹配的。
最后输入被锁定的jean的账号密码:
可以看到后台提示用户被锁定:
至此我们的一个简单的Shiro校验工程编写完毕。
注意我们使用的AuthenticationInfo对象为SimpleAuthenticationInfo实现类,而一般密码都是会进行加密的,所以当加密的时候要使用其它类型的AuthenticationInfo实现类,这个在下一篇总结中会提到。