前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Shiro权限管理框架入门到实战

Shiro权限管理框架入门到实战

作者头像
码农小胖哥
发布2020-06-28 14:29:02
1.7K0
发布2020-06-28 14:29:02
举报

前言:前几天学习了SpringSecurity安全框架,这几天又接着学习shiro框架,这两者框架都是同一类产品,解决同一类问题,但是在官方推荐使用Shiro框架,因为它简单易学,所以这里有时间学习了以下。

Shiro的作用

  • 用于验证登陆用户的身份
  • 用户访问权限控制和登陆的认证,1.用于用户登陆的验证,2.用户用户登录后的授权,也就是那些用户拥有访问那些接口的权限
  • 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
  • 可将一个或以上用户安全数据源数据组合成一个复合的用户“view”(视图)
  • 支持单点登录(SSO)功能
  • 支持提供“Remember Me”服务,当用户第二次登陆时只要session还可用就不需要再次登陆

Shiro的优点

  • 易于上手
  • 灵活——Apache Shiro可以在任何应用程序环境中工作。虽然在网络工作、EJB和IoC环境中可能并不需要它。但Shiro的授权也没有任何规范,甚至没有许多依赖关系。
  • Web支持——Apache Shiro拥有令人兴奋的web应用程序支持,允许您基于应用程序的url创建灵活的安全策略和网络协议(例如REST),同时还提供一组JSP库控制页面输出。
  • 低耦合——Shiro干净的API和设计模式使它容易与许多其他框架和应用程序集成。你会看到Shiro无缝地集成Spring这样的框架,以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin…等。
  • 被广泛支持——Apache Shiro是Apache软件基金会的一部分。项目开发和用户组都有友好的网民愿意帮助。这样的商业公司如果需要Katasoft还提供专业的支持和服务。

Apache Shiro架构

下图为描述Shiro的架构图:

在这里插入图片描述 Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)是Shiro框架的四大基石

  • Authentication(认证):用于用户登陆时的认证
  • Authorization(授权):访问控制。指定哪些用户拥有访问哪些接口的权限
  • Session Management(会话管理):特定于用户的会话管理。
  • Cryptography(加密):对用户的登陆的信息进行加密

其它特点:

  • Web支持:Shiro的Web支持API有助于保护Web应用程序。缓存:缓存是Apache Shiro API中的第一级,以确保安全操作保持快速和高效。
  • 并发性:Apache Shiro支持具有并发功能的多线程应用程序。 测试:存在测试支持,可帮助您编写单元测试和集成测试,并确保代码按预期得到保障。
  • “运行方式”:允许用户承担另一个用户的身份(如果允许)的功能,有时在管理方案中很有用。
  • “记住我”:记住用户在会话中的身份,所以用户只需要强制登录即可。

注意:Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro

在概念层,Shiro 中的重要概念:Subject,SecurityManager和 Realm。

在这里插入图片描述

  • Subject:当前用户,Subject可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro架构的核心,配合内部安全组件共同组成安全伞。
  • Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm来实现认证(authentication)和/或授权(authorization)。

Shiro 认证流程

在这里插入图片描述

Shiro 实战

(1)创建SpringBooot项目,pom.xml依赖如下:

代码语言:javascript
复制
<?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>1.5.16.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ldc.org</groupId>
    <artifactId>shirodemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>shirodemo</name>
    <description>Demo project for shiro</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.20</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>4.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • SpringBoot整合Mybaties进行数据库连接
  • SpringBoot整合Shiro框架
  • SpringBoot整合jsp并使用jstl表达式
  • SpringBoot整合阿里巴巴的Druid

(2)application.properties的配置如下:

代码语言:javascript
复制
## database##
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=user

## mybaties ##
mybatis.mapper-locations=mappers/*.xml
mybatis.type-aliases-package=com.ldc.org.shirodemo.pojo

## jsp ##
spring.mvc.view.prefix=/pages/
spring.mvc.view.suffix=.jsp

这些都是一些常用的配置,这里就不再赘述,不懂得自行百度 (3)创建与数据库对应的实体类,主要有User、Role、Permission这三个实体类。代码如下:

代码语言:javascript
复制
package com.ldc.org.shirodemo.pojo;
public class Permission {

    private Integer pid;

    private String name;

    private String url;

//get和set方法
}


package com.ldc.org.shirodemo.pojo;
import java.util.HashSet;
import java.util.Set;
public class Role {

    private Integer rid;

    private String rname;

    private Set<Permission> permissions=new HashSet<>();

    private Set<User> users=new HashSet<>();
    //get和set方法
}


package com.ldc.org.shirodemo.pojo;
import java.util.HashSet;
import java.util.Set;
public class User {

    private Integer uid;

    private String username;

    private String password;

    private Set<Role> roles=new HashSet<>();
     //get和set方法
}

在这里插入图片描述 (4)创建mapper接口

代码语言:javascript
复制
package com.ldc.org.shirodemo.mapper;

import com.ldc.org.shirodemo.pojo.User;
import org.apache.ibatis.annotations.Param;

public interface UserMapper {

    User findByUsername(@Param("username") String username);
}

在这里插入图片描述 并在主方法的加上这些注解,指定mapper的位置,和开启组件扫描

在这里插入图片描述 (5)创建mapper.xml文件,代码如下:

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ldc.org.shirodemo.mapper.UserMapper">
    <resultMap id="userMap" type="com.ldc.org.shirodemo.pojo.User">
        <id property="uid" column="uid"/>
        <result property="username" column="username" />
        <result property="password" column="password" />
        <collection property="roles" ofType="com.ldc.org.shirodemo.pojo.Role">
            <id property="rid" column="rid"/>
            <result property="rname" column="rname"/>
            <collection property="permissions" ofType="com.ldc.org.shirodemo.pojo.Permission">
                <id property="pid" column="pid"/>
                <result property="name" column="name"/>
                <result property="url" column="url"/>
            </collection>
        </collection>
    </resultMap>

    <select id="findByUsername" parameterType="string" resultMap="userMap">
        SELECT u.*,r.*,p.*
        FROM user u
          INNER JOIN user_role ur on ur.uid=u.uid
          INNER JOIN role r on r.rid=ur.rid
          INNER JOIN permission_role pr on pr.rid=r.rid
          INNER JOIN permission p on pr.pid=p.pid
        WHERE  u.username=#{username}
    </select>
</mapper>

在这里插入图片描述 (6)创建数据库test,sql语句如下,以及初始化一些测试数据:

代码语言:javascript
复制
-- 权限表--
CREATE TABLE permission (
  pid int(11) NOT NULL AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL DEFAULT '',
  url VARCHAR(255) DEFAULT '',
  PRIMARY KEY (pid)
) ENGINE =InnoDB DEFAULT CHARSET = utf8;

INSERT INTO permission VALUES ('1','add','');
INSERT INTO permission VALUES ('2','delete','');
INSERT INTO permission VALUES ('3','edit','');
INSERT INTO permission VALUES ('4','query','');

-- 用户表--
CREATE TABLE user(
uid int(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(255) NOT NULL DEFAULT '',
password VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (uid)
) ENGINE =InnoDB DEFAULT CHARSET=utf8;

INSERT INTO user VALUES ('1','admin','123');
INSERT INTO user VALUES ('2','demo','123');

-- 角色表--
CREATE TABLE role(
rid int(11) NOT NULL AUTO_INCREMENT,
rname VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (rid)
) ENGINE =InnoDB DEFAULT CHARSET=utf8;

INSERT INTO role VALUES ('1','admin');
INSERT INTO role VALUES ('2','customer');

-- 权限角色关系表--
CREATE TABLE permission_role(
  rid int(11) NOT NULL ,
  pid int(11) NOT NULL ,
  KEY idx_rid(rid),
  KEY idx_pid(pid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

INSERT INTO permission_role VALUES ('1','1');
INSERT INTO permission_role VALUES ('1','2');
INSERT INTO permission_role VALUES ('1','3');
INSERT INTO permission_role VALUES ('1','4');
INSERT INTO permission_role VALUES ('2','1');
INSERT INTO permission_role VALUES ('2','4');

-- 用户角色关系表--
CREATE TABLE user_role(
  uid int(11) NOT NULL ,
  rid int(11) NOT NULL ,
  KEY idx_uid(uid),
  KEY idx_rid(rid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

INSERT INTO user_role VALUES (1,1);
INSERT INTO user_role VALUES (2,2);

(7)创建AuthRealm,实现Shiro框架的AuthorizingRealm,代码如下:

代码语言:javascript
复制
package com.ldc.org.shirodemo;

import com.ldc.org.shirodemo.pojo.Permission;
import com.ldc.org.shirodemo.pojo.Role;
import com.ldc.org.shirodemo.pojo.User;
import com.ldc.org.shirodemo.service.UserService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user= (User) principals.fromRealm(this.getClass().getName()).iterator().next();
        List<String> permissionList =new ArrayList<>();
        List<String> roleNameList =new ArrayList<>();
        Set<Role> roleSet=user.getRoles();
        if (CollectionUtils.isNotEmpty(roleSet)){
            for(Role role:roleSet){
                roleNameList.add(role.getRname());
                Set<Permission> permissionSet = role.getPermissions();
                if (CollectionUtils.isNotEmpty(permissionSet)){
                    for(Permission permission:permissionSet){
                        permissionList.add(permission.getName());
                    }
                }
            }
        }
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
        info.addStringPermissions(permissionList);
        info.addRoles(roleNameList);
        return info;
    }

    //认证登陆
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();
        User user = userService.findByUsername(username);
        return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());

    }
}

(8)创建Shiro的配置类,如下:

代码语言:javascript
复制
package com.ldc.org.shirodemo;

import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.servlet.ShiroFilter;
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;

@Configuration
public class ShiroConfiguration {

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager){
        ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
        bean.setSecurityManager(manager);

        bean.setLoginUrl("/login");
        bean.setSuccessUrl("/index");
        bean.setUnauthorizedUrl("/unauthorized");

        LinkedHashMap<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
        //authc标识只有登录后才有权限访问,anon标识没有登陆也有权限访问
        filterChainDefinitionMap.put("/index","authc");
        filterChainDefinitionMap.put("/login","anon");
        filterChainDefinitionMap.put("/loginUser","anon");
        //admin接口只允许admin角色访问
        filterChainDefinitionMap.put("/admin","roles[admin]");
        filterChainDefinitionMap.put("/edit","perms[edit]");
        //放开druid的所有请求,可以访问druid监控
//        filterChainDefinitionMap.put("/druid/**","anon");
        filterChainDefinitionMap.put("/**","user");
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }

    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm){
        DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
        manager.setRealm(authRealm);
        return manager;
    }

    @Bean("authRealm")
    public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher credentialMatcher){
        AuthRealm authRealm=new AuthRealm();
        //使用缓存
        authRealm.setCacheManager(new MemoryConstrainedCacheManager());
        authRealm.setCredentialsMatcher(credentialMatcher);
        return authRealm;
    }

    @Bean("credentialMatcher")
    public CredentialMatcher credentialMatcher(){
        return new CredentialMatcher();
    }

    /**
     * 以下的两个方法是设置shiro与spring之间的关联
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator=new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

}

(9)创建CredentialMatcher,作为密码比较的规则验证:

代码语言:javascript
复制
package com.ldc.org.shirodemo;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

/**
 * 密码校验规则
 */
public class CredentialMatcher extends SimpleCredentialsMatcher {

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken) token;
        String password=new String(usernamePasswordToken.getPassword());
        String dbPassword = (String) info.getCredentials();
        return this.equals(password,dbPassword);
    }


}

(10)Service层以及Service的实现:

代码语言:javascript
复制
package com.ldc.org.shirodemo.service;

import com.ldc.org.shirodemo.pojo.User;

public interface UserService {

    User findByUsername( String username);

}
代码语言:javascript
复制
package com.ldc.org.shirodemo.service;

import com.ldc.org.shirodemo.mapper.UserMapper;
import com.ldc.org.shirodemo.pojo.User;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public User findByUsername(String username) {
        return userMapper.findByUsername(username);
    }
}

(10)Controller层:

代码语言:javascript
复制
package com.ldc.org.shirodemo.controller;

import com.ldc.org.shirodemo.pojo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpSession;

@Controller
public class UserController {

    @RequestMapping("/login")
    public String login(){
        return "login";
    }

    /**
     * 两个case:
     * 第一个是只有登录后才能访问相关的接口,没有登陆是不允许访问相关的接口,例如admin接口
     * 第二个是某些接口只能被某些角色来访问
     * @return
     */
    @RequestMapping("/admin")
    @ResponseBody
    public String admin(){
        return "admin success";
    }

    @RequestMapping("/loginOut")
    public String loginOut(){
        Subject subject = SecurityUtils.getSubject();
        if (subject!=null){
            subject.logout();
        }
        return "login";
    }

    @RequestMapping("/index")
    public String index(){
        return "index";
    }

    @RequestMapping("/edit")
    @ResponseBody
    public String edit(){
        return "edit success";
    }

    @RequestMapping("/unauthorized")
    public String unauthorized(){
        return "unauthorized";
    }

    @RequestMapping("/loginUser")
    public String loginUser(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            HttpSession session){
        UsernamePasswordToken token=new UsernamePasswordToken(username,password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            User user= (User) subject.getPrincipal();
            session.setAttribute("user",user);
            return "index";
        }catch (Exception e){
            e.printStackTrace();
            return "login";
        }
    }
}

(11)jsp页面:

代码语言:javascript
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Home</title>
</head>
<body>

<h1>欢迎登陆, ${user.username}</h1>

</body>
</html>
代码语言:javascript
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Login</title>
</head>
<body>

<h1>欢迎登陆</h1>
<form action="/loginUser" method="post">
    <input type="text" name="username"><br>
    <input type="password" name="password"><br>
    <input type="submit" value="提交">
</form>
</body>
</html>
代码语言:javascript
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Unauthorized</title>
</head>
<body>

Unauthorized!
</body>
</html>

最后的项目的结构,如图所示:

在这里插入图片描述 (12)测试--其实前面粘贴了一大堆的代码都是为后面的测试准备,这里讲解的Shiro的技术点,主要有三点:

  • 第一个是只有登录后才能访问相关的接口,没有登陆是不允许访问相关的接口,例如admin接口
  • 第二个是某些接口只能被某些角色来访问
  • 第三点某些接口只能被特定权限的才能访问

以下是验证这三点的过程: 例如,index接口,只有登陆才能访问,启动项目,直接输入localhost:8080/index是不能访问首页的,会被重定向会登陆页面:

在这里插入图片描述 当我们输入正确的密码的时候,如图所示,才能访问index这个接口:

在这里插入图片描述 验证第二点admin接口只有admin角色才能访问,其它角色无权访问,测试如下,先以demo的用户登陆:

在这里插入图片描述 然后输入localhost:8080/admin,出现如图所示的,直接进入我们的错误页面:

在这里插入图片描述 以admin的角色登陆,再访问admin接口,访问成功

在这里插入图片描述

在这里插入图片描述 最后验证最后一点,edit的权限只有admin才有,demo用户没有,这个是在创建数据的时候就已经初始化好了,自行查前面的创建数据库的过程。 在demo用户下直接访问edit接口,就会直接被跳转到错误页面

在这里插入图片描述 而在admin用户下访问edit接口,允许访问

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农小胖哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Shiro的作用
  • Shiro的优点
  • Apache Shiro架构
  • Shiro 认证流程
  • Shiro 实战
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档