本小节使用SpringJDBC、SpringMVC、Shiro实现一个简单的权限验证。实际开发中数据层可以使用Hibernate、Mybatis等数据层框架。本节的示例只是介绍Web工程中使用Shiro验证的基本流程,完整的权限系统会比这个示例复杂的多。为了简化代码,这个示例中没有写接口。
这个测试项目用第2小节中的数据进行测试。
工程中定义几个地址:
/gologin.html 不需要权限验证就可以访问
/login.html 不需要验证就可以访问
/doadd.html 要有perm1和perm2权限才可以访问,访问成功后页面显示add
/doget.html 要有admin权限才可以访问,访问成功后页面显示get
/doupdate.html 要有perm1权限才能访问,访问成功后页面显示update
/dodel.html 要有perm2权限才可以访问,访问成功后页面显示del
第2小节的测试数据中,test@shiro.com这个用户由perm1和perm2两个权限,没有admin权限,所以/doget.html访问不到,会显示error。
3.1 创建web工程
完整的工程目录如下:
3.2 导入jar包
!--shiro核心类库-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<!--日志工具包-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<!--spring相关包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
3.3 DAO层
package dao;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
public class ShiroDAO {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* 根据用户名查询密码
*/
public String getPasswordByUserName(String username) {
String sql = "select PASSWORD from SHIRO_USER where USER_NAME = ?";
String password = jdbcTemplate.queryForObject(sql, String.class, username);
return password;
}
/**
* 查询当前用户对应的权限
*/
public List<String> getPermissionByUserName(String username) {
String sql = "select P.PERM_NAME from SHIRO_ROLE_PERMISSION P inner join SHIRO_USER_ROLE R on P.ROLE_NAME=R.ROLE_NAME where R.USER_NAME = ?";
List<String> perms = jdbcTemplate.queryForList(sql, String.class, username);
return perms;
}
}
3.4 Service层
package service;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import dao.ShiroDAO;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import java.util.List;
public class ShiroService {
private ShiroDAO shiroDAO;
public void setShiroDAO(ShiroDAO shiroDAO) {
this.shiroDAO = shiroDAO;
}
/**
* 登录
*/
public void doLogin(String username, String password) throws Exception {
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token =
new UsernamePasswordToken(username, password);
token.setRememberMe(true);//是否记住用户
try {
currentUser.login(token);//执行登录
} catch (UnknownAccountException uae) {
throw new Exception("账户不存在");
} catch (IncorrectCredentialsException ice) {
throw new Exception("密码不正确");
} catch (LockedAccountException lae) {
throw new Exception("用户被锁定了 ");
} catch (AuthenticationException ae) {
ae.printStackTrace();
throw new Exception("未知错误");
}
}
}
/**
* 根据用户名查询密码
*/
public String getPasswordByUserName(String username) {
return shiroDAO.getPasswordByUserName(username);
}
/**
* 查询用户所有权限
*/
public List<String> getPermissionByUserName(String username) {
return shiroDAO.getPermissionByUserName(username);
}
}
注:rememberMe后浏览器里会生成一个cookie:
如果访问的路径,要求权限是user,所有使用过rememberMe的用户就都可以访问。但是它只是记录你登录过,不会记住你是谁以及你的权限信息。
3.5 Controller
package controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import service.ShiroService;
@Controller
public class ShiroController {
@Autowired
private ShiroService shiroService;
@RequestMapping("/gologin.html")
public String goLogin() {
return "/login.jsp";
}
@RequestMapping("/login.html")
public ModelAndView login(String username, String password) {
try {
shiroService.doLogin(username, password);
} catch (Exception e) {
return new ModelAndView("/error.jsp", "msg", e.getMessage());
}
return new ModelAndView("/index.jsp");
}
@RequestMapping("/logout.html")
public String logout() {
Subject currentUser = SecurityUtils.getSubject();
currentUser.logout();
return "/login.jsp";
}
/**
* 模拟不同的请求,在配置文件中对请求进行权限拦截
*/
@RequestMapping("/do{act}.html")
public ModelAndView get(@PathVariable String act) {
//简化代码,省略数据库操作
//通过页面上显示的信息查看请求是否被拦截
return new ModelAndView("/page.jsp", "page", act);
}
}
3.6 自定义Realm
package util;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import service.ShiroService;
import java.util.List;
public class MyShiroRealm extends AuthorizingRealm {
private ShiroService shiroService;
public void setShiroService(ShiroService shiroService) {
this.shiroService = shiroService;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo
(PrincipalCollection principalCollection) {
//根据自己的需求编写获取授权信息,这里简化代码获取了用户对应的所有权限
String username = (String) principalCollection.fromRealm(getName()).iterator().next();
if (username != null) {
List<String> perms = shiroService.getPermissionByUserName(username);
if (perms != null && !perms.isEmpty()) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (String each : perms) {
//将权限资源添加到用户信息中
info.addStringPermission(each);
}
return info;
}
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo
(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 通过表单接收的用户名,调用currentUser.login的时候执行
String username = token.getUsername();
if (username != null && !"".equals(username)) {
//查询密码
String password = shiroService.getPasswordByUserName(username);
if (password != null) {
return new SimpleAuthenticationInfo(username, password, getName());
}
}
return null;
}
}
3.7 配置文件
resources目录下的db.properties用于存放数据库配置信息:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/数据库?characterEncoding=utf-8
jdbc.username=用户名
jdbc.password=密码
log4j.properties
log4j.rootLogger=INFO, stdout,
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
WEB-INF目录下的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"
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.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<!--读取配置文件-->
<context:property-placeholder location="classpath:db.properties" ignore-unresolvable="true"/>
<!--从配置文件中获取数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置Jdbc模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="shiroDAO" class="dao.ShiroDAO">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<bean id="shiroService" class="service.ShiroService">
<property name="shiroDAO" ref="shiroDAO"/>
</bean>
<bean id="myShiroRealm" class="util.MyShiroRealm">
<property name="shiroService" ref="shiroService"/>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myShiroRealm"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--去登录的地址-->
<property name="loginUrl" value="/gologin.html"/>
<!--登录成功的跳转地址-->
<property name="successUrl" value="/index.html"/>
<!--验证失败的跳转地址-->
<property name="unauthorizedUrl" value="/error.jsp"/>
<!--定义过滤的规则-->
<!--复杂的系统中,url和权限都可以从数据库中读取-->
<!--anon是不需要验证,authc时需要验证,perms[admin]代表要admin权限-->
<property name="filterChainDefinitions">
<value>
/gologin.html = anon
/login.html = anon
/doadd.html = authc, perms[perm1,perm2]
/doget.html = authc, perms[admin]
/doupdate.html = authc, perms[perm1]
/dodel.html = authc, perms[perm2]
</value>
</property>
</bean>
</beans>
shiro过滤器过滤属性含义:
securityManager:这个属性是必须的。
loginUrl :没有登录的用户请求需要登录的页面时自动跳转到登录页面,不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
successUrl :登录成功默认跳转页面,不配置则跳转至”/”。如果登陆前点击的一个需要登录的页面,则在登录自动跳转到那个需要登录的页面。不跳转到此。
unauthorizedUrl :没有权限默认跳转的页面
其权限过滤器及配置释义:
anon:
例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:
例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数
roles(角色):
例子/admins/user/**=roles[admin],参数可以写多个,参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms(权限):
例子/admins/user/**=perms[add],参数可以写多个,例如/admins/user/**=perms["add, modify"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:
例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:
例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString
是你访问的url里的?后面的参数。
authcBasic:
例如/admins/user/**=authcBasic没有参数.表示httpBasic认证
ssl:
例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user:
例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查
springMVC-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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<context:annotation-config />
<!-- 启动自动扫描 -->
<context:component-scan base-package="controller">
<!-- 制定扫包规则 ,只扫描使用@Controller注解的JAVA类 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Archetype Created Web Application</display-name>
<!--配置listener,在启动Web容器的时候加载Spring的配置-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--欢迎页面-->
<welcome-file-list>
<welcome-file>gologin.html</welcome-file>
</welcome-file-list>
<!--配置DispatcherServlet-->
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<!-- 配置shiro的核心拦截器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>*.html</url-pattern>
</filter-mapping>
</web-app>
3.8 页面
页面没有添加样式,将这些代码写在body中即可。
login.jsp
<form action="/login.html" method="post">
username:<input type="text" name="username"/><br/>
password:<input type="password" name="password"/><br/>
<input type="submit" value="login"/>
</form>
index.jsp
<a href="/doadd.html" target="_blank">add</a><br/>
<a href="/dodel.html" target="_blank">del</a><br/>
<a href="/doupdate.html" target="_blank">update</a><br/>
<a href="/doget.html" target="_blank">get</a><br/>
page.jsp
<h2>${page}</h2>
error.jsp
<h2>ERROR:${msg}</h2>
测试结果:
登录成功:
点击add、del、update可以看到对应的页面:
点击get会报错,并且直接跳转到error.jsp,因为没有权限:
3.9 动态配置过滤规则
在实际开发中,url和对应的访问权限可能需要从数据库中读取,我们可以定义一个工具类从数据库中读取访问权限并传递给Shiro。
package util;
import org.apache.shiro.config.Ini;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.web.config.IniFilterChainResolverFactory;
import org.springframework.beans.factory.FactoryBean;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
public class MyChainDefinitions implements FactoryBean<Ini.Section> {
public static final String PREMISSION_STRING = "perms[{0}]";
public static final String ROLE_STRING = "roles[{0}]";
private String filterChainDefinitions;
public void setFilterChainDefinitions(String filterChainDefinitions) {
this.filterChainDefinitions = filterChainDefinitions;
}
@Override
public Ini.Section getObject() throws Exception {
//urls可以从数据库查出来,此处省略代码,直接模拟几条数据
Set<String> urls = new LinkedHashSet<>();
urls.add("/dotest1.html");
urls.add("/dotest2.html");
//每个url对应的权限也可以从数据库中查出来,这里模拟几条数据
Map<String, String[]> permsMap = new HashMap<>();
permsMap.put("/dotest1.html", new String[]{"perm1", "admin"});
permsMap.put("/dotest2.html", new String[]{"perm1"});
//加载配置默认的过滤链
Ini ini = new Ini();
ini.load(filterChainDefinitions);
Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
if (CollectionUtils.isEmpty(section)) {
section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
}
for (String url : urls) {
String[] perms = permsMap.get(url);
StringBuilder permFilters = new StringBuilder();
for (int i = 0; i < perms.length; i++) {
permFilters.append(perms[i]).append(",");
}
//去掉末尾的逗号
String str = permFilters.substring(0, permFilters.length() - 1);
//生成结果如:/dotest1.html = authc, perms[admin]
section.put(url, MessageFormat.format(PREMISSION_STRING, str));
}
return section;
}
@Override
public Class<?> getObjectType() {
return this.getClass();
}
@Override
public boolean isSingleton() {
return false;
}
}
注意section中是以Map存放的数据,所以放入相同的key,后放的会覆盖先放的数据。
修改spring的配置:
<!--声明自定义规则-->
<bean id="myChainDefinitions" class="util.MyChainDefinitions">
<!--静态的条件-->
<property name="filterChainDefinitions">
<value>
/gologin.html = anon
/login.html = anon
/doadd.html = authc, perms[perm1,perm2]
/doget.html = authc, perms[admin]
/doupdate.html = authc, perms[perm1]
/dodel.html = authc, perms[perm2]
/logout.html=user
</value>
</property>
</bean>
将shiroFilter中的filterChainDefinitions替换掉:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/gologin.html"/>
<property name="successUrl" value="/index.html"/>
<property name="unauthorizedUrl" value="/error.jsp"/>
<!--定义过滤的规则-->
<property name="filterChainDefinitionMap" ref="myChainDefinitions"/>
</bean>
可以访问/dotest1.html和/dotest2.html查看拦截效果。
3.10 重写过滤器
/doadd.html = authc,perms[perm1,perm2]
shiro默认的拦截是要满足所有的条件,但有时我们只要满足其中的一个,用于拥有perm1或perm2任何一个条件就可以访问/doadd.html。这时我们可以重写过滤器,将and变成or
package util;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
public class MyShiroPermFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed
(ServletRequest req, ServletResponse resp, Object mappedValue)
throws Exception {
Subject subject = getSubject(req, resp);
String[] permsArray = (String[]) mappedValue;
if (permsArray == null || permsArray.length == 0) { //没有权限限制
return true;
}
for (int i = 0; i < permsArray.length; i++) {
//如果是角色,就是subject.hasRole()
//若当前用户是permsArray中的任何一个,则有权限访问
if (subject.isPermitted(permsArray[i])) {
return true;
}
}
return false;
}
}
此处需要引入servlet的jar包:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
在spring中的配置,在id=”shiroFilter”的bean中加入过滤拦截:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/gologin.html"/>
<property name="successUrl" value="/index.html"/>
<property name="unauthorizedUrl" value="/error.jsp"/>
<property name="filterChainDefinitionMap" ref="myChainDefinitions"/>
<!--修改后的过滤规则,从and变成or-->
<property name="filters">
<map>
<entry key="perms">
<bean class="util.MyShiroPermFilter"/>
</entry>
</map>
</property>
</bean>
3.11 rememberMe属性
rememberMe可以在浏览器中设置cookie,在spring配置中可以设置cookie的属性,如过期时间、cookie名字、加密的秘钥等:
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMeShiro"/><!-- 浏览器中cookie的名字 -->
<property name="httpOnly" value="true"/><!--document对象中就看不到cookie了-->
<property name="maxAge" value="2592000"/><!-- 30天 -->
</bean>
<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!--秘钥要16位,24位或32位的Base64。这个解密后是1234567890abcdef-->
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('MTIzNDU2Nzg5MGFiY2RlZg==')}"/>
<property name="cookie" ref="rememberMeCookie"/>
</bean>
在securityManager中加入rememberMe中加入配置:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myShiroRealm"/>
<!--加入rememberMe的设置-->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
HttpOnly属性:
浏览器中通过document.cookie可以获取cookie属性,设置了HttpOnly=true,在脚本中就不能的到cookie了。可以避免cookie被盗用。