SpringBoot整合Shiro

因为工作原因,项目中使用了Springboot 整合Shiro来做权限的整合,项目接近尾声,我也按照网上的资料自己搭了一个,踩了一些坑。下面是步骤

目录

(一) 基础工程

1.1 首先是搭建一个springboot 的web项目 这个就不多说了

1.2 依赖引入

1.3 这里我们用到thymeleaf模版

1.4 创建页面

1.5 在entity包下把实体类创建起来。

(二)身份校验和角色设置

2.1 数据库中插入数据

2.2 .Realms

2.3 service方法实现

2.4 dao

2.5 接下来配置Shiro的关键部分

2.6 修改我们的HomeController中的/login请求


 

(一) 基础工程

图片是项目搭建结构图

1.1 首先是搭建一个springboot 的web项目 这个就不多说了

1.2 依赖引入

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.bobby.springboot</groupId>
  <artifactId>boot_shrio</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>boot_shrio Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

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

    <!--thymeleaf  模板-->
    <dependency>
      <groupId>net.sourceforge.nekohtml</groupId>
      <artifactId>nekohtml</artifactId>
      <version>1.9.22</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--shiro   依赖-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.4.0</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <optional>true</optional>
    </dependency>

    <!-- mp 依赖 -->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus</artifactId>
      <version>2.3</version>
    </dependency>

  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <fork>true</fork>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

1.3 这里我们用到thymeleaf模版

application.yml:在这个地方我遇到了一个问题 就是我不指定templates目录,使用spriboot默认的static文件夹下,但访问不了。

已解决访问static中index文件夹下的index.html页面

spring:
    thymeleaf:
        suffix: .html
        encoding: UTF-8
        content-type: text/html
        mode: HTML5
        prefix: classpath:/templates/
    application:
        name: shiro
    datasource:
        url: jdbc:mysql://127.0.0.1:3306/bobby?useUnicode=true&characterEncoding=utf-8
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
    jpa:
        database: mysql
        showSql: true
        hibernate:
            ddlAuto: update
        properties:
            hibernate:
                dialect: org.hibernate.dialect.MySQL5Dialect
                format_sql: true

1.4 创建页面

index.html、userAdd.html、userDel.html、userInfo.html中只需要显示一句简单的文字即可。

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
错误信息:<h4 style="color: red;" th:text="${msg}"></h4>
<form th:action="@{/login}" method="post">
    <p>账号:<input type="text" name="username" value="admin"/></p>
    <p>密码:<input type="password" name="password" value="123456"/></p>
    <p><input type="submit" value="登录"/></p>
</form>
</body>
</html>

1.5 在entity包下把实体类创建起来。

SysPermission 

package com.bobby.springboot.entity;

/**
 * Created by Bobby on 2019/9/19 17:23
 */
import java.io.Serializable;
import java.util.List;

import javax.persistence.*;

@Entity
@Table(name = "sys_permission")
public class SysPermission implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id@GeneratedValue
    private long id;//主键.
    private String name;//名称.

    @Column(columnDefinition="enum('menu','button')")
    private String resourceType;//资源类型,[menu|button]
    private String url;//资源路径.
    private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
    private Long parentId; //父编号
    private String parentIds; //父编号列表
    private Boolean available = Boolean.FALSE;

    @ManyToMany
    @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
    private List<SysRole> roles;

    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getResourceType() {
        return resourceType;
    }
    public void setResourceType(String resourceType) {
        this.resourceType = resourceType;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getPermission() {
        return permission;
    }
    public void setPermission(String permission) {
        this.permission = permission;
    }
    public Long getParentId() {
        return parentId;
    }
    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }
    public String getParentIds() {
        return parentIds;
    }
    public void setParentIds(String parentIds) {
        this.parentIds = parentIds;
    }
    public Boolean getAvailable() {
        return available;
    }
    public void setAvailable(Boolean available) {
        this.available = available;
    }
    public List<SysRole> getRoles() {
        return roles;
    }
    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }
    @Override
    public String toString() {
        return "SysPermission [id=" + id + ", name=" + name + ", resourceType=" + resourceType + ", url=" + url
                + ", permission=" + permission + ", parentId=" + parentId + ", parentIds=" + parentIds + ", available="
                + available + ", roles=" + roles + "]";
    }
}


userinfo

package com.bobby.springboot.entity;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

/**
 * Created by Bobby on 2019/9/23 11:13
 */
@Entity
@Table(name = "user_info")
public class UserInfo implements Serializable {


    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue
    private long uid;//用户id;

    @Column(unique=true)
    private String username;//账号.

    private String name;//名称(昵称或者真实姓名,不同系统不同定义)

    private String password; //密码;
    private String salt;//加密密码的盐

    private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.


    @ManyToMany(fetch= FetchType.EAGER)//立即从数据库中进行加载数据;
    @JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
    private List<SysRole> roleList;// 一个用户具有多个角色

    public List<SysRole> getRoleList() {
        return roleList;
    }

    public void setRoleList(List<SysRole> roleList) {
        this.roleList = roleList;
    }

    public long getUid() {
        return uid;
    }

    public void setUid(long uid) {
        this.uid = uid;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getSalt() {
        return salt;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }

    public byte getState() {
        return state;
    }

    public void setState(byte state) {
        this.state = state;
    }

    /**
     * 密码盐.
     * @return
     */
    public String getCredentialsSalt(){
        return this.username+this.salt;
    }

    @Override
    public String toString() {
        return "UserInfo [uid=" + uid + ", username=" + username + ", name=" + name + ", password=" + password
                + ", salt=" + salt + ", state=" + state + "]";
    }
}

sysrole

package com.bobby.springboot.entity;

/**
 * Created by Bobby on 2019/9/19 17:23
 */
import java.io.Serializable;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

@Entity
@Table(name = "sys_role")
public class SysRole implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id@GeneratedValue
    private Long id; // 编号
    private String role; // 角色标识程序中判断使用,如"admin",这个是唯一的:
    private String description; // 角色描述,UI界面显示使用
    private Boolean available = Boolean.FALSE; // 是否可用,如果不可用将不会添加给用户

    //角色 -- 权限关系:多对多关系;
    @ManyToMany(fetch= FetchType.EAGER)
    @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
    private List<SysPermission> permissions;

    // 用户 - 角色关系定义;
    @ManyToMany
    @JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
    private List<UserInfo> userInfos;// 一个角色对应多个用户

    public List<UserInfo> getUserInfos() {
        return userInfos;
    }
    public void setUserInfos(List<UserInfo> userInfos) {
        this.userInfos = userInfos;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getRole() {
        return role;
    }
    public void setRole(String role) {
        this.role = role;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public Boolean getAvailable() {
        return available;
    }
    public void setAvailable(Boolean available) {
        this.available = available;
    }
    public List<SysPermission> getPermissions() {
        return permissions;
    }
    public void setPermissions(List<SysPermission> permissions) {
        this.permissions = permissions;
    }
//    @Override
//    public String toString() {
//        return "SysRole [id=" + id + ", role=" + role + ", description=" + description + ", available=" + available
//                + ", permissions=" + permissions + "]";
//    }

}

 

 


@Controller
public class HomeController {
    @RequestMapping({"/","/index"})
    public String index() {
        return "index";
    }
    
    @RequestMapping("/login")
    public String login() {
        return "login";
    }
}

启动项目,数据库表会自动建好,index和login页面也可以访问。

(二)身份校验和角色设置

2.1 数据库中插入数据

INSERT INTO test.sys_permission VALUES ('1', 1, '用户管理', '0', '0/', 'userInfo:view', 'menu', 'userInfo/userList');
INSERT INTO test.sys_permission VALUES ('2', 1, '用户添加', '1', '0/1', 'userInfo:add', 'button', 'userInfo/userAdd');
INSERT INTO test.sys_permission VALUES ('3', 1, '用户删除', '1', '0/1', 'userInfo:del', 'button', 'userInfo/userDel');

INSERT INTO test.sys_role VALUES ('1', 1, '管理员', 'admin');
INSERT INTO test.sys_role VALUES ('2', 1, 'VIP会员', 'vip');

INSERT INTO test.user_info (uid,username,name,password,salt,state) VALUES ('1', 'admin','管理员' , 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);

INSERT INTO test.sys_role_permission VALUES ('1', '1');
INSERT INTO test.sys_role_permission VALUES ('1', '2');

INSERT INTO test.sys_user_role VALUES ('1', '1');

2.2 .Realms

Realm是一个Dao,通过它来验证用户身份和权限。这里Shiro不做权限的管理工作,需要我们自己管理用户权限,只需要从我们的数据源中把用户和用户的角色权限信息取出来交给Shiro即可。
config包下再建一个包Shiro,然后在Shiro包下建一个MyShiroRealm类,继承AuthorizingRealm抽象类。

package com.bobby.springboot.config;

import com.bobby.springboot.entity.SysPermission;
import com.bobby.springboot.entity.SysRole;
import com.bobby.springboot.entity.UserInfo;
import com.bobby.springboot.service.UserInforService;
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 org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Created by Bobby on 2019/9/23 14:14
 */
public class MyShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserInforService userService;


// 配置用户权限   访问页面的时  链接配置了相应的权限或者是shiro标签才能执行此方法
//    否则不执行
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("开始配置权限");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        UserInfo userInfo = (UserInfo)principalCollection.getPrimaryPrincipal();

        for (SysRole role : userInfo.getRoleList()){
//            添加 角色
            info.addRole(role.getRole());
            for (SysPermission p : role.getPermissions()){
//                添加权限
                info.addStringPermission(p.getPermission());
            }
        }
        return info;
    }



//    用户登录身份校验
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("开始身份校验");
        String userName = (String) authenticationToken.getPrincipal();
        UserInfo userInfo = userService.findByUsername(userName);
        if(null == userInfo){
//             //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,
//             就会在LoginController中抛出UnknownAccountException异常
            return null;
        }
//验证通过返回一个封装了用户信息的AuthenticationInfo实例即可。
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userInfo,//用户信息
                userInfo.getPassword(),//密码
                getName()//realm name
        );
//        设置盐
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(userInfo.getCredentialsSalt()));
        return authenticationInfo;
    }
}

重载以上两个方法来配置用户身份验证和权限验证。

别忘了在Service包下新建个UserInfoService和它的实现类:

2.3 service方法实现
 

/**
 * Created by Bobby on 2019/9/23 14:18
 */
public interface UserInforService {

    public UserInfo findByUsername(String username);
}
@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Resource
    private UserInfoRepository userInfoRepository;

    @Override
    public UserInfo findByUsername(String username) {

        System.out.println("UserInfoServiceImpl.findByUsername");
        return userInfoRepository.findByUsername(username);
    }
}

2.4 dao

/**
 * Created by Bobby on 2019/9/23 16:27
 */
public interface UserInfoRepository extends CrudRepository<UserInfo, Long> {


 
    public UserInfo findByUsername(String username);

    public UserInfo save(UserInfo userInfo);
}

2.5 接下来配置Shiro的关键部分

这里要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限


/**
 * Created by Bobby on 2019/9/23 14:46
 */
@Configuration
public class ShiroConfiguration {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean =  new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String,String> filterChainDefinitionMap = new LinkedHashMap();
        filterChainDefinitionMap.put("/logout","logout");
        filterChainDefinitionMap.put("/**","authc");
        filterChainDefinitionMap.put("/favicon.ico","anon");
    //authc 表示需要验证身份证才能访问 ,还有一些比如anon表示不需要验证身份就能访问等。
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("/index");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return  shiroFilterFactoryBean;
    }


    //SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager  securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return  securityManager;
    }


    @Bean
    public  MyShiroRealm myShiroRealm(){
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    //因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,
    // 需要告诉它我们用的是md5加密的,并且是加密了两次。同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。
    // 这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
    @Bean
    public  HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }

关于为什么设置filterChainDefinitionMap.put("/favicon.ico", "anon");请参考Shiro登录后下载favicon.ico问题

 

2.6 修改我们的HomeController中的/login请求

    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) {
        // 登录失败从request中获取shiro处理的异常信息。
        // shiroLoginFailure:就是shiro异常类的全类名.
        Object exception = request.getAttribute("shiroLoginFailure");
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println("账户不存在");
                msg = "账户不存在或密码不正确";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println("密码不正确");
                msg = "账户不存在或密码不正确";
            } else {
                System.out.println("其他异常");
                msg = "其他异常";
            }
        }
        map.put("msg", msg);
        // 此方法不处理登录成功,由shiro进行处理.
        return "login";
    }

这里@RequestMapping之所以没加method是因为如果用户没登录,Shiro会调用get方法请求/login,而后面我们在login页面会用post请求发送form表单,所以这里就没设置method(默认支持所有请求)。

好,启动项目。这时候我们再访问http://localhost:8080/index会跳转到登录页面,因为我们设置了filterChainDefinitionMap.put("/**", "authc");,所以要先验证身份才能访问。同时因为设置了shiroFilterFactoryBean.setLoginUrl("/login");,所以会跳转到登录页面。这时候在登录页面输入正确的用户名密码就可以登录了,登录成功会自动跳转到index页面。

 

(三)权限 (精确到按钮级别的权限控制)

3.1在以上的基础上,添加一个UserInfoController

package com.bobby.springboot.controller;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Created by Bobby on 2019/9/23 17:00
 */
@Controller
@RequestMapping("/userinfor")
public class UserInfoController {


    /**
     * 用户查询.
     * @return
     */
    @RequestMapping("/userList")
    @RequiresPermissions("userInfo:view")
    public String userInfo(){
        return "userInfo";
    }

    /**
     * 用户添加;
     * @return
     */
    @RequestMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String userInfoAdd(){
        return "userAdd";
    }

    @RequestMapping("/userDel")
    @RequiresPermissions("userInfo:del")
    public String userInfoDel() {
        return "userDel";
    }
}

这时候启动项目,上面的几个请求在登录之后我们都是可以访问的,并且不会执行doGetAuthorizationInfo()打印权限验证信息。如果我们要限制某些人必须有一定的权限才能访问,怎么办呢?

3.2 开启Shiro AOP注解支持

ShiroConfiguration中加入以下代码开启:

  /**
     *  开启shiro aop注解支持.
     *  使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

3.3然后,在controller中加入相应的注解即可:

 /**
     * 用户查询.
     * @return
     */
    @RequestMapping("/userList")
    @RequiresPermissions("userInfo:view")
    public String userInfo(){
        return "userInfo";
    }

    /**
     * 用户添加;
     * @return
     */
    @RequestMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String userInfoAdd(){
        return "userAdd";
    }

    @RequestMapping("/userDel")
    @RequiresPermissions("userInfo:del")
    public String userInfoDel() {
        return "userDel";
    }

这时候,我们再登录之后访问http://localhost:8080/userInfo/userDel就会报org.apache.shiro.authz.AuthorizationException异常了。同时后台会打印权限验证的信息。

关于怎么处理这个没权限的异常,如果是在ShiroConfiguration中配置403是不起作用的,具体请参考setUnauthorizedUrl("/403")不起作用.

说下,按钮级别的权限控制,在我们的项目中,我们使用的Beetl这个前段模板+layui.js(吐槽下,真的不是很好用喃,做数据渲染的时候),在显示的过程中,会在模板中判断是否有权限,如同@RequiresPermissions("userInfo:add")这个教研的效果是一样的,通过访问用户是否有该权限字段,来判断按钮牛是否显示,这样做到按钮级别的权限控制。

 

上面所有的代码参考 : 工程参考 springboot整合shiro 完整版 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值