权限管理
权限管理:包括身份认证和授权两部分。对于需要访问控制的资源,用户首先经过身份认证,认证成功后具有某一资源访问权限才能访问。
认证
身份认证,就是判断一个用户是否为合法的用户,最常用的认证方式通过核对用户输入的用户名和密码,看是否和系统存储的用户名和密码一致,以此来判断用户的身份是否合法。常见的认证方式有:
- 手机短信验证
- 三方授权认证
- 刷脸认证等
- 微信扫码登录等
- ……….
授权
控制谁能访问哪些资源。用户(主体)身份认证通过后,需要分配权限才能访问系统的资源,对于某些资源如果没有分配权限是不能访问的。可以理解为:主体(subject)对系统资源(resource)有什么权限(Permission)
资源(Resource)
如网站页面、目录、菜单、按钮等
权限(Permission)
一个主体能访问哪些资源
相关专业术语
subject
访问系统的用户,主体可以是用户,也可以是一台设备比如路由器、服务器、程序等,进行认证的都称为主体。
principle
身份信息,是主体(subject)证明自己身份的标识,标识必须有唯一性。如手机号、邮箱地址、指纹、密钥等。一个主体可以有多个身份信息,但必须有一个主身份(Primary Principal)
Credential
凭证信息,只有主体自己才知道的安全信息,如密码,私钥等。
权限管理流程
我们需要实现如下流程
解决方案
怎么实现上图中的流程呢?行业常见的解决方案有2种:
- 使用Filter或SpringMVC的拦截器自己实现上面流程。
- 使用框架比如Shiro、Springsecurity等。
我们主要讲解Shiro的使用。
表的设计
在讲Shiro之前,我们先看怎么设计表结构。
常见的有2种方式
用户直接跟资源挂钩
设计成3张表:用户表(sys_usr)、资源表(sys_resource)、用户资源表(sys_user_resource)。
用户表(sys_usr)
id | user_name | password |
---|---|---|
1 | 张三 | 123456 |
2 | 李四 | 888888 |
3 | 王二 | 666666 |
资源表(sys_resource)
id | resource_name |
---|---|
1 | 用户模块-查询 |
2 | 用户模块-删除 |
3 | 商品模块-查询 |
4 | 商品模块-删除 |
用户资源表(sys_user_resource)。
user_id | resource_id |
---|---|
1 | 1 |
1 | 2 |
2 | 1 |
2 | 2 |
2 | 3 |
表示用户id为1的用户也就是张三有:用户模块-查询、用户模块-删除权限。用户id为2的用户也就是李四有:用户模块-查询、用户模块-删除、商品模块-查询权限。
这种设计有很大的弊端:假设新增一个用户叫赵四,赵四和李四的的权限是一样的。这就会在sys_user_resource表里面添加3条记录。如果用户很多,就会产生很多冗余的数据。
Role-based Access Control (以角色为基础的访问控制)
用户跟角色挂钩、角色跟资源挂钩,不同的用户有不同的角色,不同的角色有不同的访问资源的权限。
设计5张表:用户表(sys_usr)、角色表(sys_role)、资源表(sys_resource)
用户角色表(sys_user_role)、角色资源表(sys_role_resource)。
用户表(sys_usr)
id | user_name | password |
---|---|---|
1 | 张三 | 123456 |
2 | 李四 | 888888 |
3 | 王二 | 666666 |
资源表(sys_resource)
id | resource_name |
---|---|
1 | 用户模块-查询 |
2 | 用户模块-删除 |
3 | 商品模块-查询 |
4 | 商品模块-删除 |
角色表(sys_role)
id | role |
---|---|
1 | 董事长 |
2 | 总经理 |
3 | 普通员工 |
角色资源表(sys_role_resource)
role_id | resource_id |
---|---|
1 | 1 |
1 | 2 |
1 | 3 |
用户角色表(sys_user_role)
user_id | role_id |
---|---|
1 | 1 |
2 | 2 |
由于角色的增加是有限的,新增角色表的同时,更新一下角色对应的资源表即可,当新增用户的时候,在用户角色表新增对应的角色即可,至于角色对应多少个资源权限,我们不需要关系。
推荐使用这种方式设计表结构。
Shiro
Shiro是Apache推出的安全管理框架,比SpringSecurity更加简单易用。
使用文档:https://shiro.apache.org/reference.html
核心角色
- SecurityManager:安全管理器,是 Shiro 架构的核心,充当一种“伞形”对象,协调其内部各个组件
- Subject:主体,比如用户
- Realm:相当于数据源,可以获取主体(subject)的权限信息,主体的权限信息、用户名等信息存储在Realm
- Authenticator:认证器,从Realm里获取subject相关信息,对subject进行认证看一下是否为合法用户。
流程:当用户进行认证的时候,会找到SecurityManager,SecurityManager找到Authenticator,Authenticator去Realm里面读取用户数据,进行认证,后面有详细流程。
Authorizer:授权器,从Realm里面获取subject相关信息,对subject进行授权验证,看一下subject有没有对某一个资源有操作的权限。
流程:当用户进行授权的时候,会找到SecurityManager,SecurityManager找到Authorizer,Authorizer去Realm里面读取用户数据,进行授权。<br />![Snipaste_2022-01-31_12-25-40.png](https://cdn.nlark.com/yuque/0/2022/png/21796966/1643603160733-30c2dd1e-b3b9-4628-87fc-a581868de460.png#clientId=u8657ed76-9580-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=uecdb0cba&margin=%5Bobject%20Object%5D&name=Snipaste_2022-01-31_12-25-40.png&originHeight=530&originWidth=797&originalType=binary&ratio=1&rotation=0&showTitle=false&size=243582&status=done&style=none&taskId=uc0286600-2c22-4ab3-9197-aea8a32b50e&title=)
基本使用
/*创建安全管理器*/
DefaultSecurityManager securityManager = new DefaultSecurityManager();
/*设置Realm,从配置文件里面读取Realm,Realm有很多种,我们真实的项目肯定是要从数据库里面读取数据,来放到Realm上*/
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
String userName = "lisi";
String passWord = "123456";
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,passWord);
try {
/*先传递一个简单的UsernamePasswordToken类型的AuthenticationToken*/
subject.login(usernamePasswordToken);
System.out.println(subject.isAuthenticated());//用户是否认证成功
/*能来到这里说明上面认证没有问题,判断用户有没有某个权限*/
Boolean isUserListPer = subject.isPermitted("user:list");
/*判断用户是否有某个角色*/
Boolean isAdminRole = subject.hasRole("admin");
System.out.println(subject.isAuthenticated());
System.out.println(isAdminRole);
subject.logout(); //退出登录,代表该用户没有认证,此时查用户的权限、角色就查不到了
System.out.println(subject.isAuthenticated());
System.out.println(subject.hasRole("admin"));
}catch (UnknownAccountException e){ //账号不存在
System.out.println("账号不存在");
}catch (IncorrectCredentialsException e){ //密码不正确
System.out.println("密码不正确");
}catch (AuthenticationException e){ //认证失败
System.out.println("认证失败");
}
Reaalm数据先从最简单的ini文件里面读取,shiro.ini文件数据如下
# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================
# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = 123456, admin
lisi = 123456, guest
zhangsan = 123456,guest
# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
guest = user:list,user:update
上面是最基本的使用过程,以后用户的的数据,比如用户名、密码、角色、权限是要从数据库里面读取的。这时Realm的数据来源就不能像上面的ini文件了,我们需要从数据库里面读取。
Shiro提供给我们很多Realm的实现类,如下图所示
其中JdbcRealm就是从数据库里面读取数据创建Realm的,但遗憾的是该类型Realm要有固定的表结构,如下表名需要是users,用户的密码字段需要是password
public class JdbcRealm extends AuthorizingRealm {
//TODO - complete JavaDoc
/*--------------------------------------------
| C O N S T A N T S |
============================================*/
/**
* The default query used to retrieve account data for the user.
*/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
/**
* The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
*/
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
/**
* The default query used to retrieve the roles that apply to a user.
*/
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
/**
* The default query used to retrieve permissions that apply to a particular role.
*/
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class);
//.....
}
自定义Realm
为了自定义查询数据的方案,我们需要自定义Realm。
自定义类继承AuthorizingRealm 因为上面JdbcRealm也继承该类并且同时包含认证和授权。
import lombok.Data;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomRealm extends AuthorizingRealm {
/**
* 当用户subject需要进行权限、角色等判断时,就会调用该方法比如:subject.isPermitted("user:list")、subject.hasRole("admin");
* 开发者需要在此方法里面根据用户名查询用户的权限、角色信息
* @param principals
* @return 返回用户的权限、角色信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// String userName = principals.getPrimaryPrincipal();//根据用户名到数据库里面查询用户的角色以及权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("user:list");
return simpleAuthorizationInfo;
}
/**
* 当主体(subject)进行认证的时候调用 subject.login(token);
* 开发者需要在这里根据用户标识(如用户名)到数据库查询用户的相关信息
* @param token:为subject.login(token)传进来的token
* @return 返回用户的具体信息如用户名、密码
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = String.valueOf(token.getPrincipal()); //获取用户名
String password = String.valueOf(token.getPrincipal()); //获取密码
//根据用户名到数据库查找用户相关信息,这 里做个假数据
User user = new User();
user.setUserName("lishi");
user.setPasWord("123456");
if (user == null) return null; //返回null内部会自己抛出UnknownAccountException异常
// if (user == null){
// throw new UnknownAccountException();
// }
// if (!password.equals(user.getPasWord())){ //这个我们不需要自己判断,内部会自行判断
// throw new IncorrectCredentialsException();
// }
//返回用户具体信息,传入数据库中查询到的用户名和密码,Realm内部会根据传来的token和数据库传来的用户名、密码进行对比
return new SimpleAuthenticationInfo(user.getUserName(),user.getPasWord(),getName());
}
}
@Data
class User{
private String userName;
private String pasWord;
}
外界使用
/*创建安全管理器*/
DefaultSecurityManager securityManager = new DefaultSecurityManager();
/*自定义Realm*/
securityManager.setRealm(new CustomRealm());
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
String userName = "lisi";
String passWord = "123456";
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,passWord);
try {
/*先传递一个简单的UsernamePasswordToken类型的AuthenticationToken*/
subject.login(usernamePasswordToken);
//到这里说明认证已经通过
System.out.println("subject 是否有user:list权限 " + subject.isPermitted("user:list"));
System.out.println("subject 是否有admin角色 " + subject.hasRole("admin"));
}catch (UnknownAccountException e){ //账号不存在
System.out.println("账号不存在");
}catch (IncorrectCredentialsException e){ //密码不正确
System.out.println("密码不正确");
}catch (AuthenticationException e){ //认证失败
System.out.println("认证失败");
}
认证流程
当用户调用subject.login()后,SecurityManager会找到认证器Authenticator,Authenticator会找到我们自定义的CustomRealm,就会调用CustomRealm里面的如下方法
/**
* 当主体(subject)进行认证的时候调用 subject.login(token);
* 开发者需要在这里根据用户标识(如用户名)到数据库查询用户的相关信息
* @param token:为subject.login(token)传进来的token
* @return 返回用户的具体信息
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
}
调用完上面的方法后会返回AuthenticationInfo。如果AuthenticationInfo没有值说明用户名没有就会报UnknownAccountException异常,如果有值就会调用Realm里面的assertCredentialsMatch方法
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) { //注意如果有值就会来到这里
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
�assertCredentialsMatch的方法内部是这样的
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
//not successful - throw an exception to indicate this:
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
"credentials during authentication. If you do not wish for credentials to be examined, you " +
"can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
}
}
也就是会调用CredentialsMatcher里面的doCredentialsMatch方法来判断密码是否正确,下面接口实现类就是专门负责匹配密码是否正确的。
public interface CredentialsMatcher {
/**
* Returns {@code true} if the provided token credentials match the stored account credentials,
* {@code false} otherwise.
*
* @param token the {@code AuthenticationToken} submitted during the authentication attempt
* @param info the {@code AuthenticationInfo} stored in the system.
* @return {@code true} if the provided token credentials match the stored account credentials,
* {@code false} otherwise.
*/
boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info);//token为用户subject.login(token)传过来的,info为doGetAuthenticationInfo方法中我们返回的
}
�不同的实现类有不同的密码匹配方式。为了满足项目中的需求我们通常也自定义CredentialsMatcher类自己实现密码匹配规则。
�
授权流程
当用户调用subject.isPermitted()或者 subject.hasRole()等权限、角色等方法时,SecurityManager会找到授权器Authorizer,Authorizer找到我们自定义的CustomRealm,调用CustomRealm的如下方法
/**
* 当用户subject需要进行权限、角色等判断时,就会调用该方法比如:subject.isPermitted("user:list")、subject.hasRole("admin");
* 开发者需要在此方法里面根据用户名查询用户的权限、角色信息
* @param principals
* @return 返回用户的权限、角色信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// String userName = principals.getPrimaryPrincipal();//根据用户名到数据库里面查询用户的角色以及权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("user:list");
return simpleAuthorizationInfo;
}
根据返回的AuthorizationInfo信息判断权限、角色是否正确。
SpringBoot集成Shiro
场景:用户通过用户名密码登录后,服务端会返回客户端一个token,后续的请求中携带token放在请求头中,服务端验证token是否为合法的token。
依赖配置
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>
日志这里使用logback
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
运行直接报如下错误
No bean of type 'org.apache.shiro.realm.Realm' found.
自定义CustomRealm
新建自定义CustomRealm类继承AuthorizingRealm
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomRealm extends AuthorizingRealm {
public CustomRealm() {
super(new CustomMatcher()); //校验密码时,使用我们自定义的CredentialsMatcher去验证密码是否正确
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof Token;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 拿到token
String token = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//根据token或者用户ID到数据库查找用户的角色、权限信息,也可以提前把用户信息缓存起来
// 添加角色
info.addRole("xxx");
// 添加权限
info.addStringPermission("resource.getPermission()");
return info;
}
/**
* 认证:基于token的方案,能来到这里说明是合法的用户,已经登录过了并且token是有效的,直接保证该方法能认证通过即可
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
Token tk = (Token) token;
return new SimpleAuthenticationInfo(
tk.getPrincipal(),
tk.getCredentials(),
getName());
}
}
Shiro配置文件
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroCfg {
@Bean
public Realm realm() {
return new CustomRealm();
}
/**
* 在这里设置管理器、并且告诉Shiro如何进行拦截、拦截哪些URL、每个URL需要经过哪些Filter进行处理
* @param realm
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(Realm realm) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置管理器,在web项目里面直接这样配置即可
DefaultWebSecurityManager mgr = new DefaultWebSecurityManager(realm);
bean.setSecurityManager(mgr);
// 添加我们自定义的Filter
Map<String, Filter> filters = new HashMap<>();
//添加自定义Filter并给Filter起个别名
filters.put("customFilterxxx", new CustomFilter());//添加了一个Filter,Filter的名称为customFilterxxx。类似于anon对应的Filter为org.apache.shiro.web.filter.authc.AnonymousFilter
//设置自定义Filter
bean.setFilters(filters);
// 配置拦截的路径对应的Filter
Map<String, String> filterMap = new LinkedHashMap<>();//使用LinkedHashMap有顺序性,先加入的优先级最高
//登录接口不需要拦截
filterMap.put("/xx/login", "anon");//使用匿名Filter进行过滤,不需要登录也能访问
//swagger接口文档不需要拦截
filterMap.put("/swagger*/**", "anon");
filterMap.put("/v2/api-docs/**", "anon");
//测试接不需要拦截
filterMap.put("/test/**", "anon");
// 如果Filter里面发生异常就会来到我们自定义的ErrorController,这里面的URL也不应该被拦截
filterMap.put("/error/filter", "anon");
//所有其它的路径使用我们自定义的Filter拦截
filterMap.put("/**", "customFilterxxx");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
/**
* 解决:发现只要方法上面加@RequiresPermissions注解后,很多接口都报404问题,加下面代码解决此问题
*/
@Bean
public DefaultAdvisorAutoProxyCreator proxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setUsePrefix(true);
return proxyCreator;
}
}
Shiro提供了如下Filter供我们使用,Filter Name可以理解Filter的别名。
自定义CredentialsMatcher
�自定义CredentialsMatcher来自己校验密码规则
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
public class CustomMatcher implements CredentialsMatcher {
/**
* @return true代表校验通过,直接返回true即可,因为我们是基于token的没有密码的概念
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
return true;
}
}
自定义token
public class Token implements AuthenticationToken {
private final String token;
public Token(String accessToken) {
this.token = accessToken;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
自定义Filter
import com.lff.dr.common.util.Strings;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.filter.AccessControlFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义Filter验证用户是否合法
*/
public class CustomFilter extends AccessControlFilter {
public static final String TOKEN_HEADER = "Token";
/**可以在这个方法里面初步判断是否允许请求通过
* @return true会进入下一个链式调用(过滤器、拦截器、控制器),false就进入下面的onAccessDenied方法,交给shiro处理
*/
@Override
protected boolean isAccessAllowed(ServletRequest req,
ServletResponse resp, Object o) throws Exception {
return false;
}
/**
* 上面的isAccessAllowed方法返回false,就会来到这里
* @return true会进入下一个链式调用(过滤器、拦截器、控制器),false就不会进入
*/
@Override
protected boolean onAccessDenied(ServletRequest req,
ServletResponse resp) throws Exception {
// 获得请求
HttpServletRequest request = (HttpServletRequest) req;
// 获得token
String token = request.getHeader("Token");
// token为空
if (Strings.isEmpty(token)) {
throw new RuntimeException("token为空");
}
// 查找用户
if (user == null) {
throw new RuntimeException("token过期等");
}
// 鉴权,进入Realm,触发Realm的doGetAuthenticationInfo和doGetAuthorizationInfo方法,到数据库加载用户的角色、权限信息
SecurityUtils.getSubject().login(new Token(token));
// getSubject(req, resp).login(new Token(token));
return true;
}
}
如果Filter里面有异常,比如CustomFilter里面抛出异常,Tomcat内部就会报500错误给客户端,我们的异常拦截器就会拦截不到这样的异常处理。下面是异常处理的示例代码,@RestControllerAdvice这样的注解只能拦截到Controller出现的异常。
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
@ExceptionHandler
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public JsonVO handleThrowable(Throwable t) {
}
}
�为了能够拦截到Filter里面的异常,我们单独搞一个ErrorFilter,所有的请求先来到这个ErrorFilter,ErrorFilter处理完之后再把请求传递到其它Filter(如:我们的CustomFilter)。如果其中某一个Filter发生异常,我们再转发给Controller(单独搞一个ErrorController),在ErrorController里面抛出异常,这样上面的异常处理拦截器就能拦截到异常,把异常信息返回到客户端了。
ErrorFilter示例代码
import javax.servlet.*;
import java.io.IOException;
public class ErrorFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (Throwable e) {
request.setAttribute("error_filter", e);
request.getRequestDispatcher("/error/filter").forward(request, response);
}
}
}
配置ErrorFilter为第一个处理请求的Filter
@Configuration
public class WebCfg implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new ErrorFilter());
bean.addUrlPatterns("/*");
// 最高权限
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
处理错误ErrorFilterController代码如下
@RestController
public class ErrorFilterController {
@RequestMapping("/error/filter")
public void handle(HttpServletRequest request) throws Throwable {
throw (Throwable) request.getAttribute("error_filter");
}
}
外界直接用注解的方式使用即可
@RestController
@RequestMapping("/sysUsers")
@Api(tags = "系统用户")
public class SysUserController {
@GetMapping
@ApiOperation("分页查询")
@RequiresPermissions("user:list") //表示该方法必须有user:list权限
@RequiresRoles("admin") //表示该方法必须有admin角色
public ListJsonVo<SysUserVo> list(SysUserListReqVo reqVo) {
}
@GetMapping
@ApiOperation("分页查询")
@RequiresPermissions(value = {"user:list","user:update"},logical = Logical.AND) //多个权限使用方式
public ListJsonVo<SysUserVo> list1(SysUserListReqVo reqVo) {
return JsonVos.ok(service.list(reqVo));
}
}
整体执行流程
推荐学习视频
这套视频感觉比尚硅谷和黑马的教程讲的要清晰一些。
点击查看【bilibili】