整合营销服务商

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

免费咨询热线:

极简入门,Shiro的认证与授权流程解析

下来的几天,我们开讲Shiro,从入门到分析、集成、单点登录整合等几篇。今天我们先来认识一下Shiro吧~


其实Shiro框架并不难,我梳理了一下,你只需要学会以下内容基本就足够了:

  • 登陆、授权流程
  • shiro过滤器链
  • 整合Springboot、redis做共享会话
  • 结合xxl-sso实现单点登录

接下来我会分为几篇文章分别去介绍,这篇我们先来了解一下shiro的一些基础知识,以及登录授权逻辑。

Shiro简介

在Web系统中我们经常要涉及到权限问题,例如不同角色的人登录系统,他操作的功能、按钮、菜单是各不相同的,这就是所谓的权限。

而构建一个互联网应用,权限校验管理是很重要的安全措施,这其中主要包含:

  • 用户认证 - 用户身份识别,即登录
  • 用户授权 - 访问控制
  • 密码加密 - 加密敏感数据防止被偷窥
  • 会话管理 - 与用户相关的时间敏感的状态信息

Shiro对以上功能都进行了很好的支持,它可以非常容易的开发出足够好的应用。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。而且Shiro的API也是非常简单。

官方源码:https://github.com/apache/shiro

整体结构与重要组件


从上图可以看出,Security Manager是Shiro的核心管理器,认证授权会话缓存等都是在其内部完成,然后会委托给具体的组件来处理,比如认证过程委托给Authenticator,授权委托给Authorizer组件。所以,整理还是比较清晰,源代码也容易追踪。

我们来具体聊聊所有的组件:

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager:Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;负责所有Subject、且负责进行认证和授权、及会话、缓存的管理。

  • Authenticator:认证器,判断用户是否正常登陆
  • Authorizer:授权器,判断用户是否有权限操作资源

Realm:可以有1个或多个Realm,主要提供认证和授权的数据;

Session:Shiro提供一个权限的企业级Session解决方案,session的生命周期都在SessionManager中进行管理。

SessionManager:shiro的会话管理器;

SessionDAO:用于会话的CRUD,比如存储到ehcache或者redis中的会话增删改查;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

官方简单示例

官网例子:http://shiro.apache.org/tutorial.html

刚入门Shiro的同学,真的需要去看看这个官方例子,你可以更加深入了解Shiro的权限校验流程。我还是贴一下代码吧,一些同学比较懒:

  • shiro.ini

上面代码中, root=secret,admin表示,用户名root,密码secret,角色是admin; schwartz=lightsaber:*表示角色schwartz拥有权限lightsaber:*。你其实可以把这个文件看成一个Realm,其实就是shiro默认的IniRealm。

  • 测试类Tutorial
public class Tutorial {

private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);

public static void main(String[] args) {
log.info("My First Apache Shiro Application");

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

// get the currently executing user:
Subject currentUser = SecurityUtils.getSubject();

// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}

// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}

//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}

//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

//a (very powerful) Instance Level permission:
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!");
}

//all done - log out!
currentUser.logout();

System.exit(0);
}
}

从上面的实例中,我们可以总结一下常用的API:

常用API


其实稍微梳理一下,可以发现上面代码主要有两个步骤:

  • 认证:

判断权限



接下来,我们去探讨一下shiro的认证与授权流程,并从源码层去解析一下shiro各个组件之间的关系。

认证流程

上面图片中,根据序号,其实我们大概能猜出里shiro的认证流程:

  1. Subject进行login操作,参数是封装了用户信息的token
  2. Security Manager进行登录操作
  3. Security Manager委托给Authenticator进行认证逻辑处理
  4. 调用AuthenticationStrategy进行多Realm身份验证
  5. 调用对应Realm进行登录校验,认证成功则返回用户属性,失败则抛出对应异常

我们从login方法开始debug一下流程,用简要方式追踪shiro源码的认证逻辑:

ok,一条线下来,从login到委托给authenticator,再最后调用realm的doGetAuthenticationInfo方法。

所以,从源码上来看,如果要实现shiro的认证逻辑,至少要准备一个Realm组件、和初始化securityManager组件。

常见异常

  • DisabledAccountException(禁用的帐号)
  • LockedAccountException(锁定的帐号)
  • UnknownAccountException(错误的帐号)
  • ExcessiveAttemptsException(登录失败次数过多)
  • IncorrectCredentialsException (错误的凭证)
  • ExpiredCredentialsException(过期的凭证)

授权流程

从上图中,我们可以知道授权流程如下:

  • 调用Subject.isPermitted/hasRole接口
  • 委托给SecurityManager
  • 而SecurityManager接着会委托给Authorizer
  • Authorizer会判断Realm的角色/权限是否和传入的匹配
  • 匹配如isPermitted/hasRole会返回true,否则返回false表示授权失败

追踪一下源码如下:

所以shiro判断用户是否有权限首先会从realm中获取用户所拥有的权限角色信息,然后再匹配当前的角色或权限是否包含,从而判定用户是否有权限!

说到权限,很多人自然会想起权限系统,涉及到几个关键对象:

  • 主体(Subject)
  • 资源(Resource)
  • 权限(Permission)
  • 角色(Role)

通过这几个要素,可以设计出比较合理的权限系统。

Shiro常见3种授权判断方式:

  • 编码实现

注解实现

JSP Taglig实现,freemarker等类似

jsp页面引入shiro标签

在线会话管理

获取当前会话总人数

强制下线

结束语

ok,感觉是高度极简的一篇文章,主要把重要的组件和登录、授权几个流程搞清楚之后,其实shiro基本已经学会了,后面我们再学一下shiro的几个主要内置过滤器怎么使用,如何集成SpringBoot,基本就差不多了。

近新接手了一个老项目。之前没有任何的权限管理。现在想接入shiro把权限这块管理起来。

关于shiro网上的资料已经很多了,我就不重复了。

直接开搞。

1,引入依赖

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

这一个就行了

2,修改web.xml

因为是个老的SpringMVC项目,要改的xml配置还不少。先要在web.xml中配置shiro过滤器,接管所有请求。

<!-- 使用shiro认证 -->
 <!-- 配置Shiro的过虑器,如果与Spring集成,则必须要使用Shiro提供的过虑器代理 -->
 <filter>
 <filter-name>shiroFilter</filter-name>
 <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 <init-param>
 <!-- 指定SpringBean代理Bean的名称,如果没有指定则与过虑器名称保持一致 -->
 <param-name>targetBeanName</param-name>
 <param-value>shiroFilter</param-value>
 </init-param>
 <init-param>
 <!-- 配置是否启动过虑器的init/destory方法 -->
 <param-name>targetFilterLifecycle</param-name>
 <param-value>true</param-value>
 </init-param>
 </filter>
 <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>
 <!-- 配置如果没有权限,则跳转到的页面 -->
 <error-page>
 <exception-type>org.apache.shiro.authz.AuthorizationException</exception-type>
 <location>/login.htm</location>
 </error-page>
 <error-page>
 <exception-type>org.apache.shiro.authz.UnauthorizedException</exception-type>
 <location>/login.htm</location>
 </error-page>
 <!-- 使用shiro认证 结束 -->

3,配置shiro的bean

<!-- shiro配置 -->
 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
 <property name="securityManager" ref="securityManager"/>
 <property name="loginUrl" value="/login.htm"/><!--登录页面,-->
 <property name="successUrl" value="/index.htm"/><!--登录成功页面,如果自己设置了返回页面,则不跳转-->
 <property name="unauthorizedUrl" value="/error.htm"/>
 <property name="filterChainDefinitions">
 <value>
 /assets/**=anon
 /api/loginCheck=anon <!--表示都可以访问-->
 /api/register=anon
 /login=anon
 /**=authc <!--authc表示需要认证才能访问的页面-->
 </value>
 </property>
 </bean>
 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
 <property name="realm" ref="myshiroRealm"/>
 </bean>

注释的内容我不重复,注意filterChainDefinitions这个,有一些路径比如登录注册等等是需要不加权限的,注意配置一下。

另外看这个myshiroRealm。多说一句,我开始也不理解为啥叫realm,后来查了下,realm是有域的意思,安全域。

这个类是需要我们自己创建的。

是整个shiro中需要我们编写的核心代码。简单的其实也就是查询数据库,比对用户名密码对不对。

4,编写shiroRealm

@Component("myshiroRealm")
public class MyShiroRealm extends AuthorizingRealm {
 @Autowired
 @Qualifier("permissionService")
 private PermissionService permissionService;
 @Autowired
 @Qualifier("userService")
 private UserService userService;
 @Autowired
 @Qualifier("roleService")
 private RoleService roleService;
 /**
 * 权限检查
 */
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
 // 做权限检查
 。。。
 }
 /**
 * 登录认证举例
 */
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
 //通过表单接收的用户名
 String username = token.getUsername();
 char[] password = token.getPassword();
 User userByUsername = userService.getUserByUsername(username);
 if (null == userByUsername) {
 throw new AccountException("账户不存在");
 }
 User user = userService.getUserByPwd(username, String.valueOf(password));
 if (null == user) {
 throw new AccountException("帐号或密码不正确!");
 }
 return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
 }
}

以登录认证举例。

5,在登录流程中使用

 /**
 * 登录校验
 */
 @RequestMapping(value = "loginCheck", method = RequestMethod.POST)
 public RestfulResponse loginCheck(@RequestBody Map<String, String> params) {
 RestfulResponse restfulResponse = new RestfulResponse();
 restfulResponse.setSuccess(false);
 String username = params.get("username");
 String password = params.get("password");
 //登录后存放进shiro token
 UsernamePasswordToken token = new UsernamePasswordToken(username, password);
 Subject subject = SecurityUtils.getSubject();
 try {
 subject.login(token);
 restfulResponse.setSuccess(true);
 restfulResponse.setMsg("认证成功");
 } catch (AuthenticationException e) {
 e.printStackTrace();
 restfulResponse.setMsg(e.getMessage());
 }
 return restfulResponse;
 }

重点的代码就是subject.login(token),底层会调我们上面编写的realm进行校验

好了,到现在为止,对shiro在java后端代码中的使用基本流程就通了。

Subject subject = SecurityUtils.getSubject();这个特别好用,只要登录了,在以后的代码中,都可以从这一句获取登录信息。

6,velocity中使用shiro

我这个项目页面是velocity写的,我想在vm中也是用shiro标签也是可以的。做法:

 <bean id="viewResolver"
 class="org.springframework.web.servlet.view.velocity.VelocityLayoutViewResolver">
 <property name="cache" value="false"/>
 <property name="prefix" value="/templates/"/>
 <property name="layoutUrl" value="/layout/main.vm"/>
 <property name="suffix" value=".vm"/>
 <property name="exposeSpringMacroHelpers" value="true"/>
 <property name="contentType" value="text/html;charset=UTF-8"/>
 <property name="viewClass"
 value="org.springframework.web.servlet.view.velocity.VelocityLayoutView"/>
 <!--自定义工具-->
 <property name="toolboxConfigLocation" value="WEB-INF/views/velocity-tools.xml"/>
 </bean>

我找到之前的viewResolver,然后加了一个自定义工具velocity-tools

7,自定义工具

到WEB-INF下创建这个自定义工具

<?xml version="1.0" encoding="UTF-8"?>
<toolbox>
 <tool>
 <key>shiro</key>
 <scope>request</scope>
 <class>
 com.github.ltsopensource.admin.support.VelocitySupport
 </class>
 </tool>
</toolbox>

也很简单,就是指向我的一个工具类VelocitySupport

8,velocity工具类

来看看这个工具类的内容

实际上有很多方法,我这里那最简单的用户名举例

@DefaultKey("shiro")
@ValidScope(Scope.APPLICATION)
public class VelocitySupport {
 /**
 * 返回用户 Principal。
 *
 * @return 用户 Principal
 */
 public Object getPrincipal() {
 Subject subject = SecurityUtils.getSubject();
 return subject != null ? subject.getPrincipal() : null;
 }

整个类内容,参考:https://github.com/eduosi/shiro-velocity-support

9.在vm模板中使用

这样就好了

更多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、微服务、数据库、数据结构、架构等等。