https://github.com/apache/shiro

B站教程文档:https://www.yuque.com/chengxuyuanyideng/xkiu8l


简述

Apache Shiro是一个开源安全框架,可以处理身份验证授权企业级会话管理加密

其有四大安全基石:

  1. Authentication:认证,有时称为登录,即证明登录者是它们所说的身份的行为;

  2. Authorization:授权,即确定可以访问什么

  3. Session Management:会话管理,管理特定用户的会话;

  4. Cryptography:加密,使用加密算法确保数据安全;

其有三大概念:

  1. Subject:主题,类似于当前用户;

  2. Shiro SecurityManager:安全管理器,Shiro的核心,管理所有主题;

  3. Realm:域,访问数据,充当Shiro和APP安全数据之间的桥梁;

其架构图如下:


快速开始

引入依赖

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>2.0.0</version>
    </dependency>
​
    <!-- 日志依赖 -->
    <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.3.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>

编写日志配置

log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p %c{1}:%L - %m%n
​
# package logger level
log4j.logger.org.apache=ERROR
log4j.logger.org.apache.shiro=ERROR
​
# disable detail logging
log4j.logger.org.apache.shiro.util.ThreadContext=ERROR
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=ERROR

编写Shiro配置

模拟数据库

# -----------------------------------------------------------------------------
# Users and their assigned roles
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
# -----------------------------------------------------------------------------
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
​
​
# -----------------------------------------------------------------------------
# Roles with assigned permissions
# 
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 角色权限 *匹配所有权限
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

编写主类

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.env.BasicIniEnvironment;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
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) {
        // 1. 加载ini配置文件(类似于数据库,存储用户、角色、权限等信息)并获得SecurityManager,用来管理Subject(当前用户)
        SecurityManager securityManager =
                new BasicIniEnvironment("classpath:shiro.ini").getSecurityManager();
​
        // 2. 添加到上下文变量中(静态变量,全局通用唯一)
        SecurityUtils.setSecurityManager(securityManager);
​
        // 3. 获取当前Subject(当前用户),Subject使用的是ThreadLocal
        Subject currentUser = SecurityUtils.getSubject();
​
        // 4. 对当前Subject的会话进行操作
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [{}]", value);
        }
​
        // 5. 判断是否登录,如果没有登录,则进行登录
        if (!currentUser.isAuthenticated()) {
            // 5.1 设置要登录的用户名和密码
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true); // 记住我
​
            // 5.2 登录
            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 {} was incorrect!", token.getPrincipal());
            } catch (LockedAccountException lae) { // 账户锁定
                log.info("The account for username {} is locked.  Please contact your administrator to unlock it.",
                        token.getPrincipal());
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }
​
        // 6. 登录成功后,打印当前Subject的身份(这里是用户名)
        log.info("User [{}] logged in successfully.", currentUser.getPrincipal());
​
        // 7. 检查当前Subject是否有某个角色
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }
​
        // 8. 检查当前Subject是否有某个权限(非实例级别)
        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.");
        }
​
        // 9. 检查当前Subject是否有某个权限(实例级别)
        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!");
        }
​
        // 10. 退出登录
        currentUser.logout();
​
        System.exit(0);
    }
}

运行输出


身份验证

简述

即在应用中谁能证明他就是他本人,在shiro中使用身份证明来进行身份验证:

  • principals:身份,即主体的标识属性,一个主体可以有多个principals,但是只有一个primary principals

  • credentials:证明/凭证,即只有主体知道的保密值,如,密码

流程

  1. 创建安全管理器并获取当前Subject

    SecurityManager securityManager = ...;
    SecurityUtils.setSecurityManager(securityManager);
    Subject currentUser = SecurityUtils.getSubject();
  2. 判断当前Subject是否登录

    currentUser.isAuthenticated() // true为已登录,反之亦然
  3. 使用身份证明创建一个用来登录的令牌

    UsernamePasswordToken token = new UsernamePasswordToken("用户名", "密码");
    token.setRememberMe(true); // 记住我 模式
    ​
    token.getPrincipal(); // 获取用来登录的身份
    token.getCredentials(); // 获取用来登录的凭证
  4. 进行登录

    currentUser.login(token);

    登录失败会抛出异常

    异常名称

    异常说明

    DisabledAccountException

    禁用的账号

    LockedAccountException

    锁定的账号

    UnknownAccountException

    未知账号

    ExcessiveAttemptsException

    登录失败次数过多

    IncorrectCredentialsException

    错误的凭证

    ExpiredCredentialsException

    过期的凭证

    关于账号和密码错误,在返回时必须为 “用户名或密码错误” ,以防止暴力破解

  5. 退出登录

    currentUser.logout();

Realm

域,shiro从Realm中获取安全数据(如用户、角色、权限等),即SecurityManager要验证用户身份,那么它需要从Realm获取对应的用户信息进行比对才能确认用户是否合法。

可以简单的将其看作安全数据源,之前的shiro.ini就相当于一个数据源

自定义Realm

实现接口Realm

package com.xlyo.realm;
​
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.Realm;
​
public class MyRealm implements Realm {
    @Override
    public String getName() {
        return "my-realm";
    }
​
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }
​
    /**
     * 认证方法
     */
    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        // 1. 获取身份标识
        String username = (String) token.getPrincipal();
​
        // 2. 获取凭证
        String password = new String((char[]) token.getCredentials());
​
        // 3. 进行验证(这里为模拟数据)
        if (!"admin".equals(username) || !"123456".equals(password)) {
            throw new UnknownAccountException();
        }
​
        // 4. 认证成功之后返回
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

配置文件

之后在ini配置文件里配置使用到的Realm类

# 声明一个Realm
myRealm=com.xlyo.realm.MyRealm
# 指定使用该Realm
securityManager.realms=$myRealm

就可以使用自定义的Realm了

多Realm配置

默认只要有其中一个Realm认证通过,就算通过

# 声明多个Realm
myRealm1=com.xlyo.realm.MyRealm1
myRealm2=com.xlyo.realm.MyRealm2
# 指定使用Realm
securityManager.realms=$myRealm1,$myRealm2

JdbcRealm

连接MySQL获取安全数据

数据表

drop database if exists shirodb;
create database shirodb;
use shirodb;
​
create table users (
                       id bigint auto_increment,
                       username varchar(100),
                       password varchar(100),
                       password_salt varchar(100),
                       constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_users_username on users(username);
​
create table user_roles(
                           id bigint auto_increment,
                           username varchar(100),
                           role_name varchar(100),
                           constraint pk_user_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_user_roles on user_roles(username, role_name);
​
create table roles_permissions(
                                  id bigint auto_increment,
                                  role_name varchar(100),
                                  permission varchar(100),
                                  constraint pk_roles_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_roles_permissions on roles_permissions(role_name, permission);
​
insert into users(username,password)values('zhangsan','123456');

配置文件

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.cj.jdbc.Driver
dataSource.url=jdbc:mysql://192.168.102.131:3306/shirodb
dataSource.username=root
dataSource.password=123456
​
# 设置数据源和Realm
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm

测试方法

@Test
public void testJDBCRealm() {
    testRealm("shiro-jdbc-realm.ini", "zhangsan", "123456");
}
​
private void testRealm(String fileName, String username, String password) {
    SecurityManager securityManager =
        new BasicIniEnvironment("classpath:" + fileName).getSecurityManager();
    SecurityUtils.setSecurityManager(securityManager);
    Subject currentUser = SecurityUtils.getSubject();
​
    if (!currentUser.isAuthenticated()) {
        UsernamePasswordToken token =
            new UsernamePasswordToken(username, password);
​
        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 {} was incorrect!", token.getPrincipal());
        } catch (LockedAccountException lae) { // 账户锁定
            log.info("The account for username {} is locked.  Please contact your administrator to unlock it.",
                     token.getPrincipal());
        }
        catch (AuthenticationException ae) {}
    }
​
    if (currentUser.isAuthenticated()) {
        // 登录成功后,打印当前Subject的身份(这里是用户名)
        log.info("User [{}] logged in successfully.", currentUser.getPrincipal());
​
        currentUser.logout();
    }
}

验证策略

简述

用于多Realm时,shiro提供多种验证策略,如,Realm全部通过,至少一个通过等等

  1. FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;

  2. AtLeastOneSuccessfulStrategy【默认策略】:只要有一个 Realm 验证成功即可,和FirstSuccessfulStrategy不同,返回所有 Realm 身份验证成功的认证信息;

  3. AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。

SecurityManager接口继承了Authenticator,该类为ModularRealmAuthenticator,默认使用AtLeastOneSuccessfulStrategy 策略。

配置文件

#指定securityManager的authenticator实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#指定securityManager.authenticator的authenticationStrategy
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
​
myRealm1=com.realm.MyRealm1
myRealm2=com.realm.MyRealm2
myRealm3=com.realm.MyRealm3
securityManager.realms=$myRealm1,$myRealm3

授权

简述

  • 授权:也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。

    在授权中需了解的几个关键对象:主体Subject)、资源Resource)、权限Permission)、角色Role)。

  • 主体:即访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。

  • 资源:在应用中用户可以访问的URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只有授权后才能访问。

  • 权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如: 访问用户列表页面 查看/新增/修改/删除用户数据(即很多时候都是 CRUD(增查改删)式权限控制)打印文档等。如上可以看出,权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro 不会去做这件事情,而是由实现人员提供


权限控制颗粒度

Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的)

  • 角色:角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。

  • 隐式角色: 即直接通过角色来验证用户有没有操作权限,如在应用中 CTO、技术总监、开发工程师可以使用打印机,假设某天不允许开发工程师使用打印机,此时需要从应用中删除相应代码;再如在应用中 CTO、技术总监可以查看用户、查看权限;突然有一天不允许技术总监查看用户、查看权限了,需要在相关代码中把技术总监角色从判断逻辑中删除掉;即粒度是以角色为单位进行访问控制的,粒度较粗;如果进行修改可能造成多处代码修改。

  • 显示角色: 在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设哪个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无须修改多处代码;即粒度是以资源/实例为单位的;粒度较细。


RBAC

基于角色

Role-Based Access Control:每个用户拥有不同的角色,进而拥有不同的权限,控制的范围(粒度)更大

基于资源

Resource-based access control:每个用户直接拥有资源来实现权限控制。控制的控制的范围(粒度)更小


授权方式

shiro支持三种授权方式

1.编程式

通过写 if/else 授权代码块完成:如果没有权限则执行不到对应的内容

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
    //有权限
} else {
    //无权限
}

2.注解式

通过在执行的 Java 方法上放置相应的注解完成:如果没有权限则不能访问,并且将抛出相应的异常

@RequiresRoles("admin")
public void hello() {
    //有权限
}

3.标签

JSPthymeleaf等模板中 页面通过相应的标签完成:如果没有权限则看不到对应的内容

<shiro:hasRole name="admin">
<!— 有权限 —>
</shiro:hasRole>

权限配置格式

资源标识符:操作:对象实例 ID
  • :表示资源/操作/实例的分割

  • ,表示操作的分割

  • *表示任意资源/操作/实例

单个资源单个权限

subject().checkPermissions("system:user:update");

用户拥有资源system:userupdate权限

单个资源多个权限

role41=system:user:update,system:user:delete
subject().checkPermissions("system:user:update", "system:user:delete");

用户拥有资源system:userupdatedelete权限

如上可以简写成:

role42="system:user:update,delete"
subject().checkPermissions("system:user:update,delete");

单个资源全部权限

role52=system:user:*

也可以简写为(推荐上边的写法):

role53=system:user

进行判断:

subject().checkPermissions("system:user:*");
subject().checkPermissions("system:user");

所有资源单个权限

role61=*:view
subject().checkPermissions("user:view");

用户拥有所有资源的view所有权限。

假设判断的权限是system:user:view,那么需要role5=::view这样写才行

实例级别的权限

  • 单个实例单个权限

    对user实例1拥有view权限

    role71=user:view:1
    subject().checkPermissions("user:view:1");
  • 单个实例多个权限

    对user实例1拥有updatedelete权限

    role72="user:update,delete:1"
    subject().checkPermissions("user:delete,update:1");
    subject().checkPermissions("user:update:1", "user:delete:1");
  • 单个实例所有权限

    对user实例1拥有所有权限

    role73=user:*:1
    subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");
  • 所有实例单个权限

    对user所有实例拥有单个权限

    role74=user:auth:*
    subject().checkPermissions("user:auth:1", "user:auth:2");
  • 所有实例所有权限

    对user所有实例拥有所有权限

    role75=user:*:*
    subject().checkPermissions("user:view:1", "user:auth:2");

WildcardPermission

如下两种方式是等价的:

subject().checkPermission("menu:view:1");
subject().checkPermission(new WildcardPermission("menu:view:1"));

性能问题

通配符匹配方式比字符串相等匹配来说是更复杂的,因此需要花费更长时间,但是一般系统的权限不会太多,且可以配合缓存来提供其性能,如果这样性能还达不到要求我们可以实现位操作算法实现性能更好的权限匹配。

另外实例级别的权限验证如果数据量太大也不建议使用,可能造成查询权限及匹配变慢。可以考虑比如在sql查询时加上权限字符串之类的方式在查询时就完成了权限匹配。


角色判断

前置

# 定义用户及其密码和角色
[users]
zhangsan=123456,admin,manager
lisi=123456,guest
​
# 定义角色
[roles]
admin=*
manager=user:create,user:delete,user:update
guest=user:view
@AfterAll
public static void logOut() {
    SecurityUtils.getSubject().logout();
}
​
private Subject getSubject() {
    return SecurityUtils.getSubject();
}
​
private void testLogin(String fileName, String username, String password) {
    SecurityManager securityManager =
        new BasicIniEnvironment("classpath:" + fileName).getSecurityManager();
    SecurityUtils.setSecurityManager(securityManager);
    Subject currentUser = SecurityUtils.getSubject();
​
    if (!currentUser.isAuthenticated()) {
        UsernamePasswordToken token =
            new UsernamePasswordToken(username, password);
        //            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 {} was incorrect!", token.getPrincipal());
        } catch (LockedAccountException lae) { // 账户锁定
            log.info("The account for username {} is locked.  Please contact your administrator to unlock it.",
                     token.getPrincipal());
        }
        catch (AuthenticationException ae) {}
    }
​
    if (currentUser.isAuthenticated()) {
        // 登录成功后,打印当前Subject的身份(这里是用户名)
        log.info("User [{}] logged in successfully.", currentUser.getPrincipal());
    }
}
​
testLogin("shiro.ini", "zhangsan", "123456");

有某个角色

if (getSubject().hasRole("admin")) {
    log.info("当前用户有admin角色");
} else {
    log.info("当前用户没有admin角色");
}

有集合内的角色

全部匹配才能通过

List<String> roles = Arrays.asList("admin", "manager");
if (getSubject().hasAllRoles(roles)) {
    log.info("当前用户有角色 => {}", roles);
} else {
    log.info("当前用户没有角色 => {}", roles);
}

有集合内的角色,返回布尔数组

boolean[] hasRoles = getSubject().hasRoles(roles);
Stream.of(hasRoles).forEach(b -> log.info("当前用户有角色 => {}", b));

检查有某个角色

try {
    getSubject().checkRole("admin");
} catch (AuthorizationException e) {
    log.info("当前用户没有admin角色");
}

检查有多个角色

全部匹配才能通过

try {
    getSubject().checkRoles("admin", "guest");
} catch (AuthorizationException e) {
    log.info("当前用户没有给定的集合角色");
}

权限判断

前置

# 定义用户及其密码和角色
[users]
zhangsan=123456,admin,manager
dba=123456,manager
lisi=123456,guest
​
# 定义角色
[roles]
admin=*
manager=user:create,user:delete,user:update
guest=user:view

... // 跟角色判断的前置相同
testLogin("shiro.ini", "dba", "123456");

有某个权限

if (getSubject().isPermitted("user:create")) {
    log.info("当前用户有user:create权限");
} else {
    log.info("当前用户没有user:create权限");
}

有集合内的权限

全部匹配才能通过

if (getSubject().isPermittedAll("user:create", "user:delete", "user:view")) {
    log.info("当前用户有user:create, user:delete, user:view权限");
} else {
    log.info("当前用户没有user:create, user:delete, user:view权限");
}

有集合内的权限,返回布尔数组

boolean[] hasPermissions = getSubject().isPermitted("user:create", "user:delete", "user:view");
Stream.of(hasPermissions).forEach(b -> log.info("当前用户有的权限 => {}", b));

检查有某个权限

try {
    getSubject().checkPermission("user:create");
} catch (AuthorizationException e) {
    log.info("当前用户没有user:create权限");
}

检查有多个权限

全部匹配才能通过

try {
    getSubject().checkPermissions("user:create", "user:delete", "user:view");
} catch (AuthorizationException e) {
    log.info("当前用户没有给定集合所有的权限");
}

自定义授权

继承AuthorizingRealm创建自定义Realm

package com.xlyo.realm;
​
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;
​
import java.util.Arrays;
import java.util.List;
​
public class MyRealm extends AuthorizingRealm {
    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        // 从token中获取用户名和密码
        String username = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());
​
        // 验证用户名和密码
        if (!"admin".equals(username) || !"123456".equals(password)) {
            throw new AuthenticationException("用户名或密码错误!");
        }
​
        return new SimpleAuthenticationInfo(username, password, getName());
    }
    
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 需要先根据principals获取用户名,再根据用户名查询数据库获取角色和权限
        // Object username = principals.getPrimaryPrincipal(); // 获取主要身份
​
        // 这里假设角色和权限都是从数据库中查询出来的
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        List<String> roles = Arrays.asList("admin", "user");
        List<String> permissions = Arrays.asList("user:add", "user:delete", "user:update");
​
        info.addRoles(roles);
        info.addStringPermissions(permissions);
​
        return info;
    }
}

不要忘了配置文件

myRealm=com.xlyo.realm.MyRealm
securityManager.realms=$myRealm

JDBC授权

具体参考JdbcRealm

数据表

-- ----------------------------
-- Table structure for roles_permissions
-- ----------------------------
DROP TABLE IF EXISTS `roles_permissions`;
CREATE TABLE `roles_permissions` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `role_name` varchar(100) DEFAULT NULL,
  `permission` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_roles_permissions` (`role_name`,`permission`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3;
​
-- ----------------------------
-- Records of roles_permissions
-- ----------------------------
INSERT INTO `roles_permissions` VALUES ('1', 'role1', '+user1+10');
INSERT INTO `roles_permissions` VALUES ('3', 'role1', '+user2+10');
INSERT INTO `roles_permissions` VALUES ('2', 'role1', 'user1:*');
INSERT INTO `roles_permissions` VALUES ('4', 'role1', 'user2:*');
​
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  `password_salt` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_users_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3;
​
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('2', 'zhangsan', '123456', null);
​
-- ----------------------------
-- Table structure for user_roles
-- ----------------------------
DROP TABLE IF EXISTS `user_roles`;
CREATE TABLE `user_roles` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `role_name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_roles` (`username`,`role_name`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3;
​
-- ----------------------------
-- Records of user_roles
-- ----------------------------
INSERT INTO `user_roles` VALUES ('1', 'zhangsan', 'role1');
INSERT INTO `user_roles` VALUES ('2', 'zhangsan', 'role2');

配置文件

#自定义realm 一定要放在securityManager.authorizer赋值之后(因为调用setRealms会将realms设置给authorizer,并给各个Realm设置permissionResolver和rolePermissionResolver)
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.cj.jdbc.Driver
dataSource.url=jdbc:mysql://192.168.102.131:3306/shirodb
dataSource.username=root
dataSource.password=123456
​
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm

测试方法

@Test
public void testIsPermitted() {
    testLogin("classpath:shiro-authorizer.ini", "zhangsan", "123456");
    
    // 判断拥有权限
    Assertions.assertTrue(subject().isPermitted("user1:update"));
    Assertions.assertTrue(subject().isPermitted("user2:update"));
}

编码加密

散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。

一般进行散列时最好提供一个 salt(盐),再加一些只有系统知道的干扰数据,如用户名和 ID(即盐),这样散列的对象是 “密码 + 用户名 +ID”,这样生成的散列值相对来说更难破解。

现在shiro不提供MD5算法,只提供SHA-256SHA-384SHA-512和一些对称式加密算法

package com.xlyo;
​
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.hash.Sha512Hash;
import org.junit.jupiter.api.Test;
​
@Slf4j
public class CryptoTest {
​
    @Test
    public void testHash256() {
        String str = "hello";
        String salt = "123";
        String sha = new Sha256Hash(str, salt, 1024).toString(); // 1024为散列次数
        log.info("Sha256 Hash: {}", sha);
    }
​
    @Test
    public void testHash512() {
        String str = "hello";
        String salt = "123";
        String sha = new Sha512Hash(str, salt, 1024).toString();
        log.info("Sha512 Hash: {}", sha);
    }
}

哈希密码验证

使用HashedCredentialsMatcher可以对哈希的密码进行校验

1.自定义Realm
package com.xlyo.realm;
​
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.crypto.hash.Sha256Hash;
import org.apache.shiro.lang.util.ByteSource;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
​
/**
 * 自定义Sha256加密算法Realm,(密码+盐值)*散列次数
 */
public class MySha256Realm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
​
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        // 1. 从token中获取用户名
        String username = (String) token.getPrincipal();
​
        // 2. 从数据库中查询盐值和用户密码
        String salt = "admin"; // 模拟从数据库中查询盐值
        String dbPwd = getPassword(username);
​
        // 3. 返回SimpleAuthenticationInfo对象,包含用户名和密码
        return new SimpleAuthenticationInfo(username, dbPwd, ByteSource.Util.bytes(salt), getName());
    }
​
    // 从数据库中查询用户密码
    private String getPassword(String username) {
        String str = "123456";
        String salt = "admin";
        String sha = new Sha256Hash(str, salt, 1024).toString();
​
        // 模拟从数据库中查询用户密码
        return sha;
    }
}
2.测试方法
package com.xlyo;
​
import com.xlyo.realm.MySha256Realm;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.jupiter.api.Test;
​
@Slf4j
public class TestPwd {
​
    @Test
    public void testPwd() {
        // 测试登录
        testLogin("admin", "123456");
    }
​
    private void testLogin(String username, String password) {
        // 初始化SecurityManager
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
​
        // 自定义Realm设置自定义加密算法
        MySha256Realm realm = new MySha256Realm();
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("sha-256"); // 设置加密算法
        matcher.setHashIterations(1024); // 设置散列次数
        realm.setCredentialsMatcher(matcher);
​
        // 设置Realm
        securityManager.setRealm(realm);
​
        // 设置上下securityManager
        SecurityUtils.setSecurityManager(securityManager);
        Subject currentUser = SecurityUtils.getSubject();
​
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token =
                    new UsernamePasswordToken(username, password);
            //            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 {} was incorrect!", token.getPrincipal());
            } catch (LockedAccountException lae) { // 账户锁定
                log.info("The account for username {} is locked.  Please contact your administrator to unlock it.",
                        token.getPrincipal());
            }
            catch (AuthenticationException ae) {
                log.info("Authentication failed {}", ae.getMessage());
            }
        }
​
        if (currentUser.isAuthenticated()) {
            // 登录成功后,打印当前Subject的身份(这里是用户名)
            log.info("User [{}] logged in successfully!", currentUser.getPrincipal());
        } else {
            log.info("User [{}] failed to log in.", username);
        }
    }
}
核心代码
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("sha-256"); // 设置加密算法
matcher.setHashIterations(1024); // 设置散列次数

密码重试次数限制

继承HashedCredentialsMatcher即可实现自定义密码重试次数限制

自定义Mather

这里使用Ehcache做缓存,也可以替换为Redis

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.6</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
​
    <diskStore path="java.io.tmpdir"/>
​
    <!-- 登录记录缓存 锁定10分钟 -->
    <cache name="passwordRetryCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>
​
</ehcache>

以下代码逻辑比较简单,即如果密码输入正确清除 cache 中的记录;否则 cache 中的重试次数 +1,如果超出 5 次那么抛出异常表示超出重试次数了

package com.hash.credentials;
​
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
​
import javax.security.auth.login.AccountLockedException;
import java.util.concurrent.atomic.AtomicInteger;
​
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
    private Ehcache passwordRetryCache;
​
    public RetryLimitHashedCredentialsMatcher() {
        CacheManager cacheManager = CacheManager.newInstance(CacheManager.class.getClassLoader().getResource("ehcache.xml"));
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }
​
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        String username = (String)token.getPrincipal();
        //retry count + 1
        Element element = passwordRetryCache.get(username);
        if(element == null) {
            element = new Element(username , new AtomicInteger(0));
            passwordRetryCache.put(element);
        }
        AtomicInteger retryCount = (AtomicInteger)element.getObjectValue();
        if(retryCount.incrementAndGet() > 5) {
            //if retry count > 5 throw
            throw new ExcessiveAttemptsException();
        }
​
        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            //clear retry count
            passwordRetryCache.remove(username);
        }
        return matches;
    }
}

测试代码

@Test
public void testRetryLimitHashedCredentialsMatcherWithMyRealm() {
    Assertions.assertThrows(ExcessiveAttemptsException.class, ()->{
        for(int i = 1; i <= 5; i++) {
            try {
                testLogin("liu", "234");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        testLogin("liu", "234");
    });
}

Spring Boot

Shiro 提供了与 Web 集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的 URL,然后进行相应的控制

ShiroFilter类似于如 Struts2/SpringMVC 这种 web 框架的前端控制器,其是安全控制的入口点,其负责读取配置(如 ini 配置文件),然后判断 URL 是否需要登录 / 权限等工作

整合

添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
​
    <groupId>com.xlyo</groupId>
    <artifactId>ShiroLearnBoot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ShiroLearnBoot</name>
    <description>Shiro Learn Springboot</description>
​
    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
​
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
​
        <!-- Shiro安全框架 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <classifier>jakarta</classifier>
            <version>2.0.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.shiro</groupId>
                    <artifactId>shiro-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.shiro</groupId>
                    <artifactId>shiro-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <classifier>jakarta</classifier>
            <version>2.0.0</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-collections</groupId>
                    <artifactId>commons-collections</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <classifier>jakarta</classifier>
            <version>2.0.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.shiro</groupId>
                    <artifactId>shiro-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
​
​
        <!-- 数据库 -->
        <!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.2.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </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>
    </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>
</project>

SQL语句

该示例将权限和角色都整合到一张账号表中,实际为分开存储,详细参考Shiro五表

CREATE TABLE `account` (
                           `id` int NOT NULL AUTO_INCREMENT,
                           `username` varchar(20) DEFAULT NULL,
                           `password` varchar(20) DEFAULT NULL,
                           `perms` varchar(20) DEFAULT NULL,
                           `role` varchar(20) DEFAULT NULL,
                           PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
​
INSERT INTO `account` (`id`, `username`, `password`, `perms`, `role`) VALUES ('1', 'zs', '123123', '', '');
INSERT INTO `account` (`id`, `username`, `password`, `perms`, `role`) VALUES ('2', 'ls', '123123', 'user:manage', '');
INSERT INTO `account` (`id`, `username`, `password`, `perms`, `role`) VALUES ('3', 'ww', '123123', 'user:manage,make', 'admin');

应用配置

spring:
  application:
    name: ShiroLearnBoot
  datasource:
    url: jdbc:mysql://192.168.102.131:3306/shiro?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
​
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

实体类

domain/entity

package com.xlyo.domain.entity;
​
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
​
@TableName("account")
@Data
public class Account {
    private Integer id;
    private String username;
    private String password;
    private String perms;
    private String role;
}

Mapper类

mapper

package com.xlyo.mapper;
​
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xlyo.domain.entity.Account;
import org.apache.ibatis.annotations.Mapper;
​
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
}

业务层

service

package com.xlyo.service;
​
import com.baomidou.mybatisplus.extension.service.IService;
import com.xlyo.domain.entity.Account;
​
public interface IAccountService extends IService<Account> {
    Account findByUsername(String username);
}

service/impl

package com.xlyo.service.impl;
​
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xlyo.domain.entity.Account;
import com.xlyo.mapper.AccountMapper;
import com.xlyo.service.IAccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
​
@Service
@RequiredArgsConstructor
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {
    private final AccountMapper accountMapper;
​
    @Override
    public Account findByUsername(String username) {
        QueryWrapper<Account> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        return accountMapper.selectOne(wrapper);
    }
}

Realm

基础的自定义Realm,从数据库取数据

realm

package com.xlyo.realm;
​
import com.xlyo.domain.entity.Account;
import com.xlyo.service.IAccountService;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.SecurityUtils;
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;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
​
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
​
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private IAccountService accountService;
​
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取当前登录对象
        Subject subject = SecurityUtils.getSubject();
        Account account = (Account) subject.getPrincipal();
​
        // 设置角色
        Set<String> roles = new HashSet<>();
        roles.add(account.getRole());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
​
        // 设置权限
        info.addStringPermission(account.getPerms());
        return info;
    }
​
    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
            throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        Account account = accountService.findByUsername(token.getUsername());
​
        return Optional
                .ofNullable(account)
                // 这里交给SimpleAuthenticationInfo校验密码是否正确
                .map(acc -> new SimpleAuthenticationInfo(acc, acc.getPassword(), getName()))
                .orElse(null);
    }
}

Shiro配置(重点)

config

package com.xlyo.config;
​
import com.xlyo.realm.MyRealm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
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;
​
@Configuration
public class ShiroConfig {
​
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(
            @Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
​
        // 创建要拦截的路径
        // <要拦截的路径,过滤器> https://blog.csdn.net/qq_37840993/article/details/107693337
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/about", "anon");
        filterMap.put("/account/**", "authc");
        filterMap.put("/manage", "perms[user:manage]");
        filterMap.put("/admin", "authc, roles[admin]");
​
        // 设置拦截路径
        shiroFilter.setFilterChainDefinitionMap(filterMap);
​
        // 设置登录页面
        shiroFilter.setLoginUrl("/login");
​
        // shiroFilter.setGlobalFilters(List.of("noSessionCreation")); // 设置无状态服务(禁用会话)
        return shiroFilter;
    }
​
    @Bean
    public MyRealm myRealm() {
        return new MyRealm();
    }
​
    /**
     * 配置securityManager的实现类,变向的配置了securityManager
     * @param myRealm 自定义的Realm
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm);
        return manager;
    }
​
    /**
     * 通过调用Initializable.init()和Destroyable.destroy()方法,从而去管理shiro bean生命周期
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
​
    /**
     * 开启shiro权限注解
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

Controller

package com.xlyo.controller;
​
import com.xlyo.service.IAccountService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
​
@RestController
public class MyController {
​
    private final IAccountService accountService;
​
    public MyController(IAccountService accountService) {
        this.accountService = accountService;
    }
​
    @GetMapping("/about")
    public String about() {
        return "This is a sample Spring Boot application.";
    }
​
    @GetMapping("/account/{username}")
    public String account(@PathVariable String username) {
        return accountService.findByUsername(username).toString();
    }
​
    @GetMapping("/manage")
    public String manage() {
        return "This is the management page.";
    }
​
    @GetMapping("/admin")
    public String admin() {
        return "This is the admin page.";
    }
​
    @RequiresPermissions({"user:make"})
    @GetMapping("/user")
    public String user() {
        return "This is the user page.";
    }
​
    @GetMapping("/login")
    public String login(@RequestParam("un") String username, @RequestParam("pwd") String password) {
        Subject currentUser = SecurityUtils.getSubject();
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
​
            try {
                currentUser.login(token);
            } catch (AuthenticationException e) {
                return "Login failed.";
            }
​
            return "Login success.";
        } else {
            return "You are already logged in.";
        }
    }
​
    @GetMapping("/logout")
    public String logout() {
        Subject currentUser = SecurityUtils.getSubject();
        if (currentUser.isAuthenticated()) {
            currentUser.logout();
            return "Logout success.";
        } else {
            return "You are not logged in.";
        }
    }
}

过滤器

Shiro 内置了很多默认的过滤器(拦截器),比如身份验证、授权等相关的。默认拦截器可以参考 org.apache.shiro.web.filter.mgt.DefaultFilter 中的枚举拦截器:

默认拦截器名

拦截器类

说明(括号里的表示默认值)

身份验证相关的

authc

org.apache.shiro.web.filter.authc.FormAuthenticationFilter

基于表单的拦截器;如 “/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure);

authcBasic

org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);

logout

org.apache.shiro.web.filter.authc.LogoutFilter

退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/); 示例 “/logout=logout”

user

org.apache.shiro.web.filter.authc.UserFilter

用户拦截器,用户已经身份验证 / 记住我登录的都可;示例 “/**=user”

anon

org.apache.shiro.web.filter.authc.AnonymousFilter

匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 “/static/**=anon”

授权相关的

roles

org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]”

perms

org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms["user:create"]”

port

org.apache.shiro.web.filter.authz.PortFilter

端口拦截器,主要属性:port(80):可以通过的端口;示例 “/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样

rest

org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read,POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例 “/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete” 权限字符串进行权限匹配(所有都得匹配,isPermittedAll);

ssl

org.apache.shiro.web.filter.authz.SslFilter

SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样;

其他

noSessionCreation

org.apache.shiro.web.filter.session.NoSessionCreationFilter

不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常;


拦截器

shiro的拦截器实际上就是过滤器

简述

Shiro使用了与 Servlet 一样的 Filter 接口进行扩展。下图是 Shiro 拦截器的基础类图:


NameableFilter

NameableFilter用来给 Filter 起名字,如果没有设置默认就是 FilterName

还记得之前的authc吗?当我们组装拦截器链时会根据这个名字找到相应的拦截器实例


OncePerRequestFilter

OncePerRequestFilter用于防止多次执行 Filter,也就是说一次请求只会走一次拦截器链

另外提供 enabled 属性,表示是否开启该拦截器实例,默认 enabled=true 表示开启,如果不想让某个拦截器工作,可以设置为false即可


AbstractShiroFilter

ShiroFilter

ShiroFilter是整个 Shiro 的入口点,用于拦截需要安全控制的请求进行处理,这个之前已经用过了


AdviceFilter

AdviceFilter提供了AOP风格的支持,类似于SpringMVC中的 Interceptor

boolean preHandle(ServletRequest request, ServletResponse response) throws Exception
void postHandle(ServletRequest request, ServletResponse response) throws Exception
void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;
  • preHandler:类似于 AOP 中的前置增强;在拦截器链执行之前执行;如果返回 true 则继续拦截器链;否则中断后续的拦截器链的执行直接返回;进行预处理(如基于表单的身份验证、授权)

  • postHandle:类似于 AOP 中的后置返回增强;在拦截器链执行完成后执行;进行后处理(如记录执行时间之类的);

  • afterCompletion:类似于 AOP 中的后置最终增强;即不管有没有异常都会执行;可以进行清理资源(如解除 Subject 与线程的绑定之类的)


PathMatchingFilter

PathMatchingFilter提供了基于Ant风格的请求路径匹配功能及拦截器参数解析的功能,如“roles[admin,user]”自动根据“,”分割解析到一个路径参数配置并绑定到相应的路径

boolean pathsMatch(String path, ServletRequest request)
boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception
  • pathsMatch:该方法用于 path 与请求路径进行匹配的方法;如果匹配返回 true;

  • onPreHandle:在 preHandle 中,当 pathsMatch 匹配一个路径后,会调用onPreHandle方法并将路径绑定参数配置传给 mappedValue;然后可以在这个方法中进行一些验证(如角色授权),如果验证失败可以返回 false 中断流程;默认返回 true;也就是说子类可以只实现 onPreHandle 即可,无须实现 preHandle。如果没有 path 与请求路径匹配,默认是通过的(即 preHandle 返回 true)


AccessControlFilter

AccessControlFilter提供了访问控制的基础功能;比如是否允许访问/当访问拒绝时如何处理等

abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
  • isAccessAllowed:表示是否允许访问;mappedValue 就是[urls]配置中拦截器参数部分,如果允许访问返回 true,否则 false;

  • onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回 true 表示需要继续处理;如果返回 false 表示该拦截器实例已经处理了,将直接返回即可

onPreHandle会自动调用这两个方法决定是否继续处理

boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

另外AccessControlFilter还提供了如下方法用于处理如登录成功后/重定向到上一个请求

比如基于表单的身份验证就需要使用这些功能

void setLoginUrl(String loginUrl); // 身份验证时使用,默认/login.jsp
String getLoginUrl();
Subject getSubject(ServletRequest request, ServletResponse response); // 获取Subject 实例
boolean isLoginRequest(ServletRequest request, ServletResponse response); // 当前请求是否是登录请求
void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException // 将当前请求保存起来并重定向到登录页面
void saveRequest(ServletRequest request); // 将请求保存起来,如登录成功后再重定向回该请求
void redirectToLogin(ServletRequest request, ServletResponse response); // 重定向到登录页面

总结


注解权限控制

需要通过Shiro配置注解才能生效

注意:验证不通过是抛异常org.apache.shiro.authz.AuthorizationException,需要进行捕获


RequiresAuthentication

使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。

RequiresGuest

使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是guest身份,不需要经过认证或者在原先的session中存在记录。

RequiresPermissions

当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。

RequiresRoles

当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。

RequiresUser

当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。


使用方法

Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关)

RequiresRoles
RequiresPermissions
RequiresAuthentication
RequiresUser
RequiresGuest

会话管理

查看具体教程:https://www.yuque.com/chengxuyuanyideng/xkiu8l/xlyun9

前后端分离,且使用双Token(如,JWT)的情况下,不需要使用会话


记住我

查看具体教程:https://www.yuque.com/chengxuyuanyideng/xkiu8l/mobgcd

前后端分离,且使用双Token(如,JWT)的情况下,shiro的记住我功能不适用


限制登录

查看具体教程:https://www.yuque.com/chengxuyuanyideng/xkiu8l/ic7hm0k9ytuty5b4


动态 URL

通过在数据库中配置URL,以及该URL被访问到需要哪些权限,当登录用户去访问该URL,需要判断该用户是否具有访问该URL的权限、角色即可

查看具体教程:https://www.yuque.com/chengxuyuanyideng/xkiu8l/ifkwnk

规则,就是用来打破的( ̄へ ̄)!