学习如何使用 Shiro 从架构谈起 到框架整合
一、架构1、使用使用者的登入资讯建立令牌2、执行登陆动作3、判断使用者4、两条重要的英文二、实现Realm1、快取机制2、杂凑算法与加密算法3、使用者注册4、匹配5、获取使用者的角色和许可权资讯6、会话三、与SpringMVC整合1、配置前端过滤器
一、架构
要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用不依赖任何容器,它也可以在JavaSE下使用。但是最常用的环境还是JavaEE。下面以使用者登入为例:
img
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保证了专案整体的安全。
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物件,通过put储存物件,再通过get取回物件。
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
以上是ehcache.xml档案的基础配置,timeToLiveSeconds为快取的最大生存时间,timeToIdleSeconds为快取的最大空闲时间,当eternal为false时ttl和tti才可以生效。更多配置的含义大家可以去网上查询。
2、杂凑算法与加密算法
md5是本文会使用的杂凑算法,加密算法本文不会涉及。杂凑和加密本质上都是将一个Object变成一串无意义的字串,不同点是经过杂凑的物件无法复原,是一个单向的过程。例如,对密码的加密通常就是使用杂凑算法,因此使用者如果忘记密码只能通过修改而无法获取原始密码。但是对于资讯的加密则是正规的加密算法,经过加密的资讯是可以通过秘钥解密和还原。
3、使用者注册
请注意,虽然我们一直在谈论使用者登入的安全性问题,但是说到使用者登入首先就是使用者注册。如何保证使用者注册的资讯不丢失,不泄密也是专案设计的重点。
public class PasswordHelper {
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
private String algorithmName = "md5";
private final int hashIterations = 2;
public void encryptPassword(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 class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
// 宣告一个快取界面,这个界面是Shiro快取管理的一部分,它的具体实现可以通过外部容器注入
private Cache passwordRetryCache;
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == null) {
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 roles = userService.findRoles(username);
Set roleNames = new HashSet();
for (Role role : roles) {
roleNames.add(role.getRole());
}
// 将角色名称提供给info
authorizationInfo.setRoles(roleNames);
// 根据使用者名称查询当前使用者许可权
Set permissions = userService.findPermissions(username);
Set permissionNames = new HashSet();
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 == null) {
// 使用者名称不存在丢掷异常
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档案中配置
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">
Shiro_Project
index.jsp
SpringMVC
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:springmvc.xml
1
true
SpringMVC
/
org.springframework.web.context.ContextLoaderListener
org.springframework.web.util.Log4jConfigListener
contextConfigLocation
classpath:spring.xml,classpath:spring-shiro-web.xml
log4jConfigLoaction
classpath:log4j.properties
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
true
targetFilterLifecycle
true
shiroFilter
/*
熟悉Spring配置的同学可以重点看有绿字注释的部分,这里是使Shiro生效的关键。由于专案通过Spring管理,因此所有的配置原则上都是交给Spring。DelegatingFilterProxy的功能是通知Spring将所有的Filter交给ShiroFilter管理。
接着在classpath路径下配置spring-shiro-web.xml档案
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">
/authc/admin = roles[admin]
/authc/** = authc
/** = anon
需要注意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")
public class AuthcController {
// /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");
}
}
来源:http://tinyurl.com/y4egg4pa
:-D 搜寻微讯号(ID:芋道源代码),可以获得各种 Java 源代码解析、原理讲解、面试题、学习指南。
:-D 并且,回复【书籍】后,可以领取笔者推荐的各种 Java 从入门到架构的 100 本书籍。
:-D 并且,回复【技术群】后,可以加入专门讨论 Java、后端、架构的技术群。
来吧,骚年~