权限管理

权限管理:包括身份认证和授权两部分。对于需要访问控制的资源,用户首先经过身份认证,认证成功后具有某一资源访问权限才能访问。

认证

身份认证,就是判断一个用户是否为合法的用户,最常用的认证方式通过核对用户输入的用户名和密码,看是否和系统存储的用户名和密码一致,以此来判断用户的身份是否合法。常见的认证方式有:

  • 手机短信验证
  • 三方授权认证
  • 刷脸认证等
  • 微信扫码登录等
  • ……….

授权

控制谁能访问哪些资源。用户(主体)身份认证通过后,需要分配权限才能访问系统的资源,对于某些资源如果没有分配权限是不能访问的。可以理解为:主体(subject)对系统资源(resource)有什么权限(Permission)

资源(Resource)

如网站页面、目录、菜单、按钮等

权限(Permission)

一个主体能访问哪些资源

相关专业术语

subject

访问系统的用户,主体可以是用户,也可以是一台设备比如路由器、服务器、程序等,进行认证的都称为主体。

principle

身份信息,是主体(subject)证明自己身份的标识,标识必须有唯一性。如手机号、邮箱地址、指纹、密钥等。一个主体可以有多个身份信息,但必须有一个主身份(Primary Principal)

Credential

凭证信息,只有主体自己才知道的安全信息,如密码,私钥等。

权限管理流程

我们需要实现如下流程
Shiro - 图1

解决方案

怎么实现上图中的流程呢?行业常见的解决方案有2种:

  1. 使用Filter或SpringMVC的拦截器自己实现上面流程。
  2. 使用框架比如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 (以角色为基础的访问控制)

Snipaste_2022-01-27_20-41-11.png
用户跟角色挂钩、角色跟资源挂钩,不同的用户有不同的角色,不同的角色有不同的访问资源的权限。
设计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
Snipaste_2022-01-31_07-55-03.png

核心角色

  • SecurityManager:安全管理器,是 Shiro 架构的核心,充当一种“伞形”对象,协调其内部各个组件
  • Subject:主体,比如用户
  • Realm:相当于数据源,可以获取主体(subject)的权限信息,主体的权限信息、用户名等信息存储在Realm
  • Authenticator:认证器,从Realm里获取subject相关信息,对subject进行认证看一下是否为合法用户。

流程:当用户进行认证的时候,会找到SecurityManager,SecurityManager找到Authenticator,Authenticator去Realm里面读取用户数据,进行认证,后面有详细流程。
Snipaste_2022-01-31_12-21-50.png

  • Authorizer:授权器,从Realm里面获取subject相关信息,对subject进行授权验证,看一下subject有没有对某一个资源有操作的权限。

    1. 流程:当用户进行授权的时候,会找到SecurityManagerSecurityManager找到AuthorizerAuthorizerRealm里面读取用户数据,进行授权。<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=)

基本使用

  1. /*创建安全管理器*/
  2. DefaultSecurityManager securityManager = new DefaultSecurityManager();
  3. /*设置Realm,从配置文件里面读取Realm,Realm有很多种,我们真实的项目肯定是要从数据库里面读取数据,来放到Realm上*/
  4. securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
  5. SecurityUtils.setSecurityManager(securityManager);
  6. Subject subject = SecurityUtils.getSubject();
  7. String userName = "lisi";
  8. String passWord = "123456";
  9. UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,passWord);
  10. try {
  11. /*先传递一个简单的UsernamePasswordToken类型的AuthenticationToken*/
  12. subject.login(usernamePasswordToken);
  13. System.out.println(subject.isAuthenticated());//用户是否认证成功
  14. /*能来到这里说明上面认证没有问题,判断用户有没有某个权限*/
  15. Boolean isUserListPer = subject.isPermitted("user:list");
  16. /*判断用户是否有某个角色*/
  17. Boolean isAdminRole = subject.hasRole("admin");
  18. System.out.println(subject.isAuthenticated());
  19. System.out.println(isAdminRole);
  20. subject.logout(); //退出登录,代表该用户没有认证,此时查用户的权限、角色就查不到了
  21. System.out.println(subject.isAuthenticated());
  22. System.out.println(subject.hasRole("admin"));
  23. }catch (UnknownAccountException e){ //账号不存在
  24. System.out.println("账号不存在");
  25. }catch (IncorrectCredentialsException e){ //密码不正确
  26. System.out.println("密码不正确");
  27. }catch (AuthenticationException e){ //认证失败
  28. System.out.println("认证失败");
  29. }

Reaalm数据先从最简单的ini文件里面读取,shiro.ini文件数据如下

  1. # =============================================================================
  2. # Tutorial INI configuration
  3. #
  4. # Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
  5. # =============================================================================
  6. # -----------------------------------------------------------------------------
  7. # Users and their (optional) assigned roles
  8. # username = password, role1, role2, ..., roleN
  9. # -----------------------------------------------------------------------------
  10. [users]
  11. root = 123456, admin
  12. lisi = 123456, guest
  13. zhangsan = 123456,guest
  14. # -----------------------------------------------------------------------------
  15. # Roles with assigned permissions
  16. # roleName = perm1, perm2, ..., permN
  17. # -----------------------------------------------------------------------------
  18. [roles]
  19. admin = *
  20. guest = user:list,user:update

上面是最基本的使用过程,以后用户的的数据,比如用户名、密码、角色、权限是要从数据库里面读取的。这时Realm的数据来源就不能像上面的ini文件了,我们需要从数据库里面读取。
Shiro提供给我们很多Realm的实现类,如下图所示
image.png
其中JdbcRealm就是从数据库里面读取数据创建Realm的,但遗憾的是该类型Realm要有固定的表结构,如下表名需要是users,用户的密码字段需要是password

  1. public class JdbcRealm extends AuthorizingRealm {
  2. //TODO - complete JavaDoc
  3. /*--------------------------------------------
  4. | C O N S T A N T S |
  5. ============================================*/
  6. /**
  7. * The default query used to retrieve account data for the user.
  8. */
  9. protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
  10. /**
  11. * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
  12. */
  13. protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
  14. /**
  15. * The default query used to retrieve the roles that apply to a user.
  16. */
  17. protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
  18. /**
  19. * The default query used to retrieve permissions that apply to a particular role.
  20. */
  21. protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
  22. private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class);
  23. //.....
  24. }

通常我们会自定义Realm来满足我们的需求。

自定义Realm

为了自定义查询数据的方案,我们需要自定义Realm。
自定义类继承AuthorizingRealm 因为上面JdbcRealm也继承该类并且同时包含认证和授权。

  1. import lombok.Data;
  2. import org.apache.shiro.authc.*;
  3. import org.apache.shiro.authz.AuthorizationInfo;
  4. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  5. import org.apache.shiro.realm.AuthorizingRealm;
  6. import org.apache.shiro.subject.PrincipalCollection;
  7. public class CustomRealm extends AuthorizingRealm {
  8. /**
  9. * 当用户subject需要进行权限、角色等判断时,就会调用该方法比如:subject.isPermitted("user:list")、subject.hasRole("admin");
  10. * 开发者需要在此方法里面根据用户名查询用户的权限、角色信息
  11. * @param principals
  12. * @return 返回用户的权限、角色信息
  13. */
  14. @Override
  15. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  16. // String userName = principals.getPrimaryPrincipal();//根据用户名到数据库里面查询用户的角色以及权限信息
  17. SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
  18. simpleAuthorizationInfo.addRole("admin");
  19. simpleAuthorizationInfo.addStringPermission("user:list");
  20. return simpleAuthorizationInfo;
  21. }
  22. /**
  23. * 当主体(subject)进行认证的时候调用 subject.login(token);
  24. * 开发者需要在这里根据用户标识(如用户名)到数据库查询用户的相关信息
  25. * @param token:为subject.login(token)传进来的token
  26. * @return 返回用户的具体信息如用户名、密码
  27. * @throws AuthenticationException
  28. */
  29. @Override
  30. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  31. String userName = String.valueOf(token.getPrincipal()); //获取用户名
  32. String password = String.valueOf(token.getPrincipal()); //获取密码
  33. //根据用户名到数据库查找用户相关信息,这 里做个假数据
  34. User user = new User();
  35. user.setUserName("lishi");
  36. user.setPasWord("123456");
  37. if (user == null) return null; //返回null内部会自己抛出UnknownAccountException异常
  38. // if (user == null){
  39. // throw new UnknownAccountException();
  40. // }
  41. // if (!password.equals(user.getPasWord())){ //这个我们不需要自己判断,内部会自行判断
  42. // throw new IncorrectCredentialsException();
  43. // }
  44. //返回用户具体信息,传入数据库中查询到的用户名和密码,Realm内部会根据传来的token和数据库传来的用户名、密码进行对比
  45. return new SimpleAuthenticationInfo(user.getUserName(),user.getPasWord(),getName());
  46. }
  47. }
  48. @Data
  49. class User{
  50. private String userName;
  51. private String pasWord;
  52. }

外界使用

  1. /*创建安全管理器*/
  2. DefaultSecurityManager securityManager = new DefaultSecurityManager();
  3. /*自定义Realm*/
  4. securityManager.setRealm(new CustomRealm());
  5. SecurityUtils.setSecurityManager(securityManager);
  6. Subject subject = SecurityUtils.getSubject();
  7. String userName = "lisi";
  8. String passWord = "123456";
  9. UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,passWord);
  10. try {
  11. /*先传递一个简单的UsernamePasswordToken类型的AuthenticationToken*/
  12. subject.login(usernamePasswordToken);
  13. //到这里说明认证已经通过
  14. System.out.println("subject 是否有user:list权限 " + subject.isPermitted("user:list"));
  15. System.out.println("subject 是否有admin角色 " + subject.hasRole("admin"));
  16. }catch (UnknownAccountException e){ //账号不存在
  17. System.out.println("账号不存在");
  18. }catch (IncorrectCredentialsException e){ //密码不正确
  19. System.out.println("密码不正确");
  20. }catch (AuthenticationException e){ //认证失败
  21. System.out.println("认证失败");
  22. }

认证流程

image.png
当用户调用subject.login()后,SecurityManager会找到认证器Authenticator,Authenticator会找到我们自定义的CustomRealm,就会调用CustomRealm里面的如下方法

  1. /**
  2. * 当主体(subject)进行认证的时候调用 subject.login(token);
  3. * 开发者需要在这里根据用户标识(如用户名)到数据库查询用户的相关信息
  4. * @param token:为subject.login(token)传进来的token
  5. * @return 返回用户的具体信息
  6. * @throws AuthenticationException
  7. */
  8. @Override
  9. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  10. }

调用完上面的方法后会返回AuthenticationInfo。如果AuthenticationInfo没有值说明用户名没有就会报UnknownAccountException异常,如果有值就会调用Realm里面的assertCredentialsMatch方法

  1. public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  2. AuthenticationInfo info = getCachedAuthenticationInfo(token);
  3. if (info == null) {
  4. //otherwise not cached, perform the lookup:
  5. info = doGetAuthenticationInfo(token);
  6. log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
  7. if (token != null && info != null) {
  8. cacheAuthenticationInfoIfPossible(token, info);
  9. }
  10. } else {
  11. log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
  12. }
  13. if (info != null) { //注意如果有值就会来到这里
  14. assertCredentialsMatch(token, info);
  15. } else {
  16. log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
  17. }
  18. return info;
  19. }

�assertCredentialsMatch的方法内部是这样的

  1. protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
  2. CredentialsMatcher cm = getCredentialsMatcher();
  3. if (cm != null) {
  4. if (!cm.doCredentialsMatch(token, info)) {
  5. //not successful - throw an exception to indicate this:
  6. String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
  7. throw new IncorrectCredentialsException(msg);
  8. }
  9. } else {
  10. throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
  11. "credentials during authentication. If you do not wish for credentials to be examined, you " +
  12. "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
  13. }
  14. }

也就是会调用CredentialsMatcher里面的doCredentialsMatch方法来判断密码是否正确,下面接口实现类就是专门负责匹配密码是否正确的。

  1. public interface CredentialsMatcher {
  2. /**
  3. * Returns {@code true} if the provided token credentials match the stored account credentials,
  4. * {@code false} otherwise.
  5. *
  6. * @param token the {@code AuthenticationToken} submitted during the authentication attempt
  7. * @param info the {@code AuthenticationInfo} stored in the system.
  8. * @return {@code true} if the provided token credentials match the stored account credentials,
  9. * {@code false} otherwise.
  10. */
  11. boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info);//token为用户subject.login(token)传过来的,info为doGetAuthenticationInfo方法中我们返回的
  12. }

�不同的实现类有不同的密码匹配方式。为了满足项目中的需求我们通常也自定义CredentialsMatcher类自己实现密码匹配规则。

授权流程

当用户调用subject.isPermitted()或者 subject.hasRole()等权限、角色等方法时,SecurityManager会找到授权器Authorizer,Authorizer找到我们自定义的CustomRealm,调用CustomRealm的如下方法

  1. /**
  2. * 当用户subject需要进行权限、角色等判断时,就会调用该方法比如:subject.isPermitted("user:list")、subject.hasRole("admin");
  3. * 开发者需要在此方法里面根据用户名查询用户的权限、角色信息
  4. * @param principals
  5. * @return 返回用户的权限、角色信息
  6. */
  7. @Override
  8. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  9. // String userName = principals.getPrimaryPrincipal();//根据用户名到数据库里面查询用户的角色以及权限信息
  10. SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
  11. simpleAuthorizationInfo.addRole("admin");
  12. simpleAuthorizationInfo.addStringPermission("user:list");
  13. return simpleAuthorizationInfo;
  14. }

根据返回的AuthorizationInfo信息判断权限、角色是否正确。

SpringBoot集成Shiro

场景:用户通过用户名密码登录后,服务端会返回客户端一个token,后续的请求中携带token放在请求头中,服务端验证token是否为合法的token。
依赖配置

  1. <dependency>
  2. <groupId>org.apache.shiro</groupId>
  3. <artifactId>shiro-spring-boot-web-starter</artifactId>
  4. <version>1.7.0</version>
  5. </dependency>

日志这里使用logback

  1. <dependency>
  2. <groupId>ch.qos.logback</groupId>
  3. <artifactId>logback-classic</artifactId>
  4. <version>1.2.3</version>
  5. </dependency>

运行直接报如下错误

  1. No bean of type 'org.apache.shiro.realm.Realm' found.

需要把Realm加入到Spring容器里面

自定义CustomRealm

新建自定义CustomRealm类继承AuthorizingRealm

  1. import org.apache.shiro.authc.AuthenticationException;
  2. import org.apache.shiro.authc.AuthenticationInfo;
  3. import org.apache.shiro.authc.AuthenticationToken;
  4. import org.apache.shiro.authc.SimpleAuthenticationInfo;
  5. import org.apache.shiro.authz.AuthorizationInfo;
  6. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  7. import org.apache.shiro.realm.AuthorizingRealm;
  8. import org.apache.shiro.subject.PrincipalCollection;
  9. public class CustomRealm extends AuthorizingRealm {
  10. public CustomRealm() {
  11. super(new CustomMatcher()); //校验密码时,使用我们自定义的CredentialsMatcher去验证密码是否正确
  12. }
  13. @Override
  14. public boolean supports(AuthenticationToken token) {
  15. return token instanceof Token;
  16. }
  17. /**
  18. * 授权
  19. */
  20. @Override
  21. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  22. // 拿到token
  23. String token = (String) principals.getPrimaryPrincipal();
  24. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  25. //根据token或者用户ID到数据库查找用户的角色、权限信息,也可以提前把用户信息缓存起来
  26. // 添加角色
  27. info.addRole("xxx");
  28. // 添加权限
  29. info.addStringPermission("resource.getPermission()");
  30. return info;
  31. }
  32. /**
  33. * 认证:基于token的方案,能来到这里说明是合法的用户,已经登录过了并且token是有效的,直接保证该方法能认证通过即可
  34. */
  35. @Override
  36. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  37. Token tk = (Token) token;
  38. return new SimpleAuthenticationInfo(
  39. tk.getPrincipal(),
  40. tk.getCredentials(),
  41. getName());
  42. }
  43. }

CustomRealm加入Spring容器里面

Shiro配置文件

  1. import org.apache.shiro.realm.Realm;
  2. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
  3. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
  4. import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import javax.servlet.Filter;
  8. import java.util.HashMap;
  9. import java.util.LinkedHashMap;
  10. import java.util.Map;
  11. @Configuration
  12. public class ShiroCfg {
  13. @Bean
  14. public Realm realm() {
  15. return new CustomRealm();
  16. }
  17. /**
  18. * 在这里设置管理器、并且告诉Shiro如何进行拦截、拦截哪些URL、每个URL需要经过哪些Filter进行处理
  19. * @param realm
  20. * @return
  21. */
  22. @Bean
  23. public ShiroFilterFactoryBean shiroFilterFactoryBean(Realm realm) {
  24. ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
  25. // 设置管理器,在web项目里面直接这样配置即可
  26. DefaultWebSecurityManager mgr = new DefaultWebSecurityManager(realm);
  27. bean.setSecurityManager(mgr);
  28. // 添加我们自定义的Filter
  29. Map<String, Filter> filters = new HashMap<>();
  30. //添加自定义Filter并给Filter起个别名
  31. filters.put("customFilterxxx", new CustomFilter());//添加了一个Filter,Filter的名称为customFilterxxx。类似于anon对应的Filter为org.apache.shiro.web.filter.authc.AnonymousFilter
  32. //设置自定义Filter
  33. bean.setFilters(filters);
  34. // 配置拦截的路径对应的Filter
  35. Map<String, String> filterMap = new LinkedHashMap<>();//使用LinkedHashMap有顺序性,先加入的优先级最高
  36. //登录接口不需要拦截
  37. filterMap.put("/xx/login", "anon");//使用匿名Filter进行过滤,不需要登录也能访问
  38. //swagger接口文档不需要拦截
  39. filterMap.put("/swagger*/**", "anon");
  40. filterMap.put("/v2/api-docs/**", "anon");
  41. //测试接不需要拦截
  42. filterMap.put("/test/**", "anon");
  43. // 如果Filter里面发生异常就会来到我们自定义的ErrorController,这里面的URL也不应该被拦截
  44. filterMap.put("/error/filter", "anon");
  45. //所有其它的路径使用我们自定义的Filter拦截
  46. filterMap.put("/**", "customFilterxxx");
  47. bean.setFilterChainDefinitionMap(filterMap);
  48. return bean;
  49. }
  50. /**
  51. * 解决:发现只要方法上面加@RequiresPermissions注解后,很多接口都报404问题,加下面代码解决此问题
  52. */
  53. @Bean
  54. public DefaultAdvisorAutoProxyCreator proxyCreator() {
  55. DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
  56. proxyCreator.setUsePrefix(true);
  57. return proxyCreator;
  58. }
  59. }

Shiro提供了如下Filter供我们使用,Filter Name可以理解Filter的别名。

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
authcBearer org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter
invalidRequest org.apache.shiro.web.filter.InvalidRequestFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

自定义CredentialsMatcher

�自定义CredentialsMatcher来自己校验密码规则

  1. import org.apache.shiro.authc.AuthenticationInfo;
  2. import org.apache.shiro.authc.AuthenticationToken;
  3. import org.apache.shiro.authc.credential.CredentialsMatcher;
  4. public class CustomMatcher implements CredentialsMatcher {
  5. /**
  6. * @return true代表校验通过,直接返回true即可,因为我们是基于token的没有密码的概念
  7. */
  8. @Override
  9. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  10. return true;
  11. }
  12. }

自定义token

  1. public class Token implements AuthenticationToken {
  2. private final String token;
  3. public Token(String accessToken) {
  4. this.token = accessToken;
  5. }
  6. @Override
  7. public Object getPrincipal() {
  8. return token;
  9. }
  10. @Override
  11. public Object getCredentials() {
  12. return token;
  13. }
  14. }

自定义Filter

  1. import com.lff.dr.common.util.Strings;
  2. import org.apache.shiro.SecurityUtils;
  3. import org.apache.shiro.web.filter.AccessControlFilter;
  4. import javax.servlet.ServletRequest;
  5. import javax.servlet.ServletResponse;
  6. import javax.servlet.http.HttpServletRequest;
  7. /**
  8. * 自定义Filter验证用户是否合法
  9. */
  10. public class CustomFilter extends AccessControlFilter {
  11. public static final String TOKEN_HEADER = "Token";
  12. /**可以在这个方法里面初步判断是否允许请求通过
  13. * @return true会进入下一个链式调用(过滤器、拦截器、控制器),false就进入下面的onAccessDenied方法,交给shiro处理
  14. */
  15. @Override
  16. protected boolean isAccessAllowed(ServletRequest req,
  17. ServletResponse resp, Object o) throws Exception {
  18. return false;
  19. }
  20. /**
  21. * 上面的isAccessAllowed方法返回false,就会来到这里
  22. * @return true会进入下一个链式调用(过滤器、拦截器、控制器),false就不会进入
  23. */
  24. @Override
  25. protected boolean onAccessDenied(ServletRequest req,
  26. ServletResponse resp) throws Exception {
  27. // 获得请求
  28. HttpServletRequest request = (HttpServletRequest) req;
  29. // 获得token
  30. String token = request.getHeader("Token");
  31. // token为空
  32. if (Strings.isEmpty(token)) {
  33. throw new RuntimeException("token为空");
  34. }
  35. // 查找用户
  36. if (user == null) {
  37. throw new RuntimeException("token过期等");
  38. }
  39. // 鉴权,进入Realm,触发Realm的doGetAuthenticationInfo和doGetAuthorizationInfo方法,到数据库加载用户的角色、权限信息
  40. SecurityUtils.getSubject().login(new Token(token));
  41. // getSubject(req, resp).login(new Token(token));
  42. return true;
  43. }
  44. }


如果Filter里面有异常,比如CustomFilter里面抛出异常,Tomcat内部就会报500错误给客户端,我们的异常拦截器就会拦截不到这样的异常处理。下面是异常处理的示例代码,@RestControllerAdvice这样的注解只能拦截到Controller出现的异常。

  1. @RestControllerAdvice
  2. @Slf4j
  3. public class CommonExceptionHandler {
  4. @ExceptionHandler
  5. @ResponseStatus(code = HttpStatus.BAD_REQUEST)
  6. public JsonVO handleThrowable(Throwable t) {
  7. }
  8. }

�为了能够拦截到Filter里面的异常,我们单独搞一个ErrorFilter,所有的请求先来到这个ErrorFilter,ErrorFilter处理完之后再把请求传递到其它Filter(如:我们的CustomFilter)。如果其中某一个Filter发生异常,我们再转发给Controller(单独搞一个ErrorController),在ErrorController里面抛出异常,这样上面的异常处理拦截器就能拦截到异常,把异常信息返回到客户端了。
Shiro - 图7


ErrorFilter示例代码

  1. import javax.servlet.*;
  2. import java.io.IOException;
  3. public class ErrorFilter implements Filter {
  4. @Override
  5. public void doFilter(ServletRequest request,
  6. ServletResponse response,
  7. FilterChain chain) throws IOException, ServletException {
  8. try {
  9. chain.doFilter(request, response);
  10. } catch (Throwable e) {
  11. request.setAttribute("error_filter", e);
  12. request.getRequestDispatcher("/error/filter").forward(request, response);
  13. }
  14. }
  15. }

配置ErrorFilter为第一个处理请求的Filter

  1. @Configuration
  2. public class WebCfg implements WebMvcConfigurer {
  3. @Bean
  4. public FilterRegistrationBean<Filter> filterRegistrationBean() {
  5. FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
  6. bean.setFilter(new ErrorFilter());
  7. bean.addUrlPatterns("/*");
  8. // 最高权限
  9. bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
  10. return bean;
  11. }
  12. }

处理错误ErrorFilterController代码如下

  1. @RestController
  2. public class ErrorFilterController {
  3. @RequestMapping("/error/filter")
  4. public void handle(HttpServletRequest request) throws Throwable {
  5. throw (Throwable) request.getAttribute("error_filter");
  6. }
  7. }

外界直接用注解的方式使用即可

  1. @RestController
  2. @RequestMapping("/sysUsers")
  3. @Api(tags = "系统用户")
  4. public class SysUserController {
  5. @GetMapping
  6. @ApiOperation("分页查询")
  7. @RequiresPermissions("user:list") //表示该方法必须有user:list权限
  8. @RequiresRoles("admin") //表示该方法必须有admin角色
  9. public ListJsonVo<SysUserVo> list(SysUserListReqVo reqVo) {
  10. }
  11. @GetMapping
  12. @ApiOperation("分页查询")
  13. @RequiresPermissions(value = {"user:list","user:update"},logical = Logical.AND) //多个权限使用方式
  14. public ListJsonVo<SysUserVo> list1(SysUserListReqVo reqVo) {
  15. return JsonVos.ok(service.list(reqVo));
  16. }
  17. }

整体执行流程

Shiro - 图8

推荐学习视频

这套视频感觉比尚硅谷和黑马的教程讲的要清晰一些。
点击查看【bilibili】