目录
1.Shiro的介绍
1.1 Shiro的简介
1.2 Shiro的功能
1.3 Shiro的架构
2.Shiro提供的快速启动项目
3.SpringBoot整合Shiro
4.Shiro认证
4.1 Shiro中认证的介绍
4.2 Shiro中认证的实现
5.Shiro基于MD5加密认证
6.Shiro授权
6.2 Shiro中授权的介绍
6.2 Shiro中授权验证的方式
6.3 Shiro中授权验证的实现方式
6.4 Shiro中对认证用户进行授权
7.Shiro中使用缓存
7.1 Shiro中使用EhCache缓存
7.2 Shiro中使用Redis缓存
1.Shiro的介绍
1.1 Shiro的简介
- Shiro是Apache公司开发的一套关于JAVA的安全(权限)框架。
- Shiro可以让我们非常轻松的开发出足够好的应用,其不仅可以在JAVASE环境中使用,还可以在JAVAWEB中使用。
- Shiro的主要功能是:认证、授权、加密、会话管理、缓存等。
1.2 Shiro的功能
- Authentication:身份认证。验证用户是不是拥有相应的身份,即是否登录。
- Auhorization:授权,即权限验证。验证某个已经认证的用户是否拥有某个权限,也就是判断用户是否能进行某个操作,如:验证用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个(访问、编辑、删除等)权限。
- Session Manager:会话管理。用户登录后就是一次会话,在没有退出登录前,它的所有信息都在会话中。
- Cryptography:加密。保护数据的安全性,如密码加密后再存储到数据库,而不是直接明文存储。
- Web Support:Web支持,可以非常简单的继承到Web环境。
- Caching:缓存。比如用户登录后,其用户信息、拥有的角色/权限信息等不必每次都去数据库查询,可以存放在内存中,提高效率。
- Concurrency:Shiro支持多线程应用的并发验证。如在一个线程中开启另一个线程,能把权限自动传播过去。
- Testing:提供测试支持。
- Run As:允许一个用户伪装成另一个用户(如果他们允许)的身份进行访问。
- Remember Me:记住我。即常见的记住密码功能,表示一次登录后,下次一定时间内再来访问就不用再登录了,系统已经记录了你之前的登录状态。
1.3 Shiro的架构
从Shiro的外部架构来看,即从应用程序的角度来观察如何使用Shiro来完成安全方面的工作:
Subject
:Application Code
(应用程序代码)和Shiro直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject,Subject代表了当前用户
,这个用户不一定是一个具体的人,与当前应用程序交互的任何东西都是Subject,如网络爬虫和机器人等;另外,Subject实际上所有的操作并不是由自己的来完成的,而是委托给SecurityManager
来完成,Subject其实更像是一个门面,SecurityManager才是实际的执行者。Shiro SecurityManager
:安全管理器,是Shiro的核心。即所有与安全有关的操作都有SecurityManager来完成。它管理着所有的Subject,并负责与Shiro其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色;Realm
:Shiro从Realm获取安全数据,即在Shiro中专门用于访问安全方面的数据(如用户、角色和权限等信息),也就是说SecurityManager想要验证用户身份,需要从Realm中获取用户的相应角色/权限信息来进行校验。它类似于Shiro中的Dao,一个为我们提供访问安全数据的接口。
从Shiro的内部架构来看:
2.Shiro提供的快速启动项目
Shiro为我们提供了一些简单示例,示例里面有一个quickstart
,相当于入门级的HelloWorld,我们可以去其Github仓库下载:apache/shiro ,samples包下面还包括一些和SpringBoot的整合方式等源码。
官方提供的quickstart项目如下:
Quickstart.java(舔加了注释说明,很容易看懂):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.lang.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple Quickstart application showing how to use Shiro's API.
*
* @since 0.9 RC2
*/
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// The easiest way to create a Shiro SecurityManager with configured
// realms, users, roles and permissions is to use the simple INI config.
// We'll do that by using a factory that can ingest a .ini file and
// return a SecurityManager instance:
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();//获取SecurityManager
// for this simple example quickstart, make the SecurityManager
// accessible as a JVM singleton. Most applications wouldn't do this
// and instead rely on their container configuration or web.xml for
// webapps. That is outside the scope of this simple quickstart, so
// we'll just do the bare minimum so you can continue to get a feel
// for things.
SecurityUtils.setSecurityManager(securityManager);
// Now that a simple Shiro environment is set up, let's see what you can do:
// get the currently executing user:
Subject currentUser = SecurityUtils.getSubject();//获取Subject
// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession();//获取Session
session.setAttribute("someKey", "aValue");//设置Session属性
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {//校验Session属性
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对象中
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);
}
}
通过分析上述源文件可得出Shiro的一些基础使用方法:
- 首先需要配置和获取到上述提到的Shiro的
SecurityManager
对象。- 获取
Subject
作为当前用户。- 然后可以通过这个
Subject
的currentUser
对象来获取Session
、进行认证currentUser.isAuthenticated()
、登录currentUser.login(token);
、授权currentUser.hasRole("schwartz")
、currentUser.isPermitted("lightsaber:wield")
等操作。
上述都是可以通过上面的Quickstart.java得到的一些Shiro的使用,但是如果我们整合到SpringBoot中,就可以通过Spring或配置类来统一匹配管理对象,显然不用通过上述的复杂的方式获取SecurityManager等对象。
3.SpringBoot整合Shiro
首先我们需要明确,在SpringBoot中,Shiro如何处理所有的请求呢?很容易想到JavaWeb中的Filter机制,所以实际上在SpringBoot中我们需要在过滤器Filter中配置Shiro的核心SecurityManager
,然后SecurityManager需要通过Realm中获取数据,所以再向SecurityManager中设置好自定义的Realm
即可。
3.1 导入Shiro的Maven依赖:
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
3.2 配置Shiro环境:
配置流程解析:
首先,创建
ShiroConfig.java
配置类,并在配置类中创建Shiro的过滤器ShiroFilterFactoryBean
;然后继续创建Shiro的安全管理器DefaultWebSecurityManager
;由于安全管理器需要获取Realm的数据,所以还要继续创建自定义的Realm类,继承AuthorizingRealm
。最后一步,设置SecurityManager中的Realm,并将SecurityManager注入到Shiro拦截器即可。
Shiro配置类ShiroConfig.java
:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
//创建Shiro的filter
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
//注入Shiro安全管理器
filter.setSecurityManager(securityManager);
//配置过滤器,key为要拦截的受限资源或放行的公共资源,value为过滤器类型
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/","anon");
filterMap.put("/register","anon");
filterMap.put("/login","anon");
filterMap.put("/user/add","roles[admin]");
filterMap.put("/**","authc");
filter.setFilterChainDefinitionMap(filterMap);
//设置默认认证界面路径,即登录页面
filter.setLoginUrl("/toLogin");
filter.setUnauthorizedUrl("/unauthorized");
return filter;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(UserRealm userRealm){
//创建Shiro安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//给安全管理器注入自定义的Realm
securityManager.setRealm(userRealm);
return securityManager;
}
}
自定义的示例Realm类UserRealm.java
:
@Component
public class UserRealm extends AuthorizingRealm {
//处理授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//处理认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
4.Shiro认证
4.1 Shiro中认证的介绍
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
Shiro中认证的关键对象:
- Subject:主体。访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
- Principal:身份信息。是主体(subject)进行身份认证的标识,标识必须具有
唯一性
,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。- credential:凭证信息。是只有主体自己知道的安全信息,如密码、证书等。
认证流程:
4.2 Shiro中认证的实现
上述虽然完成了在SpringBoot中整合Shiro的配置,但是无论在过滤器中还没有做任何的过滤设置,所以过滤器不会拦截任何请求。因此我们还需要继续配置系统的访问受限资源和不被过滤的公共资源才能实现对用户的认证管理。
在ShiroConfig
配置类中的ShiroFilterFactoryBean
过滤器中进行配置:
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
//创建Shiro的filter
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
//注入Shiro安全管理器
filterFactoryBean.setSecurityManager(securityManager);
//配置过滤器,key为要拦截的受限资源或放行的公共资源,value为过滤器类型
//这里必须用LinkedHashMap,过滤器的添加顺序不同拦截效果就不同,后添加的过滤器是在前一个过滤器的基础上再做的过滤,
Map<String,String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
filterFactoryBean.setFilterChainDefinitionMap(filterMap);
//设置默认认证界面路径,即登录页面
filterFactoryBean.setLoginUrl("/toLogin");
return filterFactoryBean;
}
对于上述过滤器的类型,authc
代表fiter类型的别名,表示指定的Url需要进行认证后才能访问。更多关于过滤器类型的使用,请参考官方文档 default-filters 或别名请参考下表:
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问,即无需任何权限,无参 |
authc | FormAuthenticationFilter | 指定url需要认证(登录)才能使用,无参 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要HttpBasic认证登录,无参 |
logout | LogoutFilter | 登出过滤器,配置指定Url后,即可通过请求该Url实现登出 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问,可以多个参数,多个参数时必须加上引号,并且参数之间用逗号分割,例如/user/manage =perms["user:add:*,user:modify:*"] ,当有多个参数时必须每个参数都通过才通过,相当于isPermitedAll() 方法。 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要是指定角色才能访问 ,可以有多个参数,例如/user/manage/** =roles["admin,guest"] ,每个参数通过才算通过,相当于hasAllRoles() 方法。 |
ssl | SslFilter | 表示安全的url请求,需要https请求才能访问,无参 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
示例:
//以上表过滤器别名的形式添加过滤器
Map<String,String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/update","perms[user:update:*]");//一个参数
filterMap.put("/user/update","perms['user:add:*','user:update:*'");//多个参数
//以官方文档中过滤器对应类对象的形式添加过滤器
Map<String, Filter> filterMap1 = new HashMap<>();
filterMap1.put("/user/add",new AnonymousFilter());
通常上述过滤器类型可以分为两组:
anon
,authc
,authcBasic
,user
是第一组,属于认证过滤器;perms
,port
,rest
,roles
,ssl
是第二组,属于授权过滤器。
过滤器配置完成后,即实现了对请求的拦截,那么我们要实现登录认证,则需要在Controller中按照快速启动项目中的操作那样进行登录:
@PostMapping("login")
public String login(String username, String password){
Subject currentUser = SecurityUtils.getSubject();
//用户名和密码封装到token中
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
//登录
currentUser.login(token);
return "index";
}catch (UnknownAccountException e){
System.out.println("用户名不存在");
return "redirect:/toLogin";
}catch (IncorrectCredentialsException e){
System.out.println("密码错误");
return "redirect:/toLogin";
}
}
之前提到Subject并不直接进行认证操作,而是由SecurityManager中存放用户信息的Realm来处理,上述我们自定义的Realm中实现了父类的两个方法,一个执行认证操作,一个执行授权操作。UserRealm.java
:
/**
* 处理认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//通过token拿到用户名
String principal = (String)token.getPrincipal();
//将用户名与数据库校验
//我这里数据库中没有username字段,用户名是telephone,查询方式是用的mybatis-plus的条件构造器
User user = userMapper.selectOne(new QueryWrapper<User>().eq("telephone", principal));
//如果查询结果为空则用户名不存在,返回null,Controller中则捕获UnknownAccountException异常
if (user==null){
return null;
}
//第一个参数为用户身份(即用户名),第二个参数为正确密码,第三个参数为Realm名,直接用父类名即可
return new SimpleAuthenticationInfo(principal,user.getPassword(),this.getName());
}
以上即通过Shiro安全框架实现了完整的认证流程。
5.Shiro基于MD5加密认证
为什么要加密?
在实际开发中,为了保证用户的数据安全,我们并不会将密码直接明文存储到数据库中(如果数据库被坏人攻破,则坏人直接就可以看到明文密码),一般需要对密码进行加密后再存储到数据库。所以Shiro也为我们准备了多种加密算法,下述将基于MD5,实现加密认证。
MD5算法简介:
MD5算法的特点是将给定的一个明文生成为一个十六进制的,32位长的字符串,且该字符串不可逆,也就是通过密文无法解码为原文。主要用于加密和生成签名(校验文件)这两个方面。
MD5加盐(Salt):
虽然上述MD5加密不可逆,但是现在网上仍然有很多MD5的在线解密网站,实际上那些网站仍然不能解密,他们之所以能通过密码得到正确是原文是因为他们用的穷举算法,他们实现就对很多数据进行了MD5加密,然后对应生成很多这样的模板,所以一些简单的原数据能够被解密。
因此为了解决该问题,避免被别人用穷举的方式解密,我们一般设置密码不能太简单,另一种办法就是加盐,所谓加盐即给原数据加一点料,加一点其他字符后再进行MD5加密,添加的这部分字符则成为Salt。对原数据加盐后,哪怕坏人对我们的MD5码成功进行了解密,他也不知道我们的Salt是什么,哪怕知道盐是什么,他也很难猜到我们加盐的策略,因为我们可以在原数据的后面、前面或者中间任何位置添加,这样从很大程度上保证了我们密码的绝对安全。
对密码进行MD5+Salt加密后,为了保证验证时的正确性,我们一般也需要将注册账号时随机生成的Salt一同存入数据库。
哈希散列加密:
另外,为了再进一步保证我们加密过程的绝对安全性,我们还可以对MD5+Salt加密后的密文,再进行多次哈希散列。
所以,通过上述加密问题描述,我们可以采用MD5+Salt+哈希散列的方式对密码进行加密。
首先,为了保证登录认证的正确性,我们要在注册时确定好加密方式和策略,存储好加密后的密码和Salt:
//为了表述方便直接将逻辑写在了Controller中,按照MVC三层架构风格请在实际开发中写到Service层中
@GetMapping("/register")
public String register(User user){
//对密码进行MD5+Salt+哈希散列加密
//第一个参数-原数据 第二个参数-盐 第三个参数-散列次数
//盐的生成策略请自行设计或指定为固定的字符串即可
Md5Hash md5Hash = new Md5Hash(user.getPassword(),"salt",1024);
//得到十六进制加密后的32位字符串
String password = md5Hash.toHex();
System.out.println(password);
return "index";
}
登录控制器中不需要做任何修改,Subject只是一个门面,只是通知SecurityManager该执行登录操作了。所以我们只需要在自定义的UserRealm.java
中的认证方法中为认证信息SimpleAuthenticationInfo
对象加上Salt
参数,并在其构造方法中开启MD5加密和哈希散列即可:
@Component
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
public UserRealm() {
//创建hashed凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置md5加密
credentialsMatcher.setHashAlgorithmName("md5");
//设置哈希散列次数
credentialsMatcher.setHashIterations(1024);
this.setCredentialsMatcher(credentialsMatcher);
}
/**
* 处理授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 处理认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//通过token拿到用户名
String principal = (String)token.getPrincipal();
//将用户名与数据库校验
//我这里数据库中没有username字段,用户名是telephone
User user = userMapper.selectOne(new QueryWrapper<User>().eq("telephone", principal));
//如果查询结果为空则用户名不存在,返回null,Controller中则捕获UnknownAccountException异常
if (user==null){
return null;
}
//第一个参数为用户身份(即用户名),第二个参数为正确密码,第三个参数为盐,第四个参数为Realm名,直接用父类名即可
return new SimpleAuthenticationInfo(principal,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
}
}
6.Shiro授权
6.1 Shiro中授权的介绍
认证通过后,我们可以访问一些页面或者发起一些请求,但一般我们在实际应用中,还要区分不同的角色和对资源的权限。比如很多网站有会员,会员可以使用的功能会比普通用户多,这样我们就可以区分成一个系统中有很多角色,一个角色又有多种权限,每个权限对应可以访问的资源、请求等来进行权限控制。
授权:
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限才可以访问系统的资源,对于某些资源没有权限是无法访问的。
Shiro中授权关键对象:
授权可简单理解为who对what(which)进行How操作,即谁需要对什么进行什么操作:
Who,即主体(Subject)
,主体需要访问系统中的资源。What,即资源(Resource)
,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型
和资源实例
,比如商品信息为资源类型
,类型为t01的商品为资源实例
,编号为001的商品信息也属于资源实例。How,权限/许可(Permission)
,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
授权流程:
授权的前提是进行了权限和资源的分配,实现方式请参考上述认证实现中的授权过滤器。
6.2 Shiro中授权验证的方式
1.基于角色的访问控制:
RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制。
if(subject.hasRole("admin")){ //操作什么资源 }
2.基于资源的访问控制:
RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制。
if(subject.isPermission("user:update:01")){ //资源实例 //对01用户进行修改 } if(subject.isPermission("user:update:*")){ //资源类型 //对01用户进行修改 }
上述对是否拥有权限的判断是用的专门的权限字符串,有一定的规则:格式
资源标识符:操作:资源实例标识符
意思是对哪个资源的哪个实例具有什么操作,:
是资源/操作/实例的分割符,权限字符串也可以使用*
通配符。例如:
- 用户创建权限:user:create,或user:create:*
- 用户修改实例001的权限:user:update:001
- 用户实例001的所有权限:user:*:001
6.3 Shiro中授权验证的实现方式
1.编程方式:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”) && subject.isPermission("user:update:01")) {
//有权限
} else {
//无权限
}
2.注解方式: 该方式相当于同时包含了验证授权的逻辑和请求的过滤配置。
@RequiresRoles("admin")
@RequiresPermissions("user:update:01")
@GetMapping("/hello")
public void hello() {
//有权限
}
3.模板引擎标签方式:
首先,添加thymeleaf和shiro的整合依赖:
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
然后,页面中引入命名空间:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
常见权限控制标签使用:
<!-- 验证当前用户是否为“访客”,即未认证(包含未记住)的用户。 -->
<p shiro:guest="">Please <a href="login.html">login</a></p>
<!-- 认证通过或已记住的用户。 -->
<p shiro:user="">
Welcome back John! Not John? Click <a href="login.html">here</a> to login.
</p>
<!-- 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。 -->
<p shiro:authenticated="">
Hello, <span shiro:principal=""></span>, how are you today?
</p>
<a shiro:authenticated="" href="updateAccount.html">Update your contact information</a>
<!-- 输出当前用户信息,通常为登录帐号信息。 -->
<p>Hello, <shiro:principal/>, how are you today?</p>
<!-- 未认证通过用户,与authenticated标签相对应。与guest标签的区别是,该标签包含已记住用户。 -->
<p shiro:notAuthenticated="">
Please <a href="login.html">login</a> in order to update your credit card information.
</p>
<!-- 验证当前用户是否属于该角色。 -->
<a shiro:hasRole="admin" href="admin.html">Administer the system</a><!-- 拥有该角色 -->
<!-- 与hasRole标签逻辑相反,当用户不属于该角色时验证通过。 -->
<p shiro:lacksRole="developer"><!-- 没有该角色 -->
Sorry, you are not allowed to developer the system.
</p>
<!-- 验证当前用户是否属于以下所有角色。 -->
<p shiro:hasAllRoles="developer, 2"><!-- 角色与判断 -->
You are a developer and a admin.
</p>
<!-- 验证当前用户是否属于以下任意一个角色。 -->
<p shiro:hasAnyRoles="admin, vip, developer,1"><!-- 角色或判断 -->
You are a admin, vip, or developer.
</p>
<!--验证当前用户是否拥有指定权限。 -->
<a shiro:hasPermission="userInfo:add" href="createUser.html">添加用户</a><!-- 拥有权限 -->
<!-- 与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。 -->
<p shiro:lacksPermission="userInfo:del"><!-- 没有权限 -->
Sorry, you are not allowed to delete user accounts.
</p>
<!-- 验证当前用户是否拥有以下所有角色。 -->
<p shiro:hasAllPermissions="userInfo:view, userInfo:add"><!-- 权限与判断 -->
You can see or add users.
</p>
<!-- 验证当前用户是否拥有以下任意一个权限。 -->
<p shiro:hasAnyPermissions="userInfo:view, userInfo:del"><!-- 权限或判断 -->
You can see or delete users.
</p>
<a shiro:hasPermission="pp" href="createUser.html">Create a new User</a>
页面标签不起作用请在ShiroConfig.java配置类中加入方言处理:
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
6.4 Shiro中对认证用户进行授权
在自定义的Realm中继承的处理授权的方法中进行授权:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("primaryPrincipal = " + primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("user:update:*");
simpleAuthorizationInfo.addStringPermission("product:*:*");
return simpleAuthorizationInfo;
}
7.Shiro中使用缓存
在使用Shiro进行安全验证后,在请求不同的资源时,甚至一个页面中有多个资源时,这时Shiro的过滤器会对每一个资源进行是否授权的验证,即走自定义Realm中的认证和授权的处理方法;每次验证时,都要去数据库取用户的角色信息、资源权限等信息,这样多次查询数据库的一个过程中,会极大的消耗资源(操作数据库是IO操作,非效率低下),并且实际上这些角色和权限信息,很少有变动。
缓存的作用:
所以我们为了减轻数据库的访问压力,提高系统的查询效率,完全可以在查询第一次数据库后,将认证授权等信息放到缓存中,之后再次认证和授权的时候,如果缓存中已经有这个用户的信息,就直接从缓存中读取进行校验,而不再去从数据库中读取。
7.1 Shiro中使用EhCache缓存
Shiro中默认使用EhCache
来实现缓存,实现缓存的步骤如下:
1.引入依赖:
<!--引入shiro和ehcache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
2.开启缓存: 在我们自定的Realm构造方法中进行开启缓存的配置。
public UserRealm() {
//创建hashed凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置md5加密
credentialsMatcher.setHashAlgorithmName("MD5");
//设置哈希散列次数
credentialsMatcher.setHashIterations(1024);
this.setCredentialsMatcher(credentialsMatcher);
//开启缓存管理器
this.setCachingEnabled(true);
this.setAuthorizationCachingEnabled(true);
//授权缓存名
this.setAuthorizationCacheName("authorizationCache");
this.setAuthorizationCachingEnabled(true);
//认证缓存名
this.setAuthenticationCacheName("authenticationCache");
this.setCacheManager(new EhCacheManager());
}
注意:虽然这样实现了对认证和授权的缓存。但是EhCache实现的是本地缓存,当服务器重启后或者宕机后,缓存数据就消失了。
7.2 Shiro中使用Redis缓存
我们还可以使用Redis实现分布式的认证和授权数据缓存,解决本地缓存存在的问题。
1.引入redis依赖:
<!--redis整合springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.application.properties中配置redis连接:
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0
3.创建RedisCacheManager.java
,上述使用EhCache缓存时this.setCacheManager(new EhCacheManager());
实际是设置的EhCacheManager
,并且该类实现于CacheManager
接口,所以使用Redis来缓存时,就创建RedisCacheManager类并同时实现该接口,然后在自定义的Realm
构造器中的EhCacheManager替换成RedisCacheManager即可。
@Component
public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println("缓存名称: "+cacheName);
return new RedisCache<K,V>(cacheName);
}
}
4.创建RedisCache.java: , 上述第三步创建的RedisCacheManager.java
中实现的方法中,返回的是一个Cache
,所以实际上真正完成缓存任务的是这个Cache,因此我们需要 创建RedisCache类,并实现Cache接口。
@Component
public class RedisCache<K,V> implements Cache<K,V> {
@Autowired
private RedisTemplate redisTemplate;
private String cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
@Override
public V get(K k) throws CacheException {
System.out.println("获取缓存:"+ k);
return (V) redisTemplate.opsForHash().get(this.cacheName,k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
System.out.println("设置缓存key: "+k+" value:"+v);
redisTemplate.opsForHash().put(this.cacheName,k.toString(),v);
return null;
}
@Override
public V remove(K k) throws CacheException {
return (V) redisTemplate.opsForHash().delete(this.cacheName,k.toString());
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(this.cacheName);
}
@Override
public int size() {
return redisTemplate.opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<K> keys() {
return redisTemplate.opsForHash().keys(this.cacheName);
}
@Override
public Collection<V> values() {
return redisTemplate.opsForHash().values(this.cacheName);
}
}
解决序列化问题: ,在认证加盐时需要ByteSource
类型的参数,但由于Shiro中提供的SimpleByteSource
实现没有实现序列化,所以在开启缓存后在认证时会报错。因此我们需要自己创建自定义的ByteSource实现Salt的序列化。
public class MyByteSource extends SimpleByteSource implements Serializable {
public MyByteSource(String string) {
super(string);
}
}
如果仍然不能实现序列化则直接实现ByteSource,而不继承SimpleByteSource。