Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。
HP-shiro-spring是一个简单的基于Spring实现shiro的例子,进行用户的身份认证,实现基于role的授权。该项目是由MyEclipse进行构建的动态web项目。
项目具体实现层次结构如下:
- 首先定义shiro.ini,用来指定用户身份和凭据。
[users]
root = secret, root
guest = guest, guest
gandhi = 12345, role1, role2
bose = 67890, role2
[roles]
root = *
role1 = filesystem:*,system:*
role2 = "calculator:add,subtract"
上面的shiro.ini文件定义了四个用户,格式为“用户名=密码,角色”;每个角色拥有一些权限。
root拥有所有的权限,role1拥有filesystem以及system的所有权限,role2拥有calculator的add和substract权限。这些权限在当前用户对系统资源进行访问的时候要用到。
2.定义配置文件
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_2_5.xsd"
version="2.5">
<!-- 作用:在启动Web容器时,自动装配Spring applicationContext.xml的配置信息 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<!-- The filter-name matches name of a 'shiroFilter' bean inside applicationContext.xml -->
<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>
<!-- Make sure any request you want accessible to Shiro is filtered. catches all
requests. Usually this filter mapping is defined first (before all others) to
ensure that Shiro works in subsequent filters in the filter chain: -->
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<servlet>
<servlet-name>springMvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!--
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.hp.shiro.simplerbac.controller.LoginServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>logout</servlet-name>
<servlet-class>com.hp.shiro.simplerbac.controller.LogoutServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>home</servlet-name>
<servlet-class>com.hp.shiro.simplerbac.controller.HomeServlet</servlet-class>
</servlet>
-->
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>logout</servlet-name>
<url-pattern>/logout</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>home</servlet-name>
<url-pattern>/home/*</url-pattern>
</servlet-mapping>
-->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
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"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- is actually rather pointless. It declares explicit support
for annotation-driven MVC controllers (i.e.@RequestMapping,
@Controller, etc), even though support for those is the default
behaviour
当我们需要controller返回一个map的json对象时,可以设定<mvc:annotation-driven />
会自动注册DefaultAnnotationHandlerMapping与AnnotationMethodHandlerAdapter
两个bean,是spring MVC为@Controllers分发请求所必须的。并提供了:数据绑定支持,
@NumberFormatannotation支持,@DateTimeFormat支持,@Valid支持,读写XML
的支持(JAXB),读写JSON的支持(Jackson)-->
<mvc:annotation-driven />
<!-- 指定静态资源的位置,例如js,css和图片等文件,放到webroot文件夹下 -->
<mvc:resources mapping="/css/**" location="/css/" />
<mvc:default-servlet-handler />
<!-- 启用spring mvc注解 例如 @Required, @Autowired, @PostConstruct-->
<context:annotation-config />
<!-- 设置使用注解的类所在的包名 -->
<context:component-scan base-package="com.hp.shiro.simplerbac.controller" />
<!--完成请求和注解pojo的映射。
当我们需要controller返回一个map的json对象时,可以设定<mvc:annotation-driven />,
同时设定<mvc:message-converters> 标签,设定字符集和json处理类 -->
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<bean
class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
</list>
</property>
</bean>
</list>
</property>
</bean>
<!-- <bean
class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping" />
-->
<!-- 视图解析器,对转向页面的路径解析。prefix:前缀,suffix:后缀 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- <bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/> -->
</beans>
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"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<bean id="iniRealm" class="org.apache.shiro.realm.text.IniRealm">
<property name="resourcePath" value="classpath:/shiro.ini" />
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="iniRealm" />
</bean>
<!--Shiro 生命周期处理器-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.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>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login" />
<property name="successUrl" value="/home/" />
<property name="filterChainDefinitions">
<value>
/home/** = authc
</value>
</property>
</bean>
</beans>
3.然后定义ProtectedService.java来实现功能。
package com.hp.shiro.simplerbac.bean;
import java.io.File;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
public class ProtectedService {
private static final List<String> USERS = Arrays.asList("root","guest","gandhi","bose");
private static final List<String> ROLES = Arrays.asList("root","guest","role1","role2");
@RequiresPermissions("user-roles:read")
public List<String> getUsers() {
return USERS;
}
@RequiresPermissions("user-roles:read")
public List<String> getRoles() {
return ROLES;
}
@RequiresPermissions("system:read:time")
public Date getSystemTime() {
return Calendar.getInstance().getTime();
}
@RequiresPermissions("calculator:add")
public int sum(int a, int b) {
return a+b;
}
@RequiresPermissions("calculator:subtract")
public int diff(int a, int b) {
return a-b;
}
@RequiresPermissions("filesystem:read:home")
public List<String> getHomeFiles() {
File homeDir = new File(System.getProperty("user.home"));
return Arrays.asList(homeDir.list());
}
public String getGreetingMessage(String name) {
return String.format("Hello %s",name);
}
}
使用了@RequiresPermissions()注解来表示每一个方法的需要的permission,没有该注解的getGreetingMessage(String name)方法不要求任何权限。
4.定义两个Controller,分别是登陆/登出页面的controller和成功登陆以后完成访问系统资源功能的controller。
package com.hp.shiro.simplerbac.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class LoginController {
@RequestMapping(value="login", method=RequestMethod.GET)
public String login(HttpServletRequest req){
if (SecurityUtils.getSubject().isAuthenticated()) {
return "redirect:/home";
} else {
return "login";
}
}
@RequestMapping(value="login", method=RequestMethod.POST)
public String login(HttpServletRequest req,RedirectAttributes redirectAttributes,Model model) {
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println("access to login");
System.out.println(username+","+password);
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
String errorMessage = null;
try {
SecurityUtils.getSubject().login(token);
} catch (AuthenticationException e) {
errorMessage = "user name doesn't exist or wrong password";
}
if(null == errorMessage) {
redirectAttributes.addAttribute("username", username);
return "redirect:/home";
} else {
System.out.println(errorMessage);
req.setAttribute("errorMessage",errorMessage);
return "login";
}
}
@RequestMapping(value="logout")
public String logout(HttpServletRequest req){
SecurityUtils.getSubject().logout();
return "redirect:/login";
}
}
package com.hp.shiro.simplerbac.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.hp.shiro.simplerbac.bean.ProtectedService;
@Controller
public class HomeController {
@RequestMapping(value="home")
public String home(HttpServletRequest req, HttpServletResponse response, Model model){
// System.out.println("access to home controller.");
String username = (String)SecurityUtils.getSubject().getPrincipal();
// System.out.println("username:"+username);
model.addAttribute("username", username);
String method = req.getParameter("method");
// System.out.println("method:"+method);
/*
* method可能的值value包括:
* <input type="hidden" name="method" value="getUsers"/>
* <input type="hidden" name="method" value="getRoles"/>
* <input type="hidden" name="method" value="getSystemTime"/>
* <input type="hidden" name="method" value="sum"/>
* <input type="hidden" name="method" value="diff"/>
* <input type="hidden" name="method" value="getHomeFiles"/>
* <input type="hidden" name="method" value="getGreetingMessage"/>
*/
ProtectedService protectedService = new ProtectedService();
try {
if ("getUsers".equals(method)) {
model.addAttribute("users", protectedService.getUsers());
} else if ("getRoles".equals(method)) {
model.addAttribute("roles", protectedService.getRoles());
} else if ("getSystemTime".equals(method)) {
model.addAttribute("systemTime", protectedService.getSystemTime());
} else if ("sum".equals(method)) {
int a = Integer.parseInt(req.getParameter("a"));
int b = Integer.parseInt(req.getParameter("b"));
model.addAttribute("sum",protectedService.sum(a, b));
} else if ("diff".equals(method)) {
int a = Integer.parseInt(req.getParameter("a"));
int b = Integer.parseInt(req.getParameter("b"));
model.addAttribute("diff",protectedService.diff(a, b));
} else if ("getHomeFiles".equals(method)) {
model.addAttribute("homeFiles",protectedService.getHomeFiles());
} else if ("getGreetingMessage".equals(method)) {
String name = req.getParameter("name");
model.addAttribute("greetingMessage",protectedService.getGreetingMessage(name));
}
} catch(Exception e) {
model.addAttribute("errorMessage", e.getMessage());
}
return "home";
}
}
5.定义jsp文件和css样式文件,具体代码参见工程源码HP-shiro-spring
总结:
该项目的主要目的是对spring的shiro的配置文件进行一个梳理,了解它俩结合的具体配置方式。
该项目将用户名和密码简单的存放在文本文件中,而且是明文存储,以后需要迁移到数据库加密存储的形式。
参考开涛的博客进一步对shiro的功能进行探索。例如加密解密模块和session管理模块。
2016/06/22日更新:使用shiro+springmvc+mybatis实现的小例子,页面没有变化,添加了数据库的支持。
项目地址:https://github.com/lunabird/shiro-demo.git