在网络项目开发过程中经常要用到用户登录,还有权限管理,Shiro可以说是Spring的一把利器。
看懂这一篇博客需要两个要求
- 懂得SpirngMVC的基本配置和使用
- 懂得Shiro的基本配置和使用
先看一下项目结构
这个项目可以作为pull下来在作为一般项目的脚手架
POM
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jk.shiroLearning</groupId>
<artifactId>chapter5</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>chapter5 Maven Webapp</name>
<url>http://maven.apache.org</url>
<properties>
<springfox-version>2.3.0</springfox-version>
<spring-version>4.2.4.RELEASE</spring-version>
<servlet-api-version>3.1.0</servlet-api-version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!--Spring dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-api-version}</version>
<scope>provided</scope>
</dependency>
<!--shiro start-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>0.2.23</version>
</dependency>
<!--shiro end-->
</dependencies>
<build>
<finalName>chapter5</finalName>
</build>
</project>
首先要配置Web.xml
<?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_2_5.xsd"
id="WebApp_ID" version="2.5">
<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>
<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>
然后配置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.jk"></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>
然后配置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="authenticator" ref="authenticator"></property>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
</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>
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
</property>
</bean>
<!--
3. 配置 JdbcRealm
-->
<bean id="jdbcRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
<property name="dataSource">
<bean class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/shiro"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
</property>
<property name="permissionsLookupEnabled" value="true"></property>
</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"/>
<!--登录页面的url-->
<property name="loginUrl" value="/login.jsp"/>
<!--登录成功跳转到该url-->
<property name="successUrl" value="/list.jsp"/>
<!--没有角色或者身份跳转到该url-->
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!--配置自定义filterChainDefinitionMap,可用filterChainDefinitions代替-->
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property></property>
<!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
-->
<!--<property name="filterChainDefinitions">-->
<!--<value>-->
<!--/login.jsp = anon-->
<!--/shiro/login = anon-->
<!--/shiro/logout = logout-->
<!--/role1.jsp = roles[role1]-->
<!--/admin.jsp = roles[admin]-->
<!--# everything else requires authentication:-->
<!--/** = authc-->
<!--</value>-->
<!--</property>-->
</bean>
<!-- 配置一个 路由权限的 bean 替代上面的filterChainDefinitions, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="com.jk.factory.FilterChainDefinitionMapBuilder"></bean>
<bean id="shiroService"
class="com.jk.services.ShiroService"></bean>
</beans>
配置中重点讲解一下filterChainDefinitions,value对应的是url=权限或角色,具体如下
我们可以用一个filterChainDefinitionMap代替filterChainDefinitions
filterChainDefinitionMap
public class FilterChainDefinitionMapBuilder {
public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
map.put("/role1.jsp", "authc,roles[role1]");
map.put("/admin.jsp", "authc,roles[admin]");
map.put("/list.jsp", "user");
map.put("/**", "authc");
return map;
}
}
还有一点就是url的权限是先定义优先级越高,后定义的不会覆盖先定义的,可用/**匹配任何地址
然后再配置缓存
<ehcache>
<diskStore path="java.io.tmpdir"/>
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
</ehcache>
初始化数据库
drop database if exists shiro;
create database shiro;
use shiro;
create table users (
id bigint auto_increment,
username varchar(100),
password varchar(100),
password_salt varchar(100),
constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_users_username on users(username);
create table user_roles(
id bigint auto_increment,
username varchar(100),
role_name varchar(100),
constraint pk_user_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_user_roles on user_roles(username, role_name);
create table roles_permissions(
id bigint auto_increment,
role_name varchar(100),
permission varchar(100),
constraint pk_roles_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_roles_permissions on roles_permissions(role_name, permission);
insert into users(username, password, password_salt) values('jack', 'fc1709d0a95a6be30bc5926fdb7f22f4', 'jack');
insert into user_roles(username, role_name) values('jack', 'role1');
insert into user_roles(username, role_name) values('jack', 'role2');
insert into roles_permissions(role_name, permission) values('role1', 'user1:*');
insert into roles_permissions(role_name, permission) values('role1', 'user2:*');
insert into roles_permissions(role_name, permission) values('role2', 'user3:*');
再看Controller
@Controller
@RequestMapping("/shiro")
public class ShiroController {
@Autowired
private ShiroService shiroService;
@RequestMapping("/testShiroAnnotation")
public String testShiroAnnotation(HttpSession session){
session.setAttribute("key", "value12345");
try {
shiroService.testPermissionMethod();
shiroService.testRoleMethod();
}catch (UnauthorizedException e){
return "redirect:/unauthorized.jsp";
}
return "redirect:/list.jsp";
}
@RequestMapping("/login")
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);
// 记住登录
token.setRememberMe(true);
try {
// 执行登录.
currentUser.login(token);
}
// 所有认证时异常的父类.
catch (AuthenticationException ae) {
System.out.println("登录失败: " + ae.getMessage());
}
}
return "redirect:/list.jsp";
}
}
可以留意到除了登录以外还有一个测试注解的方法,注解是一种比较优雅的限制执行方法权限的方法,看一下如何来使用注解
注解
public class ShiroService {
//只需满足其中一种角色就好
@RequiresRoles({"role1","admin"})
public void testRoleMethod(){
System.out.println("testMethod, time: " + new Date());
Session session = SecurityUtils.getSubject().getSession();
Object val = session.getAttribute("key");
System.out.println("Service SessionVal: " + val);
}
//只需满足其中一种权限就好
@RequiresPermissions({"user1:*","user4:*"})
public void testPermissionMethod(){
System.out.println("testMethod, time: " + new Date());
Session session = SecurityUtils.getSubject().getSession();
Object val = session.getAttribute("key");
System.out.println("Service SessionVal: " + val);
}
}
Shiro还为我们提供了SecurityUtils.getSubject().getSession()的方法来获取Session,这样就不用传requset到方法里。
Shiro还有我们提供了标签
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!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=ISO-8859-1">
<title>Insert title here</title>
</head>
<body>
<h4>List Page</h4>
<shiro:guest>
欢迎游客访问
</shiro:guest>
<shiro:user>
已登录
</shiro:user>
<shiro:authenticated>
已通过认证
</shiro:authenticated>
<shiro:notAuthenticated>
未通过身份认证(包括记住我)
</shiro:notAuthenticated>
<shiro:hasRole name="admin">
拥有角色admin
</shiro:hasRole>
<shiro:hasAnyRoles name="admin,role1">
拥有角色admin或role1
</shiro:hasAnyRoles>
<shiro:lacksRole name="admin">
不拥有角色admin
</shiro:lacksRole>
Welcome: <shiro:principal></shiro:principal>
</body>
</html>
• guest 标签:用户没有身份验证时显示相应信息,即游客访问信息:
•user 标签:用户已经经过认证/记住我登录后显示相应的信息。
• authenticated 标签:用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的
• notAuthenticated 标签:用户未进行身份验证,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证。
• pincipal 标签:显示用户身份信息,默认调用Subject.getPrincipal() 获取,即 Primary Principal。
• hasRole 标签:如果当前 Subject 有角色将显示 body 体内容:Shiro 标签
• hasAnyRoles 标签:如果当前Subject有任意一个角色(或的关系)将显示body体内容。
Shiro对一些角色和复杂的项目简直就是福音,对spring的支持也是十分友好,配置起来十分的简单