Shiro笔记

Shiro TabTan

本篇笔记记录于2021年3月,迁移至此,分享给需要它的人。


Shiro的核心功能

Authentication、Authorization、Session Management、Cryptography

Authentication 认证

Authorization 授权

Session Management 会话管理

Cryptography 加密

支持的特性:

  • Web Support -Shiro提供过滤器
  • Caching 缓存支持
  • Concurrency 支持多线程应用
  • Testing 提供测试支持
  • Run As允许一个用户以另一种身份去访问
  • Remember Me

说明:Shiro是一个安全框架,不提供用户及权限的维护

Shiro的核心组件

Subject、Security Manager、Realms

  • Subject 表示待认证和授权的用户
  • Security Manager 安全管理器
    • Authenticator 认证器
    • Authorizer 授权器
    • SessionManager 会话管理器
    • CacheManager 缓存管理器
  • Realm 相当于Shiro进行认证和授权的数据源,充当了Shiro与安全数据之间的桥梁或者连接器。

Hello World程序

实例一:JavaSE使用shiro(数据从文件.ini导入)

导入依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.6.0</version>
</dependency>

ini:

[users]
zhangsan=123456,seller
admin=123456,ckmgr
admin=123456,admin

[roles]
admin=*
seller=order-add,order-del,order-list
ckmgr=ck-add,ck-del,ck-list

主程序:

public static void main(String[] args) {

    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入账号:");
    String username = scanner.nextLine();
    System.out.println("请输入密码:");
    String password = scanner.nextLine();
    
    DefaultSecurityManager manager = new DefaultSecurityManager();

    IniRealm realm = new IniRealm("classpath:test.ini");

    manager.setRealm(realm);

    SecurityUtils.setSecurityManager(manager);

    Subject subject = SecurityUtils.getSubject();

    AuthenticationToken token = new UsernamePasswordToken(username,password);

    System.out.println(subject.isAuthenticated());
    subject.login(token);
    System.out.println(subject.isAuthenticated());

    System.out.println(subject.hasRole("admin"));
    
}

结果

请输入账号:
admin
请输入密码:
123456
false
true
true

步骤:

  • 创建安全管理器SecurityManager
  • 创建realm
  • 将realm设置到安全管理器
  • 将安全管理器设置到工具类
  • 从工具类获取subject
  • 获取token,登录

流程:

① 通过subject.login(token)进行登录,就会将token包含的用户信息(账号密码)传递给SecurityManager

② SecurityManager调用Anthenticator进行身份认证

③ Anthenticator把token传递给Realm

④ Realm根据得到的token,调用dogetAuthenticationinfo方法进行认证(如果认证失败则抛出异常)

Shiro实例

pom.xml 依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.23</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.12</version>
</dependency>


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.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>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.6.0</version>
</dependency>
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
    <scope>compile</scope>
</dependency>

application.yml

spring:
  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 123456
      initial-size: 1
      min-idle: 1
      max-active: 20
mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.tan.pojo

ShiroConfig.java

@Configuration
public class ShiroConfig {
    //方言
    @Bean
    ShiroDialect dialect(){
        return new ShiroDialect();
    }
    //自定义Realm
    @Bean
    MyUserRealm userRealm(HashedCredentialsMatcher hashedCredentialsMatcher){
        MyUserRealm userRealm = new MyUserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return userRealm;
    }
    //自定义Realm
    @Bean
    MyAdminRealm adminRealm(HashedCredentialsMatcher credentialsMatcher){
        MyAdminRealm adminRealm = new MyAdminRealm();
        adminRealm.setCredentialsMatcher(credentialsMatcher);
        return adminRealm;
    }



    //加盐
    @Bean
    HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("md5");
        matcher.setHashIterations(3);
        return matcher;
    }

    @Bean
    MyModularRealmAuthenticator modularRealmAuthenticator(){
        return new MyModularRealmAuthenticator();
    }

    //安全管理器
    @Bean
    DefaultWebSecurityManager securityManager(HashedCredentialsMatcher credentials){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();

        Collection<Realm> realms = new ArrayList<>();
        realms.add(userRealm(credentials));
        realms.add(adminRealm(credentials));
        manager.setAuthenticator(modularRealmAuthenticator());
        manager.setRealms(realms);
        return manager;
    }

    //过滤器工厂Bean
    @Bean
    ShiroFilterFactoryBean filter(DefaultWebSecurityManager manager){

        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();

        filter.setSecurityManager(manager);
        filter.setLoginUrl("/login");
        filter.setUnauthorizedUrl("/lessPermission");

        Map<String,String> map = new HashMap<>();

        map.put("/", "anon");
        map.put("/toLogin","anon");
        map.put("/login", "anon");
        map.put("/regist", "anon");
        map.put("/tan","anon");
        map.put("/layui/**","anon");
        map.put("/layui/layui.js","anon");
        map.put("/index", "user");
        map.put("/index1", "user");
        map.put("/index2", "user");
        map.put("/logout","logout");
        map.put("/cAdd","perms[sys:c:save]");
        map.put("/**","authc");

        filter.setFilterChainDefinitionMap(map);

        return filter;
    }
}

自定义Token

MyToken.java

public class MyToken extends UsernamePasswordToken {

    private String loginType;

    public MyToken(String username,String password,String loginType){

        super(username,password);
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }
}

自定义Realm

MyAdminRealm.java

public class MyAdminRealm extends AuthorizingRealm {

    private Logger log = LoggerFactory.getLogger(MyAdminRealm.class);

    @Resource
    UserDAO userDAO;

    @Resource
    PrimissionDAO primissionDAO;

    @Resource
    RoleDAO roleDAO;

    @Override
    public String getName() {
        return "admin";
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        String username = (String) principals.iterator().next();
        Set<String> ps = primissionDAO.getPrimissionsByUsername(username);
        Set<String> rs = roleDAO.getRoleNamesByUsername(username);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(rs);
        info.setStringPermissions(ps);
        return info;
    }

    @SneakyThrows
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        log.info("--------------adminRealm--------------");
        UsernamePasswordToken up = (UsernamePasswordToken) token;

        User user = userDAO.queryUserByUsername(((UsernamePasswordToken) token).getUsername());
        ByteSource salt = ByteSource.Util.bytes(user.getPasswordSalt());

        return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),salt,getName());
    }
}

MyUserRealm.java

public class MyUserRealm extends AuthorizingRealm {

    private Logger log = LoggerFactory.getLogger(MyUserRealm.class);

    @Resource
    UserDAO userDAO;

    @Resource
    PrimissionDAO primissionDAO;

    @Resource
    RoleDAO roleDAO;

    @Override
    public String getName() {
        return "user";
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        String username = (String) principals.iterator().next();

        Set<String> rs = roleDAO.getRoleNamesByUsername(username);

        Set<String> ps = primissionDAO.getPrimissionsByUsername(username);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(rs);
        info.setStringPermissions(ps);

        return info;
    }

    @SneakyThrows
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        log.info("--------------userRealm--------------");
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;

        User user = userDAO.queryUserByUsername(upToken.getUsername());
        ByteSource bytes = ByteSource.Util.bytes(user.getPasswordSalt());
        return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),bytes,getName());
    }
}

自定义认证器MyModularRealmAuthenticator.java

public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {


    private Logger log = LoggerFactory.getLogger(MyModularRealmAuthenticator.class);
    //自定义doAuthenticate方法
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("------自定义Authenticator------");
        assertRealmsConfigured();
		//将token强转成自定义的带有loginType的MyToken
        MyToken token = (MyToken) authenticationToken;

        Collection<Realm> realms = getRealms();
        Collection<Realm> typeRealms = new ArrayList<>();

        for (Realm realm:realms) {
            String name = realm.getName();
            if (realm.getName().startsWith(token.getLoginType())){
                typeRealms.add(realm);
            }
        }

        if (typeRealms.size() == 1) {
            return doSingleRealmAuthentication((Realm) typeRealms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(typeRealms, authenticationToken);
        }
    }
}

入口:

public void checkLogin(String username,String password,boolean rememberMe,String loginType) throws Exception {

        Subject subject = SecurityUtils.getSubject();
		//使用自定义token
        UsernamePasswordToken token = new
            MyToken(username,password,"admin".equals(loginType)?"admin":"user");
        subject.login(token);

    }

登录表单:

<form action="/toLogin" method="post">

    <span>账号:</span><input name="username"/>
    <span>密码:</span><input type="password" name="password"/>
    <br>
    <input name="loginType" type="radio" checked="checked" value="admin">管理员
    <input name="loginType" type="radio" value="user">普通用户

    <span>记住我:</span><input type="checkbox" name="rememberMe" />
    <button type="submit">登录</button>

</form>

详解

认证

授权

授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。

三种方式

  • if方式

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

    • @RequiresRoles("admin")
      public void hello() {
          //有权限
      }
      
  • 命名空间的标签方式

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

粗粒度

Shiro 提供了 hasRole/hasRoles 用于判断用户是否拥有某个角色/某些权限;

Shiro 提供的 checkRole/checkRoles 和 hasRole/hasAllRoles 不同的地方是它在判断为假的情况下会抛出 UnauthorizedException 异常。

细粒度

Shiro 提供了 isPermitted 和 isPermittedAll 用于判断用户是否拥有某个权限或所有权限,也没有提供如 isPermittedAny 用于判断拥有某一个权限的接口。但是失败的情况下会抛出 UnauthorizedException 异常。

字符串通配符权限

规则:“资源标识符:操作:对象实例 ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串,“:”表示资源/操作/实例的分割;“,”表示操作的分割;“*”表示任意资源/操作/实例。

用户拥有所有资源的“view”所有权限。假设判断的权限是“"system:user:view”,那么需要“role5=::view”这样写才行

授权流程

在这里插入图片描述

  1. 首先调用 Subject.isPermitted*/hasRole*接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
  2. Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:view”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
  3. 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
  4. Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。

ModularRealmAuthorizer 进行多 Realm 匹配流程:

  • 首先检查相应的 Realm 是否实现了实现了 Authorizer;
  • 如果实现了 Authorizer,那么接着调用其相应的 isPermitted*/hasRole* 接口进行匹配;
  • 如果有一个 Realm 匹配那么将返回 true,否则返回 false。

如果 Realm 进行授权的话,应该继承 AuthorizingRealm,其流程是:

  • 如果调用 hasRole*,则直接获取 AuthorizationInfo.getRoles() 与传入的角色比较即可;首先如果调用如 isPermitted(“user:view”),首先通过 PermissionResolver 将权限字符串转换成相应的 Permission 实例,默认使用 WildcardPermissionResolver,即转换为通配符的 WildcardPermission;
  • 通过 AuthorizationInfo.getObjectPermissions() 得到 Permission 实例集合;通过 AuthorizationInfo.getStringPermissions() 得到字符串集合并通过 PermissionResolver 解析为 Permission 实例;然后获取用户的角色,并通过 RolePermissionResolver 解析角色对应的权限集合(默认没有实现,可以自己提供);
  • 接着调用 Permission.implies(Permission p) 逐个与传入的权限比较,如果有匹配的则返回 true,否则 false。
Authorizer、PermissionResolver及RolePermissionResolver

Authorizer 的职责是进行授权(访问控制),PermissionResolver 用于解析权限字符串到 Permission 实例,而 RolePermissionResolver 用于根据角色解析相应的权限集合。

会话

基本操作

登录成功后使用 Subject.getSession() 即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;另外 Subject.getSession(false),如果当前没有创建 Session 则返回 null(不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session)。

session.getId();

获取当前会话的唯一标识。

session.getHost();

获取当前 Subject 的主机地址,该地址是通过 HostAuthenticationToken.getHost() 提供的。

session.getTimeout(); 
session.setTimeout(毫秒);

获取 / 设置当前 Session 的过期时间;如果不设置默认是会话管理器的全局过期时间。

session.getStartTimestamp();
session.getLastAccessTime();

获取会话的启动时间最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。

session.touch();
session.stop();

更新会话最后访问时间及销毁会话;当 Subject.logout() 时会自动调用 stop 方法来销毁会话。如果在 web 中,调用 javax.servlet.http.HttpSession. invalidate() 也会自动调用 Shiro Session.stop 方法进行销毁 Shiro 的会话。

session.setAttribute("key", "123");
Assert.assertEquals("123", session.getAttribute("key"));
session.removeAttribute("key");

设置 / 获取 / 删除会话属性;在整个会话范围内都可以对这些属性进行操作。

会话管理器

会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。

SecurityManager 提供了如下接口:

Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话

另外用于 Web 环境的 WebSessionManager 又提供了如下接口:

boolean isServletContainerSessions();// 是否使用 Servlet 容器的会话

Shiro 还提供了 ValidatingSessionManager 用于验资并过期会话:

void validateSessions();// 验证所有会话是否过期

在这里插入图片描述

三个默认实现

DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager:用于 Web 环境的实现,可以替代 ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。

SimpleCookie

  • sessionIdCookie 是 sessionManager 创建会话 Cookie 的模板:
  • sessionIdCookie.name:设置 Cookie 名字,默认为 JSESSIONID;
  • sessionIdCookie.domain:设置 Cookie 的域名,默认空,即当前访问的域名;
  • sessionIdCookie.path:设置 Cookie 的路径,默认空,即存储在域名根下;
  • sessionIdCookie.maxAge:设置 Cookie 的过期时间,秒为单位,默认 - 1 表示关闭浏览器时过期 Cookie;
  • sessionIdCookie.httpOnly:如果设置为 true,则客户端不会暴露给客户端脚本代码,使用 HttpOnly cookie 有助于减少某些类型的跨站点脚本攻击;此特性需要实现了 Servlet 2.5 MR6 及以上版本的规范的 Servlet 容器支持;
  • sessionManager.sessionIdCookieEnabled:是否启用 / 禁用 Session Id Cookie,默认是启用的;如果禁用后将不会设置 Session Id Cookie,即默认使用了 Servlet 容器的 JSESSIONID,且通过 URL 重写(URL 中的 “;JSESSIONID=id” 部分)保存 Session Id。

另外我们可以如 “sessionManager. sessionIdCookie.name=sid” 这种方式操作 Cookie 模板。

会话监听器

会话监听器用于监听会话创建、过期及停止事件:

public class MySessionListener1 implements SessionListener {
    @Override
    public void onStart(Session session) {//会话创建时触发
        System.out.println("会话创建:" + session.getId());
    }
    @Override
    public void onExpiration(Session session) {//会话过期时触发
        System.out.println("会话过期:" + session.getId());
    }
    @Override
    public void onStop(Session session) {//退出/会话过期时触发
        System.out.println("会话停止:" + session.getId());
    }  
}

如果只想监听某一个事件,可以继承 SessionListenerAdapter 实现:

public class MySessionListener2 extends SessionListenerAdapter {
    @Override
    public void onStart(Session session) {
        System.out.println("会话创建:" + session.getId());
    }
}

在 shiro-web.ini 配置文件中可以进行如下配置设置会话监听器:

sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1
sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2
sessionManager.sessionListeners=$sessionListener1,$sessionListener2

会话存储 / 持久化

Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现

会话验证

Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler 来做这件事情。

Shiro 缓存机制

Shiro 提供了类似于 Spring 的 Cache 抽象,即 Shiro 本身不实现 Cache,但是对 Cache 进行了又抽象,方便更换不同的底层 Cache 实现。

public interface Cache<K, V> {
    //根据Key获取缓存中的值
    public V get(K key) throws CacheException;
    //往缓存中放入key-value,返回缓存中之前的值
    public V put(K key, V value) throws CacheException; 
    //移除缓存中key对应的值,返回该值
    public V remove(K key) throws CacheException;
    //清空整个缓存
    public void clear() throws CacheException;
    //返回缓存大小
    public int size();
    //获取缓存中所有的key
    public Set<K> keys();
    //获取缓存中所有的value
    public Collection<V> values();
}
public interface CacheManager {
    //根据缓存名字获取一个Cache
    public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}
public interface CacheManagerAware {
    //注入CacheManager
    void setCacheManager(CacheManager cacheManager);
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值