在参考中发现了 《Apache Shiro 参考手册》,强烈建议参看学习。
Shiro 简介
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
首先创建一个 SpringBoot 项目,并在 pom.xml 文件中引入如下会用到的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 导入shiro和spring整合依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
resources 文件夹下创建 shiro.ini
文件
[users]
zhangsan=123
lisi=123
wangwu=123
测试 Shiro
@Test
public void test01() {
// 创建安全管理器,设置它的 Realm
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//将安装工具类中设置默认安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//创建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen1", "123");
try {
subject.login(token);//用户登录
System.out.println("登录成功~~");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误!!!");
}
}
在这个测试中,我们可以把 ini 当做是个账号的数据库,UsernamePasswordToken token = new UsernamePasswordToken("xiaochen1", "123");
这一行看作是用户登陆时发来的账号和密码的通行证令牌
subject.login(token);
这里进行登陆,开始进行验证上面的 token
,这时设置的 IniRealm
对象会查找数据库(当前是 shiro.ini
文件)里是否有匹配的账号,然后验证密码,如果没有报错就是登陆成功,报错那就是不对。
该方法主要执行以下操作:
1. 检查提交的进行认证的令牌信息
2. 根据令牌信息从数据源(通常为数据库)中获取用户信息
3. 对用户信息进行匹配验证。
4. 验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
5. 验证失败则抛出AuthenticationException异常信息。
以下是几种出现的错误:
UnknownAccountException
(账号错误/没有账号)-
IncorrectCredentialsException
(密码错误) -
DisabledAccountException
(帐号被禁用) -
LockedAccountException
(帐号被锁定) -
ExcessiveAttemptsException
(登录失败次数过多) -
ExpiredCredentialsException
(凭证过期)等
上面代码运行过程:
上边的程序使用的是读取本地 ini
文件的方式进行测试进行验证判断用户名和密码是否正确,但正式开发中是不能用这种的,都需要从数据库中读取对应登陆的用户的信息,所以需要自定义 Realm
处理这些逻辑,以及添加更多样的操作。
Subject
即主体,外部应用与subject
进行交互,subject
记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject
在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject
进行认证授,而subject
是通过SecurityManager
安全管理器进行认证授权
Realm
即领域,相当于 datasource 数据源,SecurityManager
进行安全认证需要通过Realm
获取用户权限数据,比如:如果用户身份数据在数据库那么Realm
就需要从数据库获取用户身份信息。就是在Realm
里写认证和授权逻辑的,执行的时候会执行 Realm 里的认证和授权逻辑。
注意:不要把
Realm
理解成只是从数据源取数据,在Realm
中还有认证授权校验的相关的代码。
一般在真实的项目中,我们不会直接实现 Realm
接口,也不会直接继承最底层的功能贼复杂的 IniRealm
。我们一般的情况就是直接继承 AuthorizingRealm
,能够继承到认证与授权功能。它需要强制重写两个方法:doGetAuthenticationInfo
,doGetAuthorizationInfo
Shiro 提供的 Realm
体系较为复杂,一般我们为了使用 Shiro 的基本目的就是:认证、授权。可以看以下 SimpleAccountRealm
的部分源码中的两个方法。授权(doGetAuthorizationInfo
)、认证(doGetAuthenticationInfo
)。两部分的源码:
public class SimpleAccountRealm extends AuthorizingRealm {
//.......省略
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}
上面就是他的一个认证授权时的逻辑,我们可以自定义一个 UserRealm
,以实现更复杂更强大的认证、授权逻辑。
public class UserRealm extends AuthorizingRealm {
//授权方法(访问不同的 controller 里的接口时进行授权)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 这个 username 是下面 doGetAuthenticationInfo 方法 return 的对象的第一个参数的值
final String username = (String) principalCollection.getPrimaryPrincipal();
System.out.println("执行授权:授权的用户名 " + username);
final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 查询设置用户的权限 permission(下面假装是数据库查询到的数据)
Set<String> permissions = new HashSet<>();
permissions.add("auth:add_user");
permissions.add("auth:update_user");
info.setStringPermissions(permissions);
// 查询设置用户的角色 Role(下面假装是数据库查询到的数据)
Set<String> roles = new HashSet<>();
roles.add("管理员");
info.setRoles(roles);
return info;
}
//认证方法,在登陆的时候会进行验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行认证");
final UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
/*
//按数据库查询应该是以下代码,但为了方便起见,后面的写个假数据
User user = userService.findUser(token.getUsername());
if(user != null){
// 当前 Realm 中的 doGetAuthorizationInfo 方法那个参数就是这个第一个参数,保存了当前用户信息
return new SimpleAuthenticationInfo(
user,
user.getPassword(),
this.getName()
);
}
*/
// 数据库查询用户名和密码(这里假作已经查询到了结果)
String name = "zhangsan";
String password = "123";
if (name.equals(token.getUsername())) {
// new SimpleAuthenticationInfo 的时候会自动校验密码
return new SimpleAuthenticationInfo(
token.getPrincipal(),
password,
this.getName()
);
}
return null;
}
}
测试
@Test
public void test01() {
// 创建安全管理器,设置它的 Realm
final DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new UserRealm());
// 设置处理认证和授权的安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
final Subject subject = SecurityUtils.getSubject();
// 前端发送来的账号和密码生成的通行令牌,用来让 Realm 的 doGetAuthenticationInfo 方法里执行认证过程
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
try {
subject.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!!!");
}
}
上面运行过程
配置 Shiro
上面是单个测试时全都写到一个方法里了,做项目的时候我们需要给 Shiro 默认配置好,后面不用再一个个创建设置了,所以我们开始创建 ShiroConfig
,用来配置 shiro 的类:
import com.example.shirotest.common.UserRealm; // 引用自定义的 UserRealm
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro
*
* @author z
* @datetime 2022-5-16
*/
@Configuration
public class ShiroConfig {
/**
* 创建 Realm,bean会让方法返回的对象放入到spring的环境,以便使用
*/
@Bean(name = "userRealm")
public UserRealm getRealm() {
return new UserRealm();
}
/**
* @Qualifier 注释指定注入 Bean 的名称,用来消除歧义的
*/
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(userRealm);
return defaultWebSecurityManager;
}
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置一个安全管理器来关联 SecurityManager
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 默认登陆界面
shiroFilterFactoryBean.setLoginUrl("/loginPage");
// 设置权限(非注解方式设置对应 url 的权限)
// 注意要用 LinkedHashMap 遍历列表时时候按顺序进行匹配判断 URL
// 所以下面的 /** 要放在最后,否则如果放在第一个则所有的 URL 都
// 可以被匹配到,那么之后的就失效了
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/loginPage", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/add", "perms[user:add, admin]");
filterMap.put("/update", "perms[user:update]");
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 开启Shiro的注解 (如@RequiresRoles,@RequiresPermissions)
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
上面的 ShiroFilterFactoryBean
方法中主要主要配置接口的角色权限,确定接口由哪些角色或者哪些权限的用户可以访问。至于 shiro 是怎么知道当前用户是否具有某个角色或权限需要用到后面的 doGetAuthorizationInfo()
方法。
Filter | 解释 |
---|---|
anon |
无参,开放权限,可以理解为匿名用户或游客 |
authc |
无参,需要认证 |
user |
无参,表示必须存在用户,当登入操作时不做检查 |
perms[user] |
参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user:add, admin"] ,当有多个参数时必须每个参数都通过才算通过 |
roles[admin] |
参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin, user"] ,当有多个参数时必须每个参数都通过才算通过 |
常见过滤器
- 注意: shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username 、password ,rememberMe 等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
Shiro 权限字符串:
- 组成规则
在Shiro中使用权限字符串必须按照Shiro指定的规则。
权限字符串组合规则为:”资源类型标识符:操作:资源实例标识符
”
- 资源类型标识符: 一般会按模块,对系统划分资源。比如
user
模块,product
模块,order
模块等,对应的资源类型标识符就是:user
,product
,order
。 - 操作: 一般为增删改查(
create
,delete
,update
,find
),还有*
标识统配。 - 资源实例标识符: 如果Subject控制的是资源类型,那么资源实例标识符就是 “
*
” ;如果Subject控制的是资源实例,那么就需要在资源实例标识符就是该资源的唯一标识(ID
等)。
- 示例
*:*:*
表示 Subject 对所有类型的所有实例有所有操作权限,相当于超级管理员。-
user:create:*
表示 Subject 对user
类型的所有实例有创建的权限,可以简写为:user:create
。 -
user:update:001
表示 Subject 对ID
为001
的 user 实例有修改的权限。 -
user:*:001
表示 Subject 对ID
为001
的user
实例有所有权限。
Filter Chain定义说明:
- 1、一个URL可以配置多个Filter,使用逗号分隔
- 2、当设置多个过滤器时,全部验证通过,才视为通过
- 3、部分过滤器可指定参数,如perms,roles
上面的自定义的 UserRealm
和 ShiroConfig
创建的步骤你可以把它算作一个固定的套路,只要使用 Shiro 就难以避免,必须要创建这两个类,以及实现其中的方法。
MD5 加密
我们创建一个 MD5Utils
工具类,对密码进行加密,加密后的密码破解难度是“不可能破解出来”,即便黑客获取到数据库,也无法知道具体密码。(一般建议使用随机盐值,然后保存盐值到用户的数据库中,这样更加安全)
import org.apache.shiro.crypto.hash.Md5Hash;
import java.util.Random;
/**
* @author z
*/
public class MD5Utils {
/*** 加密盐值 */
public static final String SALT = "^3&5as@9.[km0";
/*** 密码进行MD5加密 (固定盐值) */
public static String md5Password(String password) {
return md5Password(password, SALT, 1024);
}
/*** 密码进行MD5加密 */
public static String md5Password(String password, String salt) {
// 加密 1024 次
Md5Hash md5Hash = new Md5Hash(password, salt, 1024);
return md5Hash.toHex();
}
**
* 生成随机盐 salt 的静态方法
*
* @length 生成长度
*/
public static String getSalt(int length) {
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
}
用户注册的时候,我们对他们的密码进行加密保存到数据库中,
那么在提交用户输入原始的密码的时候,我们需要进行加密跟我们已经保存的加密后的密码进行匹配,如果两个加密后的密码相同,那自然两个密码都是一样的
对测试类中的 token
添加加密,也就是对发来的密码进行 MD5 加密
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", MD5Utils.md5Password("123"));
UserRealm
中的 doGetAuthenticationInfo()
因为我们写的假数据,所以也要改一下,改为如下:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证");
// 数据库查询用户名和密码(这里假作已经查询到了结果)
// 如果是从数据库查找到的则不用进行 MD5Utils.md5Password 这个操作,因为数据库里已经加过密了
// 这里因为这是我们手动设置的假数据,所以需要 md5 加密一下
String name = "zhangsan";
String password = MD5Utils.md5Password("123");
final UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
if (name.equals(token.getUsername())) {
// new SimpleAuthenticationInfo 的时候会自动校验密码
return new SimpleAuthenticationInfo(
token.getPrincipal(),
password,
this.getName()
);
}
return null;
}
权限注解
注:shiro 提供了相应的注解用于权限控制,如果使用这些注解就需要使用 aop 的功能来进行判断。shiro 提供了 spring aop 集成,用于权限注解的解析和验证
(1)@RequiresAuthentication
:方法在访问或调用时,当前 Subject
必须在当前 session
中已经过认证。表示当前 Subject
已经通过 login
进行了身份验证;即 Subject.isAuthenticated()
返回 true
。
(2)@RequiresUser
:表示当前 Subject
已经身份验证或者通过记住我登录的,才能访问或调用被该注解标注的类,实例,方法。
(3)@RequiresGuest
:表示当前 Subject
没有身份验证或通过记住我登录过,即是游客身份。使用该注解标注的类,实例,方法在访问或调用时,当前 Subject
可以是 guest
身份,不需要经过认证或者在原先的 session
中存在记录。
(4)@RequiresRoles(value={"admin", "user"}, logical= Logical.AND)
当前 Subject
必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天 Subject
不同时拥有所有指定角色,则方法不会执行还会抛出 AuthorizationException
异常。
(5)@RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR)
:表示当前 Subject
需要权限 user:a
或 user:b
。当前 Subject
需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前 Subject
不具有这样的权限,则方法不会被执行。
Shiro 的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):
RequiresRoles
RequiresPermissions
RequiresAuthentication
RequiresUser
RequiresGuest
例如:你同时声明了 @RequiresRoles
和 @RequiresPermissions
,那就要求拥有此角色的同时还得拥有相应的权限。
@RequiresRoles
可以用在 Controller
或者方法上。可以多个 roles,多个roles 时默认逻辑为 AND
也就是所有具备所有 role
才能访问。
示例:
//属于user角色
@RequiresRoles("user")
//必须同时属于user和admin角色
@RequiresRoles({"user","admin"})
//属于user或者admin之一;修改logical为OR 即可
@RequiresRoles(value={"user","admin"},logical=Logical.OR)
@RequiresPermissions
和 @RequiresRoles
类似。示例:
//符合index:hello权限要求
@RequiresPermissions("index:hello")
//必须同时复核index:hello和index:world权限要求
@RequiresPermissions({"index:hello","index:world"})
//符合index:hello或index:world权限要求即可
@RequiresPermissions(value={"index:hello","index:world"},logical=Logical.OR)
@RequiresAuthentication,@RequiresUser,@RequiresGuest
这三个的使用方法一样
@RequiresAuthentication
@RequiresUser
@RequiresGusst
其他例子:
@RequiresPermissions({"file:read", "write:aFile.txt"} )
void someMethod();
要求subject中必须同时含有file:read
和write:aFile.txt
的权限才能执行方法someMethod()
。否则抛出异常AuthorizationException
。
@RequiresRoles({"admin"})
void method();
只有 admin
角色才能访问该方法,其他角色访问将会抛出异常
自定义 Shiro 注解
但是仅仅是拥有shiro中的这5个注解肯定是不够使用的。在实际的使用过程中,根据需求,我们会在权限认证中加入我们自己特有的业务逻辑的,我们为了便捷则可以采用自定义注解的方式进行使用。这种方法不仅仅适用于 Apache Shiro,很多其他的框架如:Hibernate Validator、SpringMVC、甚至我们可以写一套校验体系,在aop中去验证权限,这都是没问题的。所以自定义注解的作用很广。但是在这里,我仅仅基于shiro的来实现适用于它的自定义注解。
- 定义注解类
/**
* 用于认证的接口的注解,组合形式默认是“或”的关系
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
/**
* 业务模块
* @return
*/
String[] module();
/**
* 操作类型
*/
String[] 定义注解类
}
- 定义注解的处理类
/**
* Auth注解的操作类
*/
public class AuthHandler extends AuthorizingAnnotationHandler {
public AuthHandler() {
//写入注解
super(Auth.class);
}
@Override
public void assertAuthorized(Annotation a) throws AuthorizationException {
if (a instanceof Auth) {
Auth annotation = (Auth) a;
String[] module = annotation.module();
String[] action = annotation.action();
//1.获取当前主题
Subject subject = this.getSubject();
//2.验证是否包含当前接口的权限有一个通过则通过
boolean hasAtLeastOnePermission = false;
for(String m:module){
for(String ac:action){
//使用hutool的字符串工具类
String permission = StrFormatter.format("{}:{}",m,ac);
if(subject.isPermitted(permission)){
hasAtLeastOnePermission=true;
break;
}
}
}
if(!hasAtLeastOnePermission){
throw new AuthorizationException("没有访问此接口的权限");
}
}
}
}
- 定义shiro拦截处理类
/**
* 拦截器
*/
public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {
public AuthMethodInterceptor() {
super(new AuthHandler());
}
public AuthMethodInterceptor(AnnotationResolver resolver) {
super(new AuthHandler(), resolver);
}
@Override
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
// 验证权限
try {
((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi));
} catch (AuthorizationException ae) {
if (ae.getCause() == null) {
ae.initCause(new AuthorizationException("当前的方法没有通过鉴权: " + mi.getMethod()));
}
throw ae;
}
}
}
- 定义shiro的aop切面类
/**
* shiro的aop切面
*/
public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor {
public AuthAopInterceptor() {
super();
// 添加自定义的注解拦截器
this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver()));
}
}
- 定义shiro的自定义注解启动类
/**
* 启动自定义注解
*/
public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor {
public ShiroAdvisor() {
// 这里可以添加多个
setAdvice(new AuthAopInterceptor());
}
@SuppressWarnings({"unchecked"})
@Override
public boolean matches(Method method, Class targetClass) {
Method m = method;
if (targetClass != null) {
try {
m = targetClass.getMethod(m.getName(), m.getParameterTypes());
return this.isFrameAnnotation(m);
} catch (NoSuchMethodException ignored) {
}
}
return super.matches(method, targetClass);
}
private boolean isFrameAnnotation(Method method) {
return null != AnnotationUtils.findAnnotation(method, Auth.class);
}
}
总体的思路顺序:定义注解类(定义业务可使用的变量)-> 定义注解处理类(通过注解中的变量做业务逻辑处理)-> 定义注解的拦截器 -> 定义aop的切面类 -> 最后定义 shiro 的自定义注解启用类。其他的自定义的注解的编写思路和这个也是类似的。
注意事项
在日常开发时,往往会在 Service
层添加“@Transactional
”注解,为的是当 Service
发送数据库异常时,所有数据库操作可以回滚。
当在 Service
层添加“@Transactional
”注解后,执行 Service
方法前,会开启事务。此时的 Service
已经是一个代理对象了,此时如果我们将 Shiro 的权限注解加载 Service
层是不合适的,此时需要加到 Controller
层。这是因为不能让 Service
是“代理的代理”,如果强行注入,会发生类型转换异常。
推荐
推荐查看 SpringBoot 集成 Shiro,简明扼要
建议看完之后配合 Shiro保姆级教程 学习,他的文章中还包含有 SQL 的创建和实现步骤