30 分钟学会如何使用 Shiro

云栖号资讯:【点击查看更多行业资讯】
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!

一、架构

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

30 分钟学会如何使用 Shiro

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

UsernamePasswordToken token = new UsernamePasswordToken(username, password);

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

2、执行登陆动作

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

Shiro的核心部分是SecurityManager,它负X M 6 d 5 4 r责安全认证与授权。Shiro本身已经实现了所有的细节,用户可以完全把它当做一个黑盒来使用。Secur( K N S 5 { kityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。

Subject是初学者比较难于理解的对象,很多人以为它可以等同于User,其实不然。Subject中文翻译:项目,而正确的理解也恰恰如此。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(b $ i U Stokenq J M r & H :)与项目(subject)的登陆( V 8 Elogina m / 6 L J)关系,Shiro保证了项目整体的安全。
我整理了架构师系列视频教程 ,关注微信公众号「互联网架构师C V y s : [」回复 2T 下载。

3、判断用户

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

4、两条重要的英文

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

在解释它们前首先必须要描述一下ShiW h % 1 4 S G nro对于安全用户的界定:和大多数操作S 7 y .系统一样。用户具有角色和权限两种最基本的属性。例z } L q j } 8 ? )如,我的Windows登陆名称是learnhow,它的角色是administrator,而admE } ? M ] + ginistrator具有所有系统权限。这样learnhow自然就拥有了u k X0 ? / 8 i C l有系统权限。那么其他人需要登录我的电脑怎么办,我可以开放一个guest角色,任何无法提供正确用户名与密码的未知用户都可以通过guest来登录,而系统对于guW o ? _ n Y } IestB g # l S @角色开放的权限极其有限。

同理,Shiro对用户的约束也采用了这样的方式。AuthentM K V Z w MicationInfo代Y 4 ; * 6表了用户的角色信息集合,AuthorizatiF G 1 TonInfy D 6 ~ Ro代表了角色的权限信息集合。如此一来,当设计人员对项目中的某一个url路径设置了只允许某个角色或具有某种权限才可以访问的控制约束的时候,Shiro就可以通过以上] ] ~ | /两个对象来判断。说到这里,大家可能还比较困惑。先不要着急,继续往后看就自然会明白了。P w P V | 2 * F

二、实现Realm

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

1、缓存机制

Ehcace 5 P Q Y D l + vhe是很多~ r QJava项目中使用的缓存框架,Hibernate就是其中之一。它的本质就是将原本只能存储在内存中的数据通过算法保存到硬盘上,再根据需求依次取出。你可以把Ehcache理解为一个Ma% , ; p对象,通过put保存对象,再通过get取回对象。

<?xml version="1.0" encoding="UTF-8~ a @ ) a | E"?>
<ehcache name="shirocachec 3 #">
<diskStore pathB h p E K F a c="java.io.tmpdir" />
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"N y a |
timeToIdleSeconds="1800"
timeTom h . Q 2LiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>

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

2、散列算法与加密算法

md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密C @ k本质上都是将一个v W ! V i A 1 ~ pObject变成一串无意义的字符串,不同4 Z n d L 8 K 6点是经过散列的对象无法复* 6 ,原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。

3、用户注册

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

pQ : ~ |ublic class PasswordHelper {
private RandomNumberGenerator randomNumberGeneratn Q B zor = new Sec$ 8 n B G u RureRandomNumberGenerator();
private String algorit- ? uhmN& L 0 m i Came =T A 8 a + Y  7 9 "md5";
private final int hashIterations = 2;
ph 7 H X mublic void encryptPassword(User user) {
// User对象包含最基本的字段Username和Password
user.setSalt(ranj = b  O # s rdomNumberGenerator.nF N # X ~ extBytes().toHex());
// 将用户的注册密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐
Str{ j Ding newPassword = new SimpleHash(algO | 8orithmName, user.getPa^ & _ Dssword(),
ByteSource.Util.bytes(usT K 1er.getCre) N v TdentialsSalt()), hashIterations).toHex();
user.setPassword(ne^ Z & ? wwPassword);
}
}

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

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

4、匹配

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

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
// 声明一个缓存接口,这个接口是Shiro缓存管理的一部分,它的具体实现可以通过外部容器注入
private Cache<String, AtomicInteger> passwordRetryCache;
public RetryLimitHashedCredentialsMatcher(CacheML A k cana6  ] (ger cacheManager) {
passwordRetryCache = cacheManagv H q , !er.getCachJ C # ? w q re("passwordRetryCache");
}
@Ov| ] Terride
public bool( { X mean doCredentialsMatch(Authenticatio}   a NnToken token, AuthenticationInfo info) {
String username = (String) toke_ d n + N h h un.getPr& ( mincipal();
AtomicInteger retryCount = passwordRetryCach- q : v ? Ne.get(username);
if (retryCount ==y 4 ` Q null) {
ret= 0 e l 8 ^ @ wryCount = new Atomic{ K * A i M ~Integer(0);
passz , 8 Q , R K u (wordRetryCache.put(username, retryCount);
}
// 自定义一个R / C s验证过程:当用户连续输入密码错误5次以上禁止用户登录一段时间
if (retryCount.incrementAndGet() > 5) {
throw new Exce1 ^ / g qssiveAttemb Y ` n C F SptsException();
}
boolean match = super.doCredentialsMatch(token, info);a h | G  / (
i1 ~ i ; # pf (match) {
passwordRetryCache.remove(usernamf ? Me);
}
return match;
}
}

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

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

说了这么多才到我们的重点Realm,如果你已经理解了Shiro对于用户匹配和注册加密u ) H O O m M 6的全过程,真正理解* 2 {Realm的; l a @ f P #实现反而比较简单。我们还得回到_ T B上文提及的两个非常类似的对象AuthorizationInfo和AuthenticationInfo。因为Realm就是提供这两个对象的地方。

public class UserRealm extej t 7 f C [ nds AuthorizingRealm {
// 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据
private Uw j V U g h H * vserService userService = new UserServiceImpl();
/H Q f f & **
* 提供用户信息返回权限l 8 .信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String usernam3 = K e T  ? ] he = (String) principals.g7 L L : { E ?etPrimaryPrincipal();
SimpleAuth9 X v r HorizationInfo authb + ) P EorizationIP I ; Knfo = new SimpleAuthorizationInfo();
// 根据用户名查询当前用} 6 B户拥有的角色
Set<Role> roles =E # ( j D 5 { userService.findRoles(username);
Set<String> roleNames = new HashSet<String>();
for (Role role : roles) {
roleNK o Pames.add(role.getRole());
}
// 将角色名称提供给info
authorizationInfo.setRoles(roleNames);
// 根据用户名查询当前用户权限
Set&I W K llt;Permission> permissions = userServic` [ l -e.find5 6 ( C { {Permissions(username);
Set<String> permissN z ^ 8  iionNames = new HashSet<String>();
for (PermissionD I . permission : permissions) {
permissionNames.add(permission.getPermission` _ h u 9  Q Y());
}
// 将权限名称提供给info
authorizationInfo.setStringPermissions(pers U ; 5 r 9 i ~ kmissionNames);
return authorizationInfo;
}
/**
* 提供账户信息返回认证信息
*M y F k (/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticaG ~  $ ytionT6 = H  2 2 ? (oken token) throws AuthenticationException {
String username0 s , U F H L s * = (String) token.getPrincipal();
Usw D 7 T 8 Q # 3 Ger user = userService.fii g 1ndByUser?  f ( W b Lname(username);
if (user == null) {
// 用n B (户名不存在抛出异常
throw ne| j x #w UnknownAccountException();
}
if (user.getLocked() == 0) {
// 用户被管理员锁定抛出异常
throw new LockedAccountException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getU, S d o &sername(),
user.ge! D a C c : a w LtPassword(), ByteS] ( [ D & { D P pource.Util.bytes(user.getCredentials c iSalt()), getD u )Name());
return authz k % W o 7 ~ EenticationInfo;
}
}

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

用户表(存储用户名,密码,盐等)
角色表(角色名称,相关描述o [ * Y t N * v等)
权限表(权限名称,相关描述等)
用户-角色对应中间表(以用户ID和角色ID作为联合主键)
角色-权限| ! Y I O n ( e对应中间表(以角色ID和权限ID作为联合主键)

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

6、会话

用户的一次登录即为一次会话,Shiro也可以代替Tomcat等容器管理会话。目的是当用户W Q } I : & I停留在某个页面长时间无动作的时候,再次对任何链接的访问都P 6 S : P p (会被重定向到登录页面q f X ) R要求x T Q重新5 B c / g q ( 6输入用户名和密码而不需要程序员在Servlet中不停的判断Session中是1 G f W - X R ;否包含User对象。

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

三、与SpringMVC集成

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

1、配置前端过滤器

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

如果希望在WEB环境中使用Shiro必须首先在web.xml文件中配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="htE V P  . m Itp://www.w3.org/2001/XMLScheA Q 2 = P W H Fma-instance"
xmlns="http://java.sun.S B @ x 8 Scom/xml/ns/javaee"
xsi:schemaLocation="http://java- a }.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.x { } * 1 5 Ssd"
id="WebApp_ID" version="n P 1 Y i 3 3 y3.0">
<display-name>Shiro_Project</display-name>
&f / 0 { b z ult;welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>Spc ^ G $ t I H Br~ x L | 4ingMVC<{ $ @/servlet-name>U G =;
<servlet-class>org.springframework.web.servlet.DispatcherServlet&l1 o 2 F b V gt;/servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmL G A E 6 vc.xml</param-value>
</init-param>
<load-on-startup>1<t ! M A k C C/load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mappinC = | K Mg>
<servlet-name>SpringMVC</servlet-nam2 @ V u = $ we>
<url-pattern>/</url-pattern&[ % q M $gt;
</servlet-mapping>
<listener>
<listener-class>org.springfr& Q % % 6amework.web.coY  4ntext.ContextLoaderLi| i } * v  jstener</listener-class>
</listener>
<lis c E 1 W ntener>
<listener-class>org.springV o ; u I f & 7 yframework.web.util.Logn / S r4jConfigListener</listener-class>
</listener>
<context-param>
<param-name>contex= E j 9 / _ ;tConfigLocation</param-name>
<!-- 将Shiro的配置文件交给Spring监听器初始化 -->
<param-value>clasF D qspath:spring.xml,cl* P 5 Y ;  E 4asspath:? @ T i s V { 1 espring-shiro-web.xml</param-value>
</context-param>
<context-paR : ^ram>
<param-name>log4z R . = W 1 wjConfigLoaction</param-name>
<param-value>classpath:log4j.properu D ( yties</param-value>
</context-param>
<!-- shiro配置 开始 --&6 ( & ^ ` S m /gt;
<filter>
<filter-name>shiroFilter</filtP K Der-name>
<filter-class>org.springframework.web.filter.Del- @ ]egatingFilterProxy</filter-class>
<async-suppor. n ~  + $ted>true</async-supported>
<init-param( # l l E&gQ ? ~ * * ? . Kt;
<param-name>targetFilterLifecycle</pw % P  G O b s Zaram( y C h _ f + p S-name>
<param-value&n * K [ = K ! |gt;true</ : Lparam-value>
&lR M 6 1 A ? jt;/init-param>
</fQ ? x b / 9 ] Gilter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<ur& j ` v q 4 R v l-pattern>/*</url-pattern>
</filter-mapping>
<!-- shiro配置 结束 -->
</web-app>

熟悉Spring配置的同学可以重点看有绿字注释的部分,这里是使Shiro生效的关键。由8 0 {于项目通过Spring管理,因此所有的配置原则上H d k 6都是交给Spring。DelegatingFilZ P m 9 $ JterProxy的功能是通知Spring将所有的Filter交给Shiri = } $ ~oFilter管理。

接着在classpath路径下配置su q i E # wpring-shiro-web.xml文件

<beans xmlns3 b 5 X s q F="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchem0 T @ V i d # 9 ea-instance" xmln$ Z P _ n  P ) Ms:p=; / 8 j *"http://www.springframework.org/schema/p"
xmlns:contek % Sxt={ o X - 7 t +"http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schz 9 K t kemaLocation="httR J - mp://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.q @ F G G D # ) Uorg/schema/context
http://www.springframeworkZ d  V W E ;.org/schema/context/spring-context-3.1.xsd
http://www.sprV S !ingk : C ~ e e b 9framework.org/sch) - $ %ema/mvc
http://www.springframework.org/scheR 8 q n jma/mvc/spring-mvc-4.0.xsd">
&a p P _lt;!-- 缓存管理器 使用Ehcache实现 -->
<bean id="cacheManager" class="org.apache.shiro.cachP n / `e.ehcache.EhCacheManager">
<property name="cacheManagerConfigFB x & R * P S `ile" value="cl@ L k l R 5 aasspath:ehcache.xml" />
</bean>_ / e a @ Y _  (;
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" cl2 V 8 z P 6asr a *  W 0 1 ns="utils.Retrp P MyLimitHashedCredentialsM= u 3 Iatcher">
<constructor-arg ref="cacheManager" /&x @ t a F d l $ Zgt;
<property name="hashAlgorithmName" value=J K @ @"md5" />
<property nam1 c I W * R @e="hashIterations" value="2" /} U T 4 0 E o q &>
<property name="storedCredentialsHexEncoded" value="tB S [ T Rrue" /&g w f L # b kt;
</bean>
<!-- Realm实现 -->
<bean id="userRealm" class="utils.UserRealm">
<propert] O Q sy name="credentialsMatcher" ref="credentialsMatcher9 R ( h" />
</bean>
<= K ~ - ? ( F;!-- 安全管R n G * w 8 I 1理器y V * R --&` _ k y +  , dgt2 S H B d J * $;
<2 = U T y *;bean id=c y ( S ;"securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userR[ h 7 8 ?ealm" />
</bean>
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter$ O ! Y % ;" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref=, : ` %"security{ h 2 I 2 F / O -Manager" />
<property name="loginUrl" value="/" /r T Q 1 + + ^>
<property name="unaz g & o A }uthorizedUrlm F V 0 [ * +" value="/" />
<property name="filterChainDefinitionO a % z % t h K ]s">
<value>
/authc/admin = roles[admin]
/authc/** = authc
/** = anD c v G T Eon
</value>
</property>
</bean>
&la x s  1 p i  t;bean id="lifecycleBeanPostProcessor" classd Z V="org.apache.shiro.spring.LifecycleBeanPostProcesso, x 8 k N i ~ ar" />
&la z M 6 _ N d 9 Yt;/beans>

需要注意filterChl C W u 3 a rainDefi C ] o m o + Jnitions过滤器中对于路径的配置是有顺序的,当找到匹
器不会再继续寻找。因此带有通配符的路径要放在后面。三条配置的含义是:

/authc/admin需要用户有用admin权限
/aut[ Z C 9 @ khc/**用户必须登录才能访问
/**其他所有路径任何人都可以访问

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

@Controller
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping("login")
public ModelAndView login(@RequestParam(% t o D T W ) 8 @"usernameJ Q # S T D H L e") String username, @RequestParam("password") String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, passwordT % O S E W A ` U);
Subject subjec^ F @ G & 7 Ft = SecurityUtils.getSubject();
try {
subject.lg  p L / a .ogin(= J } P v Ctoken);
} catch (IncorrectCredentialsException ice) {
// 捕获密码错误异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message& a :", "password error!");
return mv;
}A T % Y catch? Y z n j h 9 B i (UnknownAccountException uae) {
// 捕获未知用户名异常
ModelAndView mv = nem ( B } o z S Ow ModelAndView("error");
mv.addObject(2 * G G i L"message", "username error!"( h =  i J);
return mv;
} catch (ExcessiveAttemptsException eae) {
// 捕获错误登录过多的异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "times error");
return mv;
}
User user = userService.findN ~ 0 pByUsername(username);
subject.getSession().setAttribute("user"6 | * X . & 3 U, user);
retur} [ ^ @n new ModelAndView("success");
}
}

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

@Controller
@D S I ]RequestMapping([ s , A"authc")
publicg x ( h A l 1 T classK 3 3 = [ ( 9 8 AuthcController {
// /authc/** = authc 任何通过表单登录的用户都可以访问
@RequestMapping("anyL $ ^ 6user"x # H S })
p% Q D A C * Uublic ModelAndView anyuser() {
Sum  | P qbject subjec[ 8 o zt = SecurityUtils.getSubject();
User user = (User) subject.getSession().getAttribute("user");
System.out.println(user);
return new ModelAn= C ? g r qdView("inner");
}
// /authc/admin = user[admL ; h r pin] 只有具备admin角色的用户才可以访问,否则请求将被重定向至登录界面
@RequestMapping("admin")3 ( R
public ModelAndView admin(){ O E ) 1 {
Subject subject = SecurityUtils.getSubject() . v q;
User user = (User) subject.getSession().getAttribute("uQ s x U  z 7 c User");
System.out.println(user);
return new^ ~ $ G y p K ) ModelAndView("inner");
}
}

本篇内容大多总结自张开涛的《跟我学Shiro》原文地址:
http:/i ^ . U # d ./jinnianshilongnian.iteye.com/blog/2018936

【云栖号在线课堂】每天都有产品技术专家分享
课程地址:) } S H * 9 h Bhttps://yqh.aliyun.com/zhibo

立即加入社群X U r } a ^,与专家面对面,及时了解课程最新y 2 S ~ E 0动态
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvq z 5nK

原文发布时间:2020-05-29
本文作者: 冷豪
本文来自j $ l ( 6 Z 5:“互联网架构师 微信公众号”,了解相关信息可以关注“互联网架构师”