整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

30 分钟学会如何使用 Shiro

质文章,及时送达

作者:冷豪

链接:www.cnblogs.com/learnhow/p/5694876.html

一、架构

要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用不依赖任何容器,它也可以在JavaSE下使用。但是最常用的环境还是JavaEE。下面以用户登录为例:

1、使用用户的登录信息创建令牌

UsernamePasswordToken token = new UsernamePasswordToken(username, password);

token可以理解为用户令牌,登录的过程被抽象为Shiro验证令牌是否具有合法身份以及相关权限。

2、执行登陆动作

SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager
Subject subject = SecurityUtils.getSubject; // 获取Subject单例对象
subject.login(token); // 登陆

Shiro的核心部分是SecurityManager,它负责安全认证与授权。Shiro本身已经实现了所有的细节,用户可以完全把它当做一个黑盒来使用。SecurityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。

Subject是初学者比较难于理解的对象,很多人以为它可以等同于User,其实不然。Subject中文翻译:项目,而正确的理解也恰恰如此。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(token)与项目(subject)的登陆(login)关系,Shiro保证了项目整体的安全。

我把历史发布过的实战文章整理成了 PDF ,关注微信公众号「Java后端」回复 666 下载。

3、判断用户

Shiro本身无法知道所持有令牌的用户是否合法,因为除了项目的设计人员恐怕谁都无法得知。因此Realm是整个框架中为数不多的必须由设计者自行实现的模块,当然Shiro提供了多种实现的途径,本文只介绍最常见也最重要的一种实现方式——数据库查询。

4、两条重要的英文

我在学习Shiro的过程中遇到的第一个障碍就是这两个对象的英文名称:AuthorizationInfo,AuthenticationInfo。不用怀疑自己的眼睛,它们确实长的很像,不但长的像,就连意思都十分近似。

在解释它们前首先必须要描述一下Shiro对于安全用户的界定:和大多数操作系统一样。用户具有角色和权限两种最基本的属性。例如,我的Windows登陆名称是learnhow,它的角色是administrator,而administrator具有所有系统权限。这样learnhow自然就拥有了所有系统权限。那么其他人需要登录我的电脑怎么办,我可以开放一个guest角色,任何无法提供正确用户名与密码的未知用户都可以通过guest来登录,而系统对于guest角色开放的权限极其有限。

同理,Shiro对用户的约束也采用了这样的方式。AuthenticationInfo代表了用户的角色信息集合,AuthorizationInfo代表了角色的权限信息集合。如此一来,当设计人员对项目中的某一个url路径设置了只允许某个角色或具有某种权限才可以访问的控制约束的时候,Shiro就可以通过以上两个对象来判断。说到这里,大家可能还比较困惑。先不要着急,继续往后看就自然会明白了。

二、实现Realm

如何实现Realm是本文的重头戏,也是比较费事的部分。这里大家会接触到几个新鲜的概念:缓存机制、散列算法、加密算法。由于本文不会专门介绍这些概念,所以这里仅仅抛砖引玉的谈几点,能帮助大家更好的理解Shiro即可。

1、缓存机制

Ehcache是很多Java项目中使用的缓存框架,Hibernate就是其中之一。它的本质就是将原本只能存储在内存中的数据通过算法保存到硬盘上,再根据需求依次取出。你可以把Ehcache理解为一个Map<String,Object>对象,通过put保存对象,再通过get取回对象。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache">
<diskStore path="java.io.tmpdir" />

<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>

以上是ehcache.xml文件的基础配置,timeToLiveSeconds为缓存的最大生存时间,timeToIdleSeconds为缓存的最大空闲时间,当eternal为false时ttl和tti才可以生效。更多配置的含义大家可以去网上查询。

2、散列算法与加密算法

md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密本质上都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。

3、用户注册

请注意,虽然我们一直在谈论用户登录的安全性问题,但是说到用户登录首先就是用户注册。如何保证用户注册的信息不丢失,不泄密也是项目设计的重点。

public classPasswordHelper{
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator;
private String algorithmName = "md5";
private final int hashIterations = 2;

publicvoidencryptPassword(User user) {
// User对象包含最基本的字段Username和Password
user.setSalt(randomNumberGenerator.nextBytes.toHex);
// 将用户的注册密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐
String newPassword = new SimpleHash(algorithmName, user.getPassword,
ByteSource.Util.bytes(user.getCredentialsSalt), hashIterations).toHex;
user.setPassword(newPassword);
}
}

如果你不清楚什么叫加盐可以忽略散列的过程,只要明白存储在数据库中的密码是根据户注册时填写的密码所产生的一个新字符串就可以了。经过散列后的密码替换用户注册时的密码,然后将User保存进数据库。剩下的工作就丢给UserService来处理。

那么这样就带来了一个新问题,既然散列算法是无法复原的,当用户登录的时候使用当初注册时的密码,我们又应该如何判断?答案就是需要对用户密码再次以相同的算法散列运算一次,再同数据库中保存的字符串比较。

4、匹配

CredentialsMatcher是一个接口,功能就是用来匹配用户登录使用的令牌和数据库中保存的用户信息是否匹配。当然它的功能不仅如此。本文要介绍的是这个接口的一个实现类:HashedCredentialsMatcher

public classRetryLimitHashedCredentialsMatcherextendsHashedCredentialsMatcher{
// 声明一个缓存接口,这个接口是Shiro缓存管理的一部分,它的具体实现可以通过外部容器注入
private Cache<String, AtomicInteger> passwordRetryCache;

publicRetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}

@Override
publicbooleandoCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal;
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == ) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
// 自定义一个验证过程:当用户连续输入密码错误5次以上禁止用户登录一段时间
if (retryCount.incrementAndGet > 5) {
throw new ExcessiveAttemptsException;
}
boolean match = super.doCredentialsMatch(token, info);
if (match) {
passwordRetryCache.remove(username);
}
return match;
}
}

可以看到,这个实现里设计人员仅仅是增加了一个不允许连续错误登录的判断。真正匹配的过程还是交给它的直接父类去完成。连续登录错误的判断依靠Ehcache缓存来实现。显然match返回true为匹配成功。

5、获取用户的角色和权限信息

说了这么多才到我们的重点Realm,如果你已经理解了Shiro对于用户匹配和注册加密的全过程,真正理解Realm的实现反而比较简单。我们还得回到上文提及的两个非常类似的对象AuthorizationInfo和AuthenticationInfo。因为Realm就是提供这两个对象的地方。

public class UserRealm extends AuthorizingRealm {
// 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据
private UserService userService = new UserServiceImpl;

/**
* 提供用户信息返回权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal;
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo;
// 根据用户名查询当前用户拥有的角色
Set<Role> roles = userService.findRoles(username);
Set<String> roleNames = new HashSet<String>;
for (Role role : roles) {
roleNames.add(role.getRole);
}
// 将角色名称提供给info
authorizationInfo.setRoles(roleNames);
// 根据用户名查询当前用户权限
Set<Permission> permissions = userService.findPermissions(username);
Set<String> permissionNames = new HashSet<String>;
for (Permission permission : permissions) {
permissionNames.add(permission.getPermission);
}
// 将权限名称提供给info
authorizationInfo.setStringPermissions(permissionNames);

return authorizationInfo;
}

/**
* 提供账户信息返回认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal;
User user = userService.findByUsername(username);
if (user == ) {
// 用户名不存在抛出异常
throw new UnknownAccountException;
}
if (user.getLocked == 0) {
// 用户被管理员锁定抛出异常
throw new LockedAccountException;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername,
user.getPassword, ByteSource.Util.bytes(user.getCredentialsSalt), getName);
return authenticationInfo;
}
}

根据Shiro的设计思路,用户与角色之前的关系为多对多,角色与权限之间的关系也是多对多。在数据库中需要因此建立5张表,分别是:

用户表(存储用户名,密码,盐等)

角色表(角色名称,相关描述等)

权限表(权限名称,相关描述等)

用户-角色对应中间表(以用户ID和角色ID作为联合主键)

角色-权限对应中间表(以角色ID和权限ID作为联合主键)

具体dao与service的实现本文不提供。总之结论就是,Shiro需要根据用户名和密码首先判断登录的用户是否合法,然后再对合法用户授权。而这个过程就是Realm的实现过程。

6、会话

用户的一次登录即为一次会话,Shiro也可以代替Tomcat等容器管理会话。目的是当用户停留在某个页面长时间无动作的时候,再次对任何链接的访问都会被重定向到登录页面要求重新输入用户名和密码而不需要程序员在Servlet中不停的判断Session中是否包含User对象。

启用Shiro会话管理的另一个用途是可以针对不同的模块采取不同的会话处理。以淘宝为例,用户注册淘宝以后可以选择记住用户名和密码。之后再次访问就无需登陆。但是如果你要访问支付宝或购物车等链接依然需要用户确认身份。当然,Shiro也可以创建使用容器提供的Session最为实现。

三、与SpringMVC集成

有了注册模块和Realm模块的支持,下面就是如何与SpringMVC集成开发。有过框架集成经验的同学一定知道,所谓的集成基本都是一堆xml文件的配置,Shiro也不例外。

1、配置前端过滤器

先说一个题外话,Filter是过滤器,interceptor是拦截器。前者基于回调函数实现,必须依靠容器支持。因为需要容器装配好整条FilterChain并逐个调用。后者基于代理实现,属于AOP的范畴。

如果希望在WEB环境中使用Shiro必须首先在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_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>Shiro_Project</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>SpringMVC</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>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- 将Shiro的配置文件交给Spring监听器初始化 -->
<param-value>classpath:spring.xml,classpath:spring-shiro-web.xml</param-value>
</context-param>
<context-param>
<param-name>log4jConfigLoaction</param-name>
<param-value>classpath:log4j.properties</param-value>
</context-param>
<!-- shiro配置 开始 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<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>
<!-- shiro配置 结束 -->
</web-app>

熟悉Spring配置的同学可以重点看有绿字注释的部分,这里是使Shiro生效的关键。由于项目通过Spring管理,因此所有的配置原则上都是交给Spring。DelegatingFilterProxy的功能是通知Spring将所有的Filter交给ShiroFilter管理。

接着在classpath路径下配置spring-shiro-web.xml文件

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
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.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

<!-- 缓存管理器 使用Ehcache实现 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean>

<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="utils.RetryLimitHashedCredentialsMatcher">
<constructor-arg ref="cacheManager" />
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="2" />
<property name="storedCredentialsHexEncoded" value="true" />
</bean>

<!-- Realm实现 -->
<bean id="userRealm" class="utils.UserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>

<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
</bean>

<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/" />
<property name="unauthorizedUrl" value="/" />
<property name="filterChainDefinitions">
<value>
/authc/admin = roles[admin]
/authc/** = authc
/** = anon
</value>
</property>
</bean>

<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>

需要注意filterChainDefinitions过滤器中对于路径的配置是有顺序的,当找到匹配的条目之后容器不会再继续寻找。因此带有通配符的路径要放在后面。三条配置的含义是:

/authc/admin需要用户有用admin权限

/authc/**用户必须登录才能访问

/**其他所有路径任何人都可以访问

说了这么多,大家一定关心在Spring中引入Shiro之后到底如何编写登录代码呢。

@Controller
public class LoginController {
@Autowired
private UserService userService;

@RequestMapping("login")
public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject;
try {
subject.login(token);
} catch (IncorrectCredentialsException ice) {
// 捕获密码错误异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "password error!");
return mv;
} catch (UnknownAccountException uae) {
// 捕获未知用户名异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "username error!");
return mv;
} catch (ExcessiveAttemptsException eae) {
// 捕获错误登录过多的异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "times error");
return mv;
}
User user = userService.findByUsername(username);
subject.getSession.setAttribute("user", user);
return new ModelAndView("success");
}
}

登录完成以后,当前用户信息被保存进Session。这个Session是通过Shiro管理的会话对象,要获取依然必须通过Shiro。传统的Session中不存在User对象。

@Controller
@RequestMapping("authc")
publicclassAuthcController{
// /authc/** = authc 任何通过表单登录的用户都可以访问
@RequestMapping("anyuser")
public ModelAndView anyuser {
Subject subject = SecurityUtils.getSubject;
User user = (User) subject.getSession.getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}

// /authc/admin = user[admin] 只有具备admin角色的用户才可以访问,否则请求将被重定向至登录界面
@RequestMapping("admin")
public ModelAndView admin {
Subject subject = SecurityUtils.getSubject;
User user = (User) subject.getSession.getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}
}

本篇内容大多总结自张开涛的《跟我学Shiro》原文地址:

http://jinnianshilongnian.iteye.com/blog/2018936

-END-

如果看到这里,说明你喜欢这篇文章,请 转发、点赞。同时标星(置顶)本公众号可以第一时间接受到博文推送。

1. 自己手撸一个 Spring MVC

最近整理一份面试资料《Java技术栈学习手册》,覆盖了Java技术、面试题精选、Spring全家桶、Nginx、SSM、微服务、数据库、数据结构、架构等等。

  1. 先集成Spring、SpringMVC和Shiro
    <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>4.3.18.RELEASE</version>
        </dependency>

        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>4.3.18.RELEASE</version>
        </dependency>

        <dependency>
          <groupId>org.apache.shiro</groupId>
          <artifactId>shiro-all</artifactId>
          <version>1.3.2</version>
        </dependency>

        <dependency>
          <groupId>net.sf.ehcache</groupId>
          <artifactId>ehcache-core</artifactId>
          <version>2.6.2</version>
        </dependency>

  </dependencies>
  1. 在web.xml文件中配置Shiro的过滤器
    <!--
        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>
  1. 创建Shiro的配置文件(ehcache-shiro.xml)
<ehcache updateCheck="false" name="shiroCache">

    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />
</ehcache>
  1. 在Spring的配置文件中对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"
       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"/>
    </bean>

    <!--
        2. 配置 CacheManager.
        2.1 需要加入 ehcache 的 jar 包及配置文件.
    -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
    </bean>

    </bean>

    <!-- =========================================================
         Shiro Spring-specific integration
         ========================================================= -->
    <!-- Post processor that automatically invokes init() and destroy() methods
         for Spring-configured Shiro objects so you don't have to
         1) specify an init-method and destroy-method attributes for every bean
            definition and
         2) even know which Shiro objects require these methods to be
            called. -->
    <!--
        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="/unauthorized.jsp"/>

        <!--
            配置哪些页面需要受保护.
            以及访问这些页面需要的权限.
            1). anon 可以被匿名访问
            2). authc 必须认证(即登录)后才可能访问的页面.
            3). logout 登出.
            4). roles 角色过滤器
        -->
        <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon

                # everything else requires authentication:
                /** = authc
            </value>
        </property>
    </bean>
</beans>
  1. 配置完成,启动项目即可

工作流程


在这里插入图片描述

Shiro通过在web.xml配置文件中配置的ShiroFilter来拦截所有请求,并通过配置filterChainDefinitions来指定哪些页面受保护以及它们的权限。

URL权限配置


[urls]部分的配置,其格式为:url=拦截器[参数];如果当前请求的url匹配[urls]部分的某个url模式(url模式使用Ant风格匹配),将会执行其配置的拦截器,其中:

  • anon:该拦截器表示匿名访问,即不需要登录便可访问
  • authc:该拦截器表示需要身份认证通过后才可以访问
  • logout:登出
  • roles:角色过滤器

例:

    <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon

                # everything else requires authentication:
                /** = authc
            </value>
        </property>

需要注意的是,url权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的url模式对应的拦截器链,如:

  • /bb/**=filter1
  • /bb/aa=filter2
  • /**=filter3

如果请求的url是/bb/aa,因为按照声明顺序进行匹配,那么将使用filter1进行拦截。

Shiro认证流程


  1. 获取当前的Subject —— SecurityUtils.getSubject()
  2. 校验当前用户是否已经被认证 —— 调用Subject的isAuthenticated()方法
  3. 若没有被认证,则把用户名和密码封装为UsernamePasswordToken对象
  4. 执行登录 —— 调动Subject的login(UsernamePasswordToken)方法
  5. 自定义Realm的方法,从数据库中获取对应的记录,返回给Shiro
  6. 自定义类继承org.apache.shiro.realm.AuthenticatingRealm
  7. 实现doGetAuthenticationInfo(AuthenticationToken)方法
  8. 由Shiro完成对用户名密码的比对

下面具体实现一下,首先创建login.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h4>Login Page</h4>

    <form action="shiroLogin" method="post">
        username:<input type="text" name="username"/>
        <br/>
        <br/>
        password:<input type="password" name="password"/>
        <br/>
        <br/>
        <input type="submit" value="Submit"/>
    </form>
</body>
</html>

然后编写控制器:

package com.wwj.shiro.handlers;

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;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class ShiroHandler {

    @RequestMapping("/shiroLogin")
    public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
        //获取当前的Subject
        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";
    }
}

编写自定义的Realm:

package com.wwj.shiro.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;

public class ShiroRealm extends AuthenticatingRealm {

    /**
     * @param authenticationToken   该参数实际上是控制器方法中封装用户名和密码后执行login()方法传递进去的参数token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //将参数转回UsernamePasswordToken
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //从UsernamePasswordToken中取出用户名
        String username = token.getUsername();
        //调用数据库方法,从数据表中查询username对应的记录
        System.out.println("从数据库中获取Username:" + username + "对应的用户信息");
        //若用户不存在,则可以抛出异常
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在!");
        }
        //根据用户信息的情况,决定是否需要抛出其它异常
        if("monster".equals(username)){
            throw new LockedAccountException("用户被锁定!");
        }
        /*  根据用户信息的情况,构建AuthenticationInfo对象并返回,通常使用的实现类是SimpleAuthenticationInfo
         *  以下信息是从数据库中获取的:
         *      principal:认证的实体信息,可以是username,也可以是数据表对应的用户实体类对象
         *      credentials:密码
         *      realmName:当前realm对象的name,调用父类的getName()方法即可
         */
        Object principal = username;
        Object credentials = "123456";
        String realmName = getName();
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,realmName);
        return info;
    }
}

记得在Spring配置文件中拦截表单请求:

    <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                <!-- 拦截表单请求 -->
                /shiroLogin = anon
                <!-- 登出 -->
                /logout = logout

                # everything else requires authentication:
                /** = authc
            </value>
        </property>

登录成功后跳转至list.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h4>List Page</h4>

    <a href="logout">logout</a>
</body>
</html>

这里实现了一个登出请求,是因为Shiro在登录成功后会有缓存,此时无论用户名是否有效,都将成功登录,所以这里进行一个登出操作。

编写完成,最后启动项目即可。

在这里插入图片描述

若没有进行登录,将无法访问其它页面,若输入错误的用户名,则无法成功登录,也无法访问其它页面:

在这里插入图片描述

若输入正确的用户名和密码,则登录成功,可以访问其它页面:

在这里插入图片描述

重新来回顾一下上述的认证流程:

  1. 首先在login.jsp页面中有一个表单用于登录,当用户输入用户名和密码点击登录后,请求会被ShiroHandler控制器拦截
  2. 在ShiroHandler中校验用户是否已经被认证,若未认证,则将用户名和密码封装成UsernamePasswordToken对象,并执行登录
  3. 当执行登录后,UsernamePasswordToken对象会被传入ShiroRealm类的doGetAuthenticationInfo()方法的入参中,在该方法中对数据作进一步的校验

密码校验的过程


在刚才的例子中,我们实现了在用户登录前后对页面权限的控制,事实上,在程序中我们并没有去编写密码比对的代码,而登录逻辑显然对密码进行了校验,可以猜想这一定是Shiro帮助我们完成了密码的校验。

我们在UserNamePasswordToken类中的getPassword()方法中打一个断点:

在这里插入图片描述

此时以debug的方式启动项目,在表单中输入用户名和密码,点击登录,程序就可以在该方法处暂停运行:

在这里插入图片描述

我们往前找在哪执行了密码校验的逻辑,发现在doCredentialsMatch()方法:

在这里插入图片描述

再观察右边的参数:

在这里插入图片描述

这不正是我在表单输入的密码和数据表中查询出来的密码吗?由此确认在此处Shiro帮助我们对密码进行了校验。

在往前找找可以发现:

在这里插入图片描述

Shiro实际上是用CredentialsMatcher对密码进行校验的,那么为什么要大费周章地来找CredentialsMatcher呢?

CredentialsMatcher是一个接口,我们来看看它的实现类:

在这里插入图片描述

那么相信大家已经知道接下来要做什么了,没错,密码的加密,而加密就是通过CredentialsMatcher来完成的。

MD5加密


加密算法其实有很多,这里以md5加密为例。

修改Spring配置文件中对自定义Realm的配置:

    <bean id="myRealm" class="com.wwj.shiro.realms.ShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"/>
                <!-- 指定加密次数 -->
                <property name="hashIterations" value="5"/>
            </bean>
        </property>
    </bean>

这里因为Md5CredentialsMatcher类已经过期了,Shiro推荐直接使用HashedCredentialsMatcher。

这样配置以后,从表单中输入的密码就能够自动地进行MD5加密,但是从数据表中获取的密码仍然是明文状态,所以还需要对该密码进行MD5加密:

    public static void main(String[] args) {
        String algorithmName = "MD5";
        Object credentials = "123456";
        Object salt = null;
        int hashIterations = 5;
        Object result = new SimpleHash(algorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }

该代码可以参考Shiro底层实现,我们以Shiro同样的方式对其进行MD5加密,两份密码都加密完成了,以debug运行项目,再次找到Shiro校验密码的地方:

在这里插入图片描述

我在表单输入的密码是123456,经过校验发现,两份密码的密文是一致的,所以登录成功。

考虑密码重复的情况


刚才对密码进行了加密,进一步解决了密码的安全问题,但又有一个新问题摆在我们面前,倘若有两个用户的密码是一样的,这样即使进行了加密,因为密文是一样的,这样仍然会有安全问题,那么能不能够实现即使密码一样,但生成的密文却可以不一样呢?

当然是可以的,这里需要借助一个credentialsSalt属性(这里我们假设以用户名为标识进行密文的重新加密):

    public static void main(String[] args) {
        String algorithmName = "MD5";
        Object credentials = "123456";
        Object salt = ByteSource.Util.bytes("aaa");
        //Object salt = ByteSource.Util.bytes("bbb");
        int hashIterations = 5;
        Object result = new SimpleHash(algorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }

通过该方式,我们生成了两个不一样的密文,即使密码一样:

c8b8a6de6e890dea8001712c9e149496
3d12ecfbb349ddbe824730eb5e45deca

既然这里对加密进行了修改,那么在表单密码进行加密的时候我们也要进行修改:

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //将参数转回UsernamePasswordToken
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //从UsernamePasswordToken中取出用户名
        String username = token.getUsername();
        //调用数据库方法,从数据表中查询username对应的记录
        System.out.println("从数据库中获取Username:" + username + "对应的用户信息");
        //若用户不存在,则可以抛出异常
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在!");
        }
        //根据用户信息的情况,决定是否需要抛出其它异常
        if("monster".equals(username)){
            throw new LockedAccountException("用户被锁定!");
        }
        /*  根据用户信息的情况,构建AuthenticationInfo对象并返回,通常使用的实现类是SimpleAuthenticationInfo
         *  以下信息是从数据库中获取的:
         *      principal:认证的实体信息,可以是username,也可以是数据表对应的用户实体类对象
         *      credentials:密码
         *      realmName:当前realm对象的name,调用父类的getName()方法即可
         */
        Object principal = username;
        Object credentials = null;
        //对用户名进行判断
        if("aaa".equals(username)){
            credentials = "c8b8a6de6e890dea8001712c9e149496";
        }else if("bbb".equals(username)){
            credentials = "3d12ecfbb349ddbe824730eb5e45deca";
        }
        String realmName = getName();
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);
        return info;
    }

这样就轻松解决了密码重复的安全问题了。

多Relam的配置


刚才实现的是单个Relam的情况,下面来看看多个Relam之间的配置。

首先自定义第二个Relam:

package com.wwj.shiro.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;

public class ShiroRealm2 extends AuthenticatingRealm {

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        System.out.println("ShiroRealm2...");

        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        System.out.println("从数据库中获取Username:" + username + "对应的用户信息");
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在!");
        }
        if("monster".equals(username)){
            throw new LockedAccountException("用户被锁定!");
        }
        Object principal = username;
        Object credentials = null;
        if("aaa".equals(username)){
            credentials = "ba89744a3717743bef169b120c052364621e6135";
        }else if("bbb".equals(username)){
            credentials = "29aa55fcb266eac35a6b9c1bd5eb30e41d4bfd8d";
        }
        String realmName = getName();
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);
        return info;
    }

    public static void main(String[] args) {
        String algorithmName = "SHA1";
        Object credentials = "123456";
        Object salt = ByteSource.Util.bytes("bbb");
        int hashIterations = 5;
        Object result = new SimpleHash(algorithmName, credentials, salt, hashIterations);
        System.out.println(result);
    }
}

这里简单复制了第一个Relam的代码,并将加密方式改为了SHA1。

接下来修改Spring的配置文件:

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

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <!-- 添加此处配置 -->
        <property name="authenticator" ref="authenticator"/>
    </bean>

    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
    </bean>

    <!-- 添加此处配置 -->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="realms">
            <list>
                <ref bean="myRealm"/>
                <ref bean="myRealm2"/>
            </list>
        </property>
    </bean>

    <bean id="myRealm" class="com.wwj.shiro.realms.ShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"/>
                <property name="hashIterations" value="5"/>
            </bean>
        </property>
    </bean>

    <!-- 添加此处配置 -->
    <bean id="myRealm2" class="com.wwj.shiro.realms.ShiroRealm2">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="SHA1"/>
                <property name="hashIterations" value="5"/>
            </bean>
        </property>
    </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>

    <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
                /shiroLogin = anon
                /logout = logout

                # everything else requires authentication:
                /** = authc
            </value>
        </property>
    </bean>
</beans>

注释的地方就是需要修改的地方。

此时我们启动项目进行登录,查看控制台信息:

在这里插入图片描述

可以看到两个Relam都被调用了。

认证策略


既然有多个Relam,那么就一定会有认证策略的区别,比如多个Relam中是一个认证成功即为成功还是要所有Relam都认证成功才算成功,Shiro对此提供了三种策略:

  • FirstSuccessfulStrategy:只要有一个Relam认证成功即可,只返回第一个Relam身份认证成功的认证信息,其它的忽略
  • AtLeastOneSuccessfulStrategy:只要有一个Relam认证成功即可,和FirstSuccessfulStrategy不同,它将返回所有Relam身份认证成功的认证信息
  • AllSuccessfulStrategy:所有Relam认证成功才算成功,且返回所有Relam身份认证成功的认证信息

默认使用的策略是AtLeastOneSuccessfulStrategy,具体可以通过查看源码来体会。

若要修改默认的认证策略,可以修改Spring的配置文件:

<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    <property name="realms">
        <list>
            <ref bean="myRealm"/>
            <ref bean="myRealm2"/>
        </list>
    </property>
    <!-- 修改认证策略 -->
    <property name="authenticationStrategy">
        <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
    </property>
</bean>

授权


授权也叫访问控制,即在应用中控制谁访问哪些资源,在授权中需要了解以下几个关键对象:

  • 主体:访问应用的用户
  • 资源:在应用中用户可以访问的url
  • 权限:安全策略中的原子授权单位
  • 角色:权限的集合

下面实现一个案例来感受一下授权的作用,新建aaa.jsp和bbb.jsp文件,并修改list.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h4>List Page</h4>

    <a href="aaa.jsp">aaa Page</a>
    <br/>
    <br/>
    <a href="bbb.jsp">bbb Page</a>
    <br/>
    <br/>
    <a href="logout">logout</a>
</body>
</html>

现在的情况是登录成功之后就能够访问aaa和bbb页面了:

在这里插入图片描述

但是我想实现这样一个效果,只有具备当前用户的权限才能够访问到指定页面,比如我以aaa用户的身份登录,那么我将只能访问aaa.jsp而无法访问bbb.jsp;同样地,若以bbb用户的身份登录,则只能访问bbb.jsp而无法访问aaa.jsp,该如何实现呢?

实现其实非常简单,修改Sping的配置文件:

    <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                /shiroLogin = anon
                /logout = logout

                <!-- 添加角色过滤器 -->
                /aaa.jsp = roles[aaa]
                /bbb.jsp = roles[bbb]

                # everything else requires authentication:
                /** = authc
            </value>
        </property>

启动项目看看效果:

在这里插入图片描述

这里有一个坑,就是在编写授权之前,你需要将Relam的引用放到securityManager中:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="cacheManager" ref="cacheManager"/>
    <property name="authenticator" ref="authenticator"/>
    <property name="realms">
        <list>
            <ref bean="myRealm"/>
            <ref bean="myRealm2"/>
        </list>
    </property>
</bean>

否则程序将无法正常运行。

现在虽然把权限加上了,但无论你是aaa用户还是bbb用户,你都无法访问到页面了,Shiro都自动跳转到了无权限页面,我们还需要做一些操作,对ShiroRelam类进行修改:

package com.wwj.shiro.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.util.HashSet;
import java.util.Set;

public class ShiroRealm extends AuthorizingRealm {

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        System.out.println("从数据库中获取Username:" + username + "对应的用户信息");
        if("unknow".equals(username)){
            throw new UnknownAccountException("用户不存在!");
        }
        if("monster".equals(username)){
            throw new LockedAccountException("用户被锁定!");
        }
        Object principal = username;
        Object credentials = null;
        if("aaa".equals(username)){
            credentials = "c8b8a6de6e890dea8001712c9e149496";
        }else if("bbb".equals(username)){
            credentials = "3d12ecfbb349ddbe824730eb5e45deca";
        }
        String realmName = getName();
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);
        return info;
    }

    /**
     * 授权时会被Shiro回调的方法
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取登录用户的信息
        Object principal = principalCollection.getPrimaryPrincipal();
        //获取当前用户的角色
        Set<String> roles = new HashSet<>();
        roles.add("aaa");
        if("bbb".equals(principal)){
            roles.add("bbb");
        }
        //创建SimpleAuthorizationInfo,并设置其roles属性
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        return info;
    }
}

首先将继承的类做了修改,改为继承AuthorizingRealm类,可以通过实现该类的doGetAuthenticationInfo()方法完成认证,通过doGetAuthorizationInfo()方法完成授权,所以源代码不用动,直接添加下面的doGetAuthorizationInfo()方法即可,看运行效果:

在这里插入图片描述

可以看到aaa用户只能访问到aaa.jsp而无法访问bbb.jsp,但是bbb用户却能够访问到两个页面,如果你仔细观察刚才添加的方法你就能够明白为什么。

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取登录用户的信息
        Object principal = principalCollection.getPrimaryPrincipal();
        //获取当前用户的角色
        Set<String> roles = new HashSet<>();
        roles.add("aaa");
        if("bbb".equals(principal)){
            roles.add("bbb");
        }
        //创建SimpleAuthorizationInfo,并设置其roles属性
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        return info;
    }

因为不管是什么用户登录,我都将aaa用户添加到了roles中,所以bbb用户是具有aaa用户权限的,权限完全是由你自己控制的,想怎么控制你就怎么写。

注解实现授权


先来看看关于授权的几个注解:

  • @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证;即 Subject. isAuthenticated()返回 true
  • @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的
  • @RequiresGuest:表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
  • @RequiresRoles(value={“aaa”, “bbb”}, logical=Logical.AND):表示当前 Subject 需要角色aaa和bbb
  • @RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR):表示当前 Subject 需要权限user:a 或user:b

把Spring配置文件中的角色过滤器删掉,然后定义一个Service:

package com.wwj.shiro.service;

import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Service;

@Service
public class ShiroService {

    @RequiresRoles({"aaa"})
    public void test(){
        System.out.println("test...");
    }
}

在test()方法上添加注解@RequiresRoles({"aaa"}),意思是该方法只有aaa用户才能访问,接下来在ShiroHandler中添加一个方法:

    @Autowired
    private ShiroService shiroService;

    @RequestMapping("/testAnnotation")
    public String testAnnotation(){
        shiroService.test();
        return "redirect:/list.jsp";
    }

此时当你访问testAnnotation请求时,只有aaa用户能够成功访问,bbb用户就会抛出异常。

pache Shiro 10分钟入门教程

——翻译 崔传新

1.简介

欢迎来到Apache Shiro的10分钟教程!

通过阅读这个快速简单的教程,您将充分了解开发人员如何在其应用程序中使用Shiro。 而且你应该可以在10分钟内做到这一点。

2.概览

什么是Apache Shiro?

Apache Shiro是一个功能强大且易于使用的Java安全框架,为开发人员提供了一个直观而全面的解决方案,用于身份验证、授权、加密和会话管理。

实际上,它实现了管理应用程序安全性的所有方面,同时尽可能避免出现问题。 它建立在完善的接口驱动设计和面向对象的原则之上,可以在任何你想象得到的地方实现自定义行为。 但是,对于所有事情来说,默认情况下都是合理的,这与应用程序安全性是一样的。 至少这是我们所追求的。

Apache Shiro能做什么?

很多 。 但我们不想扩张"快速入门"的内容。 如果您想了解它可以为您做什么,请查看我们的功能页面。 此外,如果您对我们如何开始以及为什么存在感到好奇,请参阅Shiro History and Mission页面。

ok,现在我们来做一些事情吧!

说明:Shiro可以在任何环境下运行,从最简单的命令行应用程序到最大的企业Web和集群应用程序,但是我们将在这个快速入门(QuickStart)中使用一个简单的"main"方法来完成一个最简单的例子,以便可以获得对API的应用体验。

3.下载

1)确保您安装了JDK1.6+和Maven 3.0.3+。

2)从下载页面下载最新的Shiro"源代码分发"包。 在这个例子中,我们使用1.3.2发行版本。

3)解压源代码包

$ unzip shiro-root-1.3.2-source-release.zip

4)进入quickstart目录

$ cd shiro-root-1.3.2/samples/quickstart

5)运行QuickStart

$ mvn compile exec:java

这个目标只会打印出一些日志消息,让你知道发生了什么,然后退出——(主要Maven构建项目的过程,包括下载一些有关的组件,下载后,下次运行就不再次下载了)。 在阅读本快速入门指南时,请随时查看samples / quickstart / src / main / java / Quickstart.java下的代码。 根据需要随时更改该文件并运行上述mvn compile exec:java命令。

备注说明:

我这里在windows8平台下运行体验的,环境为JDK1.8+maven3.5;步骤同上。

为了知道运行结果,我在源代码推出前位置加了一行代码,以观测效果:

log.info("\n====This is Quikstart Exampel.It is done!====");//增加代码行

结果示意图:

4.Quickstart.java

上面引用的Quickstart.java文件包含了所有可以帮助您熟悉API的代码。 现在让我们把它分成大块,这样你就可以很容易地理解发生了什么。

几乎在所有环境中,您都可以通过以下调用获取当前正在执行的用户:

Subject currentUser = SecurityUtils.getSubject();

使用SecurityUtils.getSubject(),我们可以获得当前正在执行的Subject。 主体只是应用程序用户的特定安全"视图"。 我们实际上想称它为'User(用户)',因为这"有道理",但我们决定不这么干:太多的应用程序都有现有的API,它们已经拥有自己的User类/框架,我们不想与这些API冲突。 另外,在安全领域,术语Subject实际上是公认的命名法。 ok,继续...

独立应用程序中的getSubject()调用,可能会根据特定于应用程序的位置中的用户数据以及服务器环境(例如Web应用程序)返回相应Subject,并根据与当前线程或传入请求关联的用户数据而获取Subject 。

现在你有一个主题,你可以用它做什么?

如果您想在应用程序的当前会话期间向用户提供可用的内容,则可以获得他们的会话:

Session session = currentUser.getSession(); session.setAttribute( "someKey", "aValue" );

Session是一个Shiro特定的实例,它给你提供了大多数习惯的常规HttpSession实例,但有一些额外的好处和一个很大的区别:它不需要HTTP环境!

如果在Web应用程序内部署,默认情况下会话将基于HttpSession。 但是,在非Web环境中,就像这个简单的快速入门一样,Shiro默认会自动使用它的企业会话管理。 这意味着无论部署环境如何,您都可以在应用程序中的任何层中使用相同的API。 这将打开一个全新的应用程序世界,因为任何需要会话的应用程序都不需要强制使用HttpSession或EJB Stateful Session Beans。 而且,任何客户端技术现在都可以共享会话数据。

所以现在你可以获得一个Subject和他们的Session。 那些真正有用的东西比如检查是否允许他们做事情,比如检查角色和权限?

那么,我们只能对已知的用户进行这些检查。 上面的Subject实例代表当前用户,但谁是当前用户? 其实,他们是匿名的 - 也就是说,直到他们登录至少一次。 所以,让我们这样做:

if ( !currentUser.isAuthenticated() ) {

//以gui特定方式收集用户主体和凭证-principals and credentials

//如html表单的用户名/密码,X509证书,OpenID等。

//我们将在这里使用用户名/密码示例,因为它是最常见的。

//(你知道这是什么电影吗?;)

UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");

//this is all you have to do to support 'remember me' (no config - built in!):

//这就是你需要做的所有事情以便来支持'记住我'(没有配置 - 内置!)

token.setRememberMe(true);

currentUser.login(token);

}

代码截图

就这样! 应用起来不可能更容易了。

但是,如果他们的登录尝试失败呢? 你可以捕捉各种具体的例外情况,告诉你到底发生了什么,并允许你相应地处理和做出反应:

try {

currentUser.login( token );

//如果没有例外,就是这样,搞定!

} catch ( UnknownAccountException uae ) {

//用户名不在系统中,如何向他们显示错误消息?

} catch ( IncorrectCredentialsException ice ) {

//密码不匹配,是否再试?

} catch ( LockedAccountException lae ) {

//该用户名的帐户被锁定 - 无法登录。如何显示一条消息?

}

... 更多类型的异常检查&mdash;&mdash;如果你想要 ...

} catch ( AuthenticationException ae ) {

//意外情况 - 怎么处理?

}

代码截图

您可以检查许多不同类型的例外情况,或者抛出Shiro可能无法解释的自定义异常情况。 有关更多信息,请参阅AuthenticationException JavaDoc。

ok,到现在为止,我们有一个登录用户。 我们还能做什么?

让我们看看他们是谁:

//打印他们的标识主体(在这种情况下是用户名)

log.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );

我们也可以测试它们是否具有特定的角色:

if ( currentUser.hasRole( "schwartz" ) ) {

log.info("May the Schwartz be with you!" );

} else {

log.info( "Hello, mere mortal." );

}

我们还可以看到他们是否有权对某种类型的实体采取行动:

if ( currentUser.isPermitted( "lightsaber:weild" ) ) {

log.info("You may use a lightsaber ring. Use it wisely.");

} else {

log.info("Sorry, lightsaber rings are for schwartz masters only.");

}

另外,我们可以执行非常强大的实例级权限检查 - 查看用户是否有权访问特定类型实例的功能:

if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {

log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'. " +

"Here are the keys - have fun!");

} else {

log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");

}

小菜一碟,对吧?

最后,当用户完成使用应用程序时,他们可以注销:

currentUser.logout(); //删除所有标识信息并使其会话无效。

那么,这就是在应用程序开发人员级别使用Apache Shiro的核心。 虽然有一些非常复杂的东西在引擎盖下进行,使得这项工作如此优雅,但这确实是它的全部。

但是你可能会问自己,"但是谁负责在登录时获取用户数据(用户名和密码,角色和权限等),以及谁在运行时真正执行这些安全检查?",这么问就对了&mdash;&mdash;你来做:通过实施 Shiro称之为Realm的东西,并将该Realm插入到Shiro的配置中来完成。

但是,如何配置Realm很大程度上取决于您的运行时环境。 例如,如果运行独立应用程序,或者如果您有基于Web的应用程序,或基于Spring或JEE容器的应用程序或其组合, 这种类型的配置不在本快速入门的范围之内,因为它的目的是让您对API和Shiro的概念感到满意。

当您准备好了解更多细节时,您一定要阅读认证指南和授权指南。 然后可以转到其他文档,特别是参考手册中,以回答任何其他问题。 您也可能想要加入用户邮件列表 - 您会发现我们有一个非常棒的社区,只要有可能,他们都愿意提供帮助。

感谢您的关注。 我们希望您喜欢使用Apache Shiro!

下次分享更高层级的实战应用案例。


Shiro架构