首发日期:2019-06-03


前言

在以往的权限管理中,我们的权限管理通常是有以下几个步骤:
1.创建用户,分配权限。
2.用户登录,权限拦截器拦截请求,识别当前用户登录信息
3.从权限表中判断是否拥有权限


从以上步骤中可以提取到以下三个问题。
三个问题:
> 1.如何让Shiro拦截请求。
> 在web开发中,Shiro会提供一个拦截器来对请求进行拦截。
>
> 2.Shiro如何判断发起请求用户的身份?
> 在web开发中,会借助session来判断,如果禁用了session,那么可能需要重写一些方法。
>
> 3.如何判断权限?
> Shiro使用realm来判断权限。


下面的也将以这三个问题为中心来描述Shiro。


Shiro的介绍

![](https://progor.oss-cn-shenzhen.aliyuncs.com/img/20190201005744.png)

  • Shiro是一个开源的java安全(权限)框架,它能够实现身份验证、授权、加密和会话管理等功能。
  • Shiro是apache旗下的产品,它的官网是: shiro官网: Apache Shiro
  • Shiro不仅可以用于javaEE环境,也可以用于javaSE

Shiro功能

  • Authentication:身份认证,验证用户是否拥有某个身份。
  • Authorization: 权限校验,验证某个已认证的用户是否拥有某个权限。确定“谁”可以访问“什么”。
  • Session Management:会话管理,管理用户登录后的会话,
  • Cryptography:加密,使用密码学加密数据,如加密密码。
  • Web Support:Web支持,能够比较轻易地整合到Web环境中。
  • Caching:缓存,对用户的数据进行缓存,
  • Concurrency:并发,Apache Shiro支持具有并发功能的多线程应用程序,也就是说支持在多线程应用中并发验证。
  • Testing:测试,提供了测试的支持。
  • Run as :允许用户以其他用户的身份来登录。
  • Remember me :记住我

补充

  • 同类的比较知名的安全框架还有spring security,Shiro的优点是比较简洁,功能虽然比不上Spring Security多样,但对于安全需求不多的时候可以使用Shiro。

HelloWorld

依赖包:

        <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 这里有用到日志打印,所以引入 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
</dependency>

对于不同的场景有不同依赖包【可以参考这个http://shiro.apache.org/download.html#latestSource】

基础包是shiro-core,这里仅作基础示例所以仅仅导入这个包。

示例代码

【一些代码可以参考https://github.com/apache/shiro/tree/master/samples/quickstart】

1.src/main/resources/shiro.ini的代码:

# -----------------------------------------------------------------------------
# users用来定义用户
[users]
# 用户名 = 密码,角色1,角色2...
admin = secret, admin
guest = guest, guest
aa = 123456, guest
# -----------------------------------------------------------------------------
# roles用来定义角色
[roles]
# 角色 = 权限 (* 代表所有权限)
admin = *
# 角色 = 权限 (* 代表所有权限)
guest = see
aa = see

2.src/main/com/demo/ShiroDemo的代码:

package com.demo;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class ShiroDemo {
private static final Logger log = LoggerFactory.getLogger(ShiroDemo.class);
public static void main(String[] args) {
//1.创建SecurityManagerFactory
IniSecurityManagerFactory factory =
new IniSecurityManagerFactory("classpath:shiro.ini");
//2.获取SecurityManager,绑定到SecurityUtils中
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3.获取一个用户识别信息
Subject currentUser = SecurityUtils.getSubject();
//4.判断是否已经身份验证
if (!currentUser.isAuthenticated()) {
// 4.1把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken("guest", "guest");
// 4.2设置rememberme
token.setRememberMe(true);
try {
// 4.3登录.
currentUser.login(token);
}
catch (UnknownAccountException uae) { //用户不存在异常
log.info("****---->用户名不存在: " + token.getPrincipal());
return;
}
catch (IncorrectCredentialsException ice) {// 密码不匹配异常
log.info("****---->" + token.getPrincipal() + " 的密码错误!");
return;
}
catch (LockedAccountException lae) {// 用户被锁定
log.info("****---->用户 " + token.getPrincipal() + " 已被锁定");
}
catch (AuthenticationException ae) { // 其他异常,认证异常的父类
log.info("****---->用户" + token.getPrincipal() + " 验证发生异常");
}
} // 5.权限测试:
//5.1判断用户是否有某个角色
if (currentUser.hasRole("guest")) {
log.info("****---->用户拥有角色guest!");
} else {
log.info("****---->用户没有拥有角色guest");
return;
}
//5.2判断用户是否执行某个操作的权限
if (currentUser.isPermitted("see")) {
log.info("****----> 用户拥有执行此功能的权限");
} else {
log.info("****---->用户没有拥有执行此功能的权限");
} //6.退出
System.out.println("****---->" + currentUser.isAuthenticated());
currentUser.logout();
System.out.println("****---->" + currentUser.isAuthenticated()); }
}

代码解析

解析一下上面的代码做了什么:

对于shiro.ini:

1.在[users]标签下以用户名 = 密码,角色1,角色2...的格式创建了用户

2.在[roles]标签下以角色 = 权限 (* 代表所有权限)的格式为用户分配了角色

对于ShiroDemo.java:

1.使用shiro.ini来获取了IniSecurityManagerFactory

2.通过IniSecurityManagerFactory获取SecurityManager,并绑定到SecurityUtils中

3.使用SecurityUtils获取一个用户识别信息Subject

4.对Subject对象判断是否已经身份验证(Authenticated)

5.将用户名和密码封装成UsernamePasswordToken对象,调用Subject对象的login方法来进行登录

6.登录成功后,调用Subject对象的hasRole方法来判断用户是否拥有某个角色

7.调用Subject对象的isPermitted方法来判断用户是否拥有某个行为

8.调用Subject对象的logout方法来退出。

补充

上面展示了一个”登录-权限验证-退出“的流程。但上面的一些代码还是硬编码的,比如说上面的用户名和密码还是从ini表中获取的,比如说上面的权限信息还是从ini中获取的,这些问题都是需要解决的,下面会进行解决。


一些概念

先来了解一些概念。

  • Application Code:代表着应用,应用使用Subject来标识自己的身份,以及使用Subject来进行认证和授权。

  • Subject:代表着“当前用户”。

  • Shiro SecurityManager:应用使用Subject来进行认证和授权,实际上执行认证和授权的是SecurityManager

  • Realm:SecurityManager的认证和授权需要使用Realm,Realm负责获取用户的权限和角色等信息,再返回给SecurityManager来进行判断。

  • 【下图是对这些概念的补充】

  • Authenticator:认证器,对用户身份进行认证的。

  • Authorizer:授权器,对用户进行授权的。用来判断用户是否拥有某个权限。

  • Realms:补充上面Realm的内容:Realm是可以有多个的。用户的认证和授权都需要Realm,也就是说Authenticator和Authorizer的实际操作还是交给了Realm

  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的

  • Cryptography :加密模块,提供对”密码“的加密等功能。


Realm

  • Realm是真正负责处理认证和授权的组件。
  • SecurityManager要完成认证,需要Realm返回一个AuthenticationInfo,AuthenticationInfo会携带存储起来的正确的用户认证信息,用来与用户提交的信息进行比对,如果信息不匹配,那么会认证失败。
  • SecurityManager要完成授权,需要Realm返回一个AuthorizationInfo

认证

下面的例子是以继承了AuthenticatingRealm的自定义Realm来实现自定义认证。

认证依赖于方法doGetAuthenticationInfo,需要返回一个AuthenticationInfo,通常返回一个他的子类SimpleAuthenticationInfo,构造方法的第一个参数是用户名,第二个是验证密码,第三个是当前realm的className。

package com.demo.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm; public class MyRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
System.out.println("MyRealm认证中---->用户:"+token.getPrincipal());
// 可以从token中获取用户名来从数据库中查询数据
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="123456";// 假设这是从数据库中查询到的用户密码
// 创建一个SimpleAuthenticationInfo,第一个参数是用户名,第二个是验证密码,第三个是当前realm的className
// 验证密码会与用户提交的密码进行比对
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,this.getName());
return info;
}
}

当创建了一个Realm之后,需要告诉SecurityManager,所以在shiro.ini中配置:

# -------------------------------------------------------------------
[main]
myRealm = com.demo.realms.MyRealm
# --------由于自定义认证,所以去除users,roles------------------------

这样子就可以进行自定义认证了,在上面的用户密码中都设置了"123456",所以如果输入的密码不正确都会认证失败。但上面没有设置授权,所以代码中要去掉授权的判断。

授权

下面的例子是以继承了AuthorizingRealm的自定义Realm来实现自定义认证和自定义授权。

授权依赖于方法doGetAuthorizationInfo,需要返回一个AuthorizationInfo,通常返回一个他的子类SimpleAuthorizationInfo。构造SimpleAuthorizationInfo可以空构造,也可以传入一个Set<String> roles来构造。

package com.demo.realms;

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 java.util.HashSet;
import java.util.Set; public class RealmForDouble extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 1. 获取授权的用户
Object principal = principals.getPrimaryPrincipal();
System.out.println("RealmForDouble授权中---->用户:"+principal);
//2.下面使用Set<String> roles来构造SimpleAuthorizationInfo
SimpleAuthorizationInfo info = null;
// SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<>();
if ("admin".equals(principal)){
roles.add("admin"); // 假设这个角色是从数据库中查出的
// 如果SimpleAuthorizationInfo实例化了,
// 可以这样来加角色,行为需要这样添加
// 角色可以传构造函数来实例化SimpleAuthorizationInfo
// info.addRole("admin");
// info.addStringPermission("*");
}
if ("guest".equals(principal)){
roles.add("guest");
}
info = new SimpleAuthorizationInfo(roles);
return info;
} // 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("RealmForDouble认证中---->用户:"+token.getPrincipal());
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="123456";// 假设这是从数据库中查询到的用户密码
// 创建一个SimpleAuthenticationInfo,第一个参数是用户名,第二个是验证密码,第三个是当前realm的className
// 验证密码会与用户提交的密码进行比对
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,this.getName());
return info;
}
}

ini文件中也需要对应修改:

# -------------------------------------------------------------------
[main]
myRealm = com.demo.realms.RealmForDouble
# --------由于自定义认证,所以去除users,roles------------------------

这样子就可以进行自定义认证和授权了,上面的认证信息和授权信息都是可以修改成从数据库中获取的。

几个父类:

AuthorizingRealm:可以同时认证和授权。

AuthenticatingRealm:用于认证。

Realm:既可以用于认证也可以用于授权。

几个方法

有好几个父类,怎么判断他们能否进行认证或授权呢?

  • 认证依赖于方法doGetAuthenticationInfo
  • 授权依赖于方法doGetAuthorizationInfo
  • supports(AuthenticationToken token)方法可以处理非UsernamePasswordToken的情况。

异常

当认证错误时会抛出异常,下面列举一下常见的异常。

  • UnknownAccountException:用户不存在的异常
  • ExcessiveAttemptsException:登录失败次数过多的异常
  • IncorrectCredentialsException:密码不匹配的异常
  • LockedAccountException:用户已被锁定的异常
  • ExpiredCredentialsException:用户登录凭证已过期的异常。
  • AuthenticationException:身份认证异常的基类。

集成于Spring MVC

依赖包

    <dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.4.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.2.4.RELEASE</version>
</dependency> <dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency> <dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
</dependency> <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.3</version>
</dependency> <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency> </dependencies>

步骤

1.先配置Shiro拦截器:

  <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>

2.匹配SpringMVC的DispatcherServlet:

  <servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

3.配置Spring的ContextLoaderListener:

  <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>

4.配置springmvc.xml【springmvc的配置文件】:

<?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.progor"></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>

5.在applicationContext.xml中配置Shiro:

<?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"> <!--1. 配置 SecurityManager!-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--缓存管理器-->
<property name="cacheManager" ref="cacheManager"/>
<!--realms-->
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
</bean> <!--2. 配置 CacheManager缓存管理器.-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!--缓存配置文件(这里暂不涉及,可以随便拷贝一个)-->
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean> <!--3. 配置 Realm-->
<bean id="jdbcRealm" class="com.progor.realms.MyRealm">
</bean> <!--4. 配置 LifecycleBeanPostProcessor,用来管理shiro一些bean的生命周期-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!--5. 启用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.-->
<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="/unauthorized.jsp"/>
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/** = authc
</value>
</property>
</bean>
</beans>

6.创建控制器来接受登录请求,执行Shiro认证:

package com.progor.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("/shiro")
public class UserController { @RequestMapping("/login")
public String login(String username,String password){
System.out.println(username+":"+password);
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
try {
currentUser.login(token);
}
catch (AuthenticationException ae) {
System.out.println("登录失败: " + ae.getMessage());
}
}
return "redirect:/admin.jsp";
}
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
System.out.println("退出成功");
return "redirect:/login.jsp";
}
}

7.创建realm:

package com.progor.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection; import java.util.HashSet;
import java.util.Set; public class MyRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Object principal = principals.getPrimaryPrincipal();
System.out.println("RealmForDouble授权中---->用户:"+principal);
SimpleAuthorizationInfo info = null;
Set<String> roles = new HashSet<>();
if ("admin".equals(principal)){
roles.add("admin");
}
if ("guest".equals(principal)){
roles.add("guest");
}
info = new SimpleAuthorizationInfo(roles);
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("RealmForDouble认证中---->用户:"+token.getPrincipal());
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="123456";// 假设这是从数据库中查询到的用户密码
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,this.getName());
return info;
}
}

8.创建几个jsp,用于权限测试:

一个用于登录的login.jsp,一个用于验证登录成功的admin.jsp。【预期结果是如果未登录,那么访问admin.jsp会重定向到login.jsp】

login.jsp:

	<form action="shiro/login" method="POST">
username: <input type="text" name="username"/>
<br><br>
password: <input type="password" name="password"/>
<br><br>
<input type="submit" value="Submit"/>
</form>

admin.jsp:

<body>
<p>这是admin.jsp</p>
<a href="shiro/logout">退出</a>
</body>

9.配置ehcache.xml,可以参考https://github.com/apache/shiro/blob/master/samples/spring-mvc/src/main/resources/ehcache.xml

代码解析

1.配置ShiroFilter是为了让ShiroFilter能够拦截请求来进行权限判断。

2.applicationContext中配置的Shiro请参考注释。

3.ehcache.xml是缓存管理器的配置文件。

补充

上述的代码简略地演示了在Spring环境中Shiro的运行流程。下面将会对一些细节进行描述。


ShiroFilter拦截

上面的ShiroFilter中有如下图的代码



这主要是用来定义ShiroFilter拦截哪些请求,以及怎么拦截请求的。

拦截器链

在上图中,左边是url,右边是拦截器。

常见的拦截器有:

  • anon:任何人都可以访问
  • authc:只有认证后才可以访问
  • logout:只有登录后才可以访问
  • roles[角色名]:只有拥有特定角色才能访问,例如/admin.jsp = roles[user]
  • perms["行为"]:只有拥有某种行为的才能访问,例如/admin/deluser = prems["user:delete"]
  • 想了解更多拦截器,可以参考shiro.apache.org/web.html#default-filters

url匹配

  • 在上图中,有用到/**,这是代表所有请求,是为了拦截其余未定义拦截规则的请求。
  • 其实这透露了url匹配是从上到下的,比如login.jsp由于前面定义了/login.jsp = anon,所以就不会交给/**来拦截了。
  • 另外,是可以有多个拦截器的,所以/admin/** = authc, roles[administrator]也是可以的。

url属性

上面的ShiroFilter还配置了下图的属性,这是用来定义发生一些情况时跳转到哪个页面的。

  • 比如配置了loginUrl,那么发起未认证的请求都会跳转到loginUrl
  • successUrl是用来定义登录成功后调整到哪个页面(如果controller跳转了视图那么这个失效)
  • unauthorizedUrl是用来定义访问不是自己权限的时跳转到哪个页面(普通的authc不会触发,roles会触发。)。

拦截器链的自定义

在上面都是使用硬编码的方式来定义拦截器链。下面将解决这个硬编码问题

一种方法是使用FilterChainResolver来处理,这里使用map的方式来处理。

定义一个类,核心方法是返回一个LinkedHashMap【有序是为了确保从上到下匹配】:

package com.progor.utils;
import java.util.LinkedHashMap; public class FilterChainMap {
// 使用静态工厂
public static LinkedHashMap<String, String> getFilterChainMap(){
LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 下面的数据可以从数据库中查询出来。
map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
map.put("/admin.jsp", "authc");
map.put("/**", "authc");
return map;
}
}

修改applicationContext.xml:

    <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="/unauthorized.jsp"/>
<property name="filterChainDefinitionMap" ref="filteChainMap"></property>
<!--去掉filterChainDefinitions-->
</bean>
<!--核心是获取这个map,由于使用了静态工厂,所以这样定义这个bean-->
<bean id="filteChainMap" class="com.progor.utils.FilterChainMap" factory-method="getFilterChainMap" ></bean>

补充:

上面讲述了ShiroFilter的配置,解决了请求的拦截问题。


密码加密

在上面的密码比对中,都是使用明文来比对。

而通常来说,被存储起来的用户密码通常都是加密后的。也就是说,在使用SimpleAuthenticationInfo返回的认证信息时候,里面的密码信息是被加密过的,如果我们直接拿用户提交的明文密码匹配的话就会匹配失败,所以我们应该还需要告诉Shiro使用什么加密方式来进行密码比较。

在Shiro中,使用credentialsMatcher来解决这个问题。

算法加密

在配置realm的时候,可以定义一个credentialsMatcher属性,例如:

    <bean id="jdbcRealm" class="com.progor.realms.MyRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--定义加密的算法-->
<property name="hashAlgorithmName" value="MD5"></property>
</bean>
</property>
</bean>

多重加密

密码加密一次后可以得到一串hash值,但还可以进行多次加密来提高安全性。

    <bean id="jdbcRealm" class="com.progor.realms.MyRealm">
<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>
</bean>

盐值加密

除了多重加密,还可以加入一个”盐值“来进行加密。一个人的名字可能是会重复的,但如果带上他的身份证的话,那么这个人就是特定唯一的。密码也是如此,直接将密码进行加密可能还是比较容易分析出来的(网上有一些md5的密码库,常见的加密结果很容易查找出来),但如果加入一个具有比较罕见的参数来进行加密的话,那么得到的结果就会难以解析了。

盐值由于不是每一个加密都是一样的,所以不能在realm中设置,需要在返回AuthenticationInfo时带上,这样securityManager就会对提交的明文密码依据加密算法、加密次数和盐值来进行加密后再与AuthenticationInfo中的密码进行比对。

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("RealmForDouble认证中---->用户:"+token.getPrincipal());
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="e10adc3949ba59abbe56e057f20f883e";// md5(123456)
String salt = "lilei";//假设这个盐值是从数据库中查出的
ByteSource credentialsSalt = ByteSource.Util.bytes(salt);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,credentialsSalt,this.getName());
return info;
}

多realm认证

  • realm是可以有多个的。
  • 多个realm代表可以使用不同来源的认证信息来进行用户认证。【适用于一些需要从多个来源查询认证信息的情况】

多个realm的配置

上面已经讲述过realm的定义方法了,所以这里主要讲怎么让Shiro知道这多个realm。

只需要把新的realm配置成bean,并告诉securityManager即可。

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
<!--省略其他配置 -->
<bean id="jdbcRealm" class="com.progor.realms.MyRealm">
<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>
</bean>
<bean id="secondRealm" class="com.progor.realms.SecondRealm"></bean>

对于上面的多个realm的认证,你可以尝试两个地方使用不同的密码来进行测试,借助sysout的话你会发现确实经过了两个realm.

认证策略

  • 对于多个realm的默认的认证策略是只要其中一个通过了即可认证通过,也就是说一个密码不匹配,一个密码匹配,最终的结果将会是认证通过。
  • 这个认证策略是可以修改的,需要对authenticator认证器进行配置。
    • FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
    • AtLeastOneSuccessfulStrategy:只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy不同,返回所有 Realm 身份验证成功的认证信息;
    • AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。

      可以在applicationContext.xml中配置authenticator来设置认证策略。
    <bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
</property>
</bean>

使用上述的代码后,需要所有的realm都验证成功才能认证成功。


授权

上面讲了权限拦截,下面讲一下怎么给请求/页面/业务来设置权限。

拦截链式

第一种是上面展示的使用拦截器链的方式,这种方式可以拦截一些请求/页面的非法权限操作。

编程式

编程式就是在代码中使用hasRole或isPermitted等方法来进行权限判断。

    @RequestMapping("/deluser")
public String deluser(){
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")){
//一系列操作....
System.out.println("执行了删除用户的操作");
return "redirect:/admin.jsp";
}else{
System.out.println("你没有权限执行");
return "redirect:/unauthorized.jsp";
}
}

除了hashRole,常见的方法还有:

  • hasRoles(List<String> roleIdentifiers):拥有List中的所有角色才返回true
  • hasAllRoles(Collection<String> roleIdentifiers):拥有集合中的所有角色才返回true
  • isPermitted(String... permissions):是否拥有某个行为(支持传入多个参数)

注解式

注解式就是使用注解来进行权限管理。【这些注解不能用在controller中】

public class UserService {
@RequiresRoles("admin") // 需要角色admin
public void deluser(){
System.out.println("执行了删除用户的操作");
}
}

除了·@RequiresRoles("admin"),常见的注解还有:

  • @RequiresPermissions():是否拥有某个权限

    注意,注解的使用需要以下两个bean:
<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>

补充

除此之外,还可以在jsp中进行授权,这将在后面再讲。


shiro标签

也可以在jsp中进行授权。



首先导入标签库:

<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

  • <shiro:guest></shiro:guest>:当用户没进行认证时,显示标签中的内容。
  • <shiro:user></shiro:user>:当用户进行认证了,显示标签中的内容。
  • <shiro:authenticated></shiro:authenticated>:当用户已经认证时,显示标签中的内容。
  • <shiro:notAuthenticated></shiro:notAuthenticated>:当用户未认证的时候显示标签中的内容(包括“remember me”的时候)
  • <shiro:principal />:用来获取用户凭证(用户名等)(从AuthenticationInfo中获取),标签所在的位置将被替换成凭证信息
  • <shiro:principal property="username" />:如果存入的用户凭证是一个对象,那么可以使用property指定获取这个对象中的属性。
  • <shiro:hasRole name="角色"></shiro:hasRole>:拥有指定角色,将显示标签中的内容。
  • <shiro:hasAnyRoles name="角色1,角色2..."></shiro:hasAnyRoles>:只要拥有多个角色中的一个就显示标签中的内容。
  • <shiro:lacksRole name="角色"></shiro:lacksRole>:没有某个角色将不显示标签中的内容
  • <shiro:hasPermission name="行为"></shiro:hasPermission>:如果拥有某个行为的权限,那么显示标签中的内容
  • <shiro:lacksPermission name="行为"></shiro:lacksPermission>:如果没有拥有某个行为,那么显示标签中内容

示例

<!-- 一个未登录的场景 -->
<shiro:guest>
Hi there! Please <a href="login.jsp">Login</a> or <a href="signup.jsp">Signup</a> today!
</shiro:guest> <!-- 已登录过,准备切换其他用户的场景 -->
<shiro:user>
Welcome back John! Not John? Click <a href="login.jsp">here<a> to login.
</shiro:user> <!-- 显示登录用户的用户名的场景 -->
Hello, <shiro:principal/>, how are you today? <!-- 用户已经认证通过的场景 -->
<shiro:authenticated>
<a href="/logout">退出</a>.
</shiro:authenticated> <!-- 拥有某个角色的场景 -->
<shiro:hasRole name="administrator">
<a href="createUser.jsp">创建用户</a>
</shiro:hasRole> <!-- 拥有某个行为的场景 -->
<shiro:hasPermission name="user:create">
<a href="createUser.jsp">创建用户</a>
</shiro:hasPermission>

remember me

remember me 主要用于再次访问时仍然保留认证状态的场景。例如,离开某个网站后,两三天再打开仍然保留你的登录信息。

setRememberMe

remember me的功能的一个前提是在认证时使用了setRememberMe :



为true才会“记住我”。

rememberme权限级别

记住我的权限并不是authc,而是user【用户已经身份验证/记住我】

所以做实验的要记得修改拦截器链。

参数设置

maxAge:过期时间

httpOnly:禁止使用js脚本读取到cookie信息

【其他的不太常用,有兴趣的自查。还有domain之类的】

一种配置方法:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/><!-- cookie的名称 -->
<property name="httpOnly" value="true"/>
<property name="maxAge" value="60"/><!-- 过期时间:60s -->
</bean> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie"/>
</bean>

第二种配置方法:

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
<property name="rememberMeManager.cookie.maxAge" value="15"/>
</bean>

补充

  • remember借助了cookie来实现记住登录状态,但这是不太安全的,因为(黑客)把cookie窃取了也能进行登录。

结语

这里仅仅只是“开了个门”,Shiro的世界还有很多广阔的地方。比如会话管理、单点登录【这些什么时候有空再写吧】

如果你想了解更多,可以参考Shiro官方参考手册http://shiro.apache.org/reference.html;

除此之外,张开涛的Shiro的PDF也是可以值得一看的。


Shiro的基本使用的更多相关文章

  1. shiro权限管理框架与springmvc整合

    shiro是apache下的一个项目,和spring security类似,用于用户权限的管理‘ 但从易用性和学习成本上考虑,shiro更具优势,同时shiro支持和很多接口集成 用户及权限管理是众多 ...

  2. springmvc 多数据源 SSM java redis shiro ehcache 头像裁剪

    获取下载地址   QQ 313596790  A 调用摄像头拍照,自定义裁剪编辑头像 B 集成代码生成器 [正反双向](单表.主表.明细表.树形表,开发利器)+快速构建表单;  技术:31359679 ...

  3. java springMVC SSM 操作日志 4级别联动 文件管理 头像编辑 shiro redis

    A 调用摄像头拍照,自定义裁剪编辑头像 B 集成代码生成器 [正反双向](单表.主表.明细表.树形表,开发利器)+快速构建表单;  技术:313596790freemaker模版技术 ,0个代码不用写 ...

  4. springmvc SSM shiro redis 后台框架 多数据源 代码生成器

    A集成代码生成器 [正反双向(单表.主表.明细表.树形表,开发利器)+快速构建表单 下载地址    ; freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本,处理类 ...

  5. springmvc SSM 多数据源 shiro redis 后台框架 整合

    A集成代码生成器 [正反双向(单表.主表.明细表.树形表,开发利器)+快速构建表单 下载地址    ; freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本,处理类 ...

  6. SpringMVC+Shiro权限管理【转】

    1.权限的简单描述 2.实例表结构及内容及POJO 3.Shiro-pom.xml 4.Shiro-web.xml 5.Shiro-MyShiro-权限认证,登录认证层 6.Shiro-applica ...

  7. shiro的使用2 灵活使用shiro的密码服务模块

    shiro最闪亮的四大特征是认证,授权,加密,会话管理. 上一篇已经演示了如何使用shiro的授权模块,有了shiro这个利器,可以以统一的编码方式对用户的登入,登出,认证进行管理,相当的优雅. 为了 ...

  8. shiro的使用1 简单的认证

    最近在重构,有空学了一个简单的安全框架shiro,资料比较少,在百度和google上能搜到的中文我看过了,剩下的时间有空会研究下官网的文章和查看下源码, 简单的分享一些学习过程: 1,简单的一些概念上 ...

  9. shiro实现session共享

    session共享:在多应用系统中,如果使用了负载均衡,用户的请求会被分发到不同的应用中,A应用中的session数据在B应用中是获取不到的,就会带来共享的问题. 假设:用户第一次访问,连接的A服务器 ...

  10. shiro在springmvc里面的集成使用【转】

    <dependency> <groupId>commons-collections</groupId> <artifactId>commons-coll ...

随机推荐

  1. 【转】SQL中GROUP BY语句与HAVING语句的使用

    一.GROUP BY GROUP BY语句用来与聚合函数(aggregate functions such as COUNT, SUM, AVG, MIN, or MAX.)联合使用来得到一个或多个列 ...

  2. java之获取变量的类型

    java要获取变量的类型必须自己定义一个函数: public class Test{ public static void main(String[] args) { short a = 1; a + ...

  3. 深入理解ES6之解构

    变量赋值的痛 对象 let o = {a:23,b:34}; let a = o.a; let b = o.b; 如上文代码,我们经常会遇到在各种场合需要获取对象中的值的场景,舒服一点的是获取单个属性 ...

  4. 【swoole】结合swoole 和 nsq 的实际应用

    集合 swoole 的框架设计 为了减少理解度,我尽量的从源头开始引入 1. nsq 案例中是使用 swoole 结合一个php 框架实现的是 NSQ 订阅功能. 启动命令: sudo bash /w ...

  5. IT兄弟连 Java语法教程 流程控制语句 经典案例

    使用continue忽略本次循环剩下的语句 continue的功能和break有点类似,区别是continue只是忽略本次循环剩下的语句,接着开始下一次循环,并不会终止循环:而break则是完全终止循 ...

  6. pytest执行用例时从conftest.py抛出ModuleNotFoundError:No module named 'XXX'异常的解决办法

    一.问题描述 在项目根目录下执行整个测试用例,直接从conftest.py模块中抛出了ModuleNotFoundError:No module named 'TestDatas'的异常: 二.解决方 ...

  7. php-laravel框架用户验证(Auth)模块解析(一)

    一.初始化 使用php artisan命令进行初始化:php artisan make:auth 和 php artisan migrate(该命令会生成users表.password_resets表 ...

  8. 给 K8s API “做减法”:阿里巴巴云原生应用管理的挑战和实践

    作者 | 孙健波(天元)  阿里巴巴技术专家本文整理自 11 月 21 日社群分享,每月 2 场高质量分享,点击加入社群. 早在 2011 年,阿里巴巴内部便开始了应用容器化,当时最开始是基于 LXC ...

  9. C++ getline函数用法详解

    转载自http://c.biancheng.net/view/1345.html 虽然可以使用 cin 和 >> 运算符来输入字符串,但它可能会导致一些需要注意的问题. 当 cin 读取数 ...

  10. Elasticsearch Query DSL 语言介绍

    目录 0. 引言 1. 组合查询 2. 全文搜索 2.1 Match 2.2 Match Phase 2.3 Multi Match 2.4 Query String 2.5 Simple Query ...