Shiro框架入门

news2025/1/15 13:10:26

概述

官网介绍如下:
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro™是一个强大且易用的Java安全框架,能够用于身份验证、授权、加密和会话管理。Shiro拥有易于理解的API,您可以快速、轻松地获得任何应用程序——从最小的移动应用程序到最大的网络和企业应用程序。
简而言之,Apache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。
Shiro能到底能做些什么呢?

  • 验证用户身份
  • 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限。
  • 在非 Web 或 EJB 容器的环境下可以任意使用Session API
  • 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
  • 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
  • 支持单点登录(SSO)功能
  • 支持提供“Remember Me”服务,获取用户关联信息而无需登录

Apache Shiro Features

Apache Shiro是一个全面的、蕴含丰富功能的安全框架。下图为描述Shiro功能的框架图:
在这里插入图片描述
Authentication(认证),Authorization(授权),Session Management(会话管理),Cryptography(加密)被Shiro框架的开发团队称之为应用安全的四大基石。

  • Authentication(认证):用户身份识别,通常被称为用户“登录”。
  • Session Management(会话管理):特定与用户的会话管理,甚至在非Web或EJB应用程序。
  • 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)。
    我们需要实现Realms的Authenticaton和Authorization。其中Authentication用来验证用户身份,Authorization是授权访问控制,用于对用户进行的操作授权,证明改用户是否运行进行当前操作,如访问某个链接,某个资源等。

Shiro认证过程

在这里插入图片描述
上图展示了 Shiro 认证的一个重要的过程,为了加深我们的印象,我们来自己动手来写一个例子,来验证一下,首先我们新建一个Maven工程,然后在pom.xml中引入相关依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

新建一个【AuthenticationTest】测试类:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;

public class AuthenticationTest {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before // 在方法开始前添加一个用户
    public void addUser() {
        simpleAccountRealm.addAccount("wmyskxz", "123456");
    }

    @Test
    public void testAuthentication() {

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true

        subject.logout(); // 登出

        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出false
    }
}

运行之后可以看到预想中的效果,先输出isAuthenticated:true表示登录认证成功,然后再输出isAuthenticated:false表示认证失败退出登录,再来一张图加深一下印象:
在这里插入图片描述
流程如下:

  1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
  2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

Shiro授权过程

在这里插入图片描述
跟认证过程大致相似,下面我们仍然通过代码来熟悉一下过程:

public class AuthenticationTest {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before // 在方法开始前添加一个用户,让它具备admin和user两个角色
    public void addUser() {
        simpleAccountRealm.addAccount("wmyskxz", "123456", "admin", "user");
    }

    @Test
    public void testAuthentication() {

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
        // 判断subject是否具有admin和user两个角色权限,如没有则会报错
        subject.checkRoles("admin","user");
//        subject.checkRole("xxx"); // 报错
    }
}

自定义 Realm

从上面我们了解到实际进行权限信息验证的是我们的 Realm,Shiro 框架内部默认提供了两种实现,一种是查询.ini文件的IniRealm,另一种是查询数据库的JdbcRealm,这两种来说都相对简单。
有了上面的对认证和授权的理解,我们先在合适的包下创建一个【MyRealm】类,继承 Shirot 框架的 AuthorizingRealm 类,并实现默认的两个方法:

package com.wmyskxz.demo.realm;

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.*;

public class MyRealm extends AuthorizingRealm {

    /**
     * 模拟数据库数据
     */
    Map<String, String> userMap = new HashMap<>(16);

    {
        userMap.put("wmyskxz", "123456");
        super.setName("myRealm"); // 设置自定义Realm的名称,取什么无所谓..
    }

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 从数据库获取角色和权限数据
        Set<String> roles = getRolesByUserName(userName);
        Set<String> permissions = getPermissionsByUserName(userName);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 模拟从数据库中获取权限数据
     *
     * @param userName
     * @return
     */
    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> permissions = new HashSet<>();
        permissions.add("user:delete");
        permissions.add("user:add");
        return permissions;
    }

    /**
     * 模拟从数据库中获取角色数据
     *
     * @param userName
     * @return
     */
    private Set<String> getRolesByUserName(String userName) {
        Set<String> roles = new HashSet<>();
        roles.add("admin");
        roles.add("user");
        return roles;
    }

    /**
     * 认证
     *
     * @param authenticationToken 主体传过来的认证信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.从主体传过来的认证信息中,获得用户名
        String userName = (String) authenticationToken.getPrincipal();

        // 2.通过用户名到数据库中获取凭证
        String password = getPasswordByUserName(userName);
        if (password == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
        return authenticationInfo;
    }

    /**
     * 模拟从数据库取凭证的过程
     *
     * @param userName
     * @return
     */
    private String getPasswordByUserName(String userName) {
        return userMap.get(userName);
    }
}

然后我们编写测试类,来验证是否正确:

import com.wmyskxz.demo.realm.MyRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class AuthenticationTest {

    @Test
    public void testAuthentication() {

        MyRealm myRealm = new MyRealm(); // 实现自己的 Realm 实例

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(myRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
        // 判断subject是否具有admin和user两个角色权限,如没有则会报错
        subject.checkRoles("admin", "user");
//        subject.checkRole("xxx"); // 报错
        // 判断subject是否具有user:add权限
        subject.checkPermission("user:add");
    }
}

Shiro 加密

在之前的学习中,我们在数据库中保存的密码都是明文的,一旦数据库数据泄露,那就会造成不可估算的损失,所以我们通常都会使用非对称加密,简单理解也就是不可逆的加密,而 md5 加密算法就是符合这样的一种算法。
在这里插入图片描述
如上面的 123456 用 Md5 加密后,得到的字符串:e10adc3949ba59abbe56e057f20f883e,就无法通过计算还原回 123456,我们把这个加密的字符串保存在数据库中,等下次用户登录时我们把密码通过同样的算法加密后再从数据库中取出这个字符串进行比较,就能够知道密码是否正确了,这样既保留了密码验证的功能又大大增加了安全性,但是问题是:虽然无法直接通过计算反推回密码,但是我们仍然可以通过计算一些简单的密码加密后的 Md5 值进行比较,推算出原来的密码

比如我的密码是 123456,你的密码也是,通过 md5 加密之后的字符串一致,所以你也就能知道我的密码了,如果我们把常用的一些密码都做 md5 加密得到一本字典,那么就可以得到相当一部分的人密码,这也就相当于“破解”了一样,所以其实也没有我们想象中的那么“安全”。

加盐 + 多次加密

既然相同的密码 md5 一样,那么我们就让我们的原始密码再加一个随机数,然后再进行 md5 加密,这个随机数就是我们说的盐(salt),这样处理下来就能得到不同的 Md5 值,当然我们需要把这个随机数盐也保存进数据库中,以便我们进行验证。
另外我们可以通过多次加密的方法,即使黑客通过一定的技术手段拿到了我们的密码 md5 值,但它并不知道我们到底加密了多少次,所以这也使得破解工作变得艰难。
在 Shiro 框架中,对于这样的操作提供了简单的代码实现:

String password = "123456";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times = 2;  // 加密次数:2
String alogrithmName = "md5";   // 加密算法

String encodePassword = new SimpleHash(alogrithmName, password, salt, times).toString();

System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodePassword);

输出:

原始密码是 123456 , 盐是: f5GQZsuWjnL9z585JjLrbQ==, 运算次数是: 2, 运算出来的密文是:55fee80f73537cefd6b3c9a920993c25 

SpringBoot 简单实例

第一步:新建SpringBoot项目,搭建基础环境

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</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>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

application.properties文件

#thymeleaf 配置
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
#缓存设置为false, 这样修改之后马上生效,便于调试
spring.thymeleaf.cache=false

#数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#显示SQL语句
spring.jpa.show-sql=true
#不加下面这句则不会默认创建MyISAM引擎的数据库
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重写的配置类,默认使用utf8编码
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig

第二步:新建实体类

新建一个【entity】包,在下面创建以下实体:
用户信息:

@Entity
public class UserInfo {
    @Id
    @GeneratedValue
    private Long id; // 主键.
    @Column(unique = true)
    private String username; // 登录账户,唯一.
    private String name; // 名称(匿名或真实姓名),用于UI显示
    private String password; // 密码.
    private String salt; // 加密密码的盐
    @JsonIgnoreProperties(value = {"userInfos"})
    @ManyToMany(fetch = FetchType.EAGER) // 立即从数据库中进行加载数据
    @JoinTable(name = "SysUserRole", joinColumns = @JoinColumn(name = "uid"), inverseJoinColumns = @JoinColumn(name = "roleId"))
    private List<SysRole> roles; // 一个用户具有多个角色

    /** getter and setter */
}

角色信息:

@Entity
public class SysRole {
    @Id
    @GeneratedValue
    private Long id; // 主键.
    private String name; // 角色名称,如 admin/user
    private String description; // 角色描述,用于UI显示

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

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

    /** getter and setter */
}

权限信息:

@Entity
public class SysPermission {
    @Id
    @GeneratedValue
    private Long id; // 主键.
    private String name; // 权限名称,如 user:select
    private String description; // 权限描述,用于UI显示
    private String url; // 权限地址.
    @JsonIgnoreProperties(value = {"permissions"})
    @ManyToMany
    @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
    private List<SysRole> roles; // 一个权限可以被多个角色使用

    /** getter and setter */
}

注意:这里有一个坑,还缠了我蛮久感觉,就是当我们想要使用RESTful风格返回给前台JSON数据的时候,这里有一个关于多对多无限循环的坑,比如当我们想要返回给前台一个用户信息时,由于一个用户拥有多个角色,一个角色又拥有多个权限,而权限跟角色也是多对多的关系,也就是造成了 查用户→查角色→查权限→查角色→查用户… 这样的无限循环,导致传输错误,所以我们根据这样的逻辑在每一个实体类返回JSON时使用了一个@JsonIgnoreProperties注解,来排除自己对自己无线引用的过程,也就是打断这样的无限循环。

根据以上的代码会自动生成user_info(用户信息表)、sys_role(角色表)、sys_permission(权限表)、sys_user_role(用户角色表)、sys_role_permission(角色权限表)这五张表,为了方便测试我们给这五张表插入一些初始化数据:

INSERT INTO `user_info` (`id`,`name`,`password`,`salt`,`username`) VALUES (1, '管理员','951cd60dec2104024949d2e0b2af45ae', 'xbNIxrQfn6COSYn1/GdloA==', 'wmyskxz');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (1,'查询用户','userInfo:view','/userList');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (2,'增加用户','userInfo:add','/userAdd');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (3,'删除用户','userInfo:delete','/userDelete');
INSERT INTO `sys_role` (`id`,`description`,`name`) VALUES (1,'管理员','admin');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);

第三步:配置 Shiro

新建一个【config】包,在下面创建以下文件:
MySQLConfig:

public class MySQLConfig extends MySQL5InnoDBDialect {
    @Override
    public String getTableTypeString() {
        return "ENGINE=InnoDB DEFAULT CHARSET=utf8";
    }
}

这个文件关联的是配置文件中最后一个配置,是让 Hibernate 默认创建 InnoDB 引擎并默认使用 utf-8 编码
MyShiroRealm:

public class MyShiroRealm extends AuthorizingRealm {
    @Resource
    private UserInfoService userInfoService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能进入这里说明用户已经通过验证了
        UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (SysRole role : userInfo.getRoles()) {
            simpleAuthorizationInfo.addRole(role.getName());
            for (SysPermission permission : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permission.getName());
            }
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户输入的账户
        String username = (String) authenticationToken.getPrincipal();
        System.out.println(authenticationToken.getPrincipal());
        // 通过username从数据库中查找 UserInfo 对象
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        UserInfo userInfo = userInfoService.findByUsername(username);
        if (null == userInfo) {
            return null;
        }

        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                userInfo, // 用户名
                userInfo.getPassword(), // 密码
                ByteSource.Util.bytes(userInfo.getSalt()), // salt=username+salt
                getName() // realm name
        );
        return simpleAuthenticationInfo;
    }
}

自定义的 Realm ,方法跟上面的认证授权过程一致
ShiroConfig:

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 拦截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");
        // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");
        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        filterChainDefinitionMap.put("/**", "authc");
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");

        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2); // 散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }

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


    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

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

    @Bean(name = "simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver
    createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError"); // 数据库异常处理
        mappings.setProperty("UnauthorizedException", "403");
        r.setExceptionMappings(mappings);  // None by default
        r.setDefaultErrorView("error");    // No default
        r.setExceptionAttribute("ex");     // Default is "exception"
        //r.setWarnLogCategory("example.MvcLogger");     // No default
        return r;
    }
}

Apache Shiro 的核心通过 Filter 来实现,就好像 SpringMvc 通过 DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
Filter Chain定义说明:
1、一个URL可以配置多个Filter,使用逗号分隔
2、当设置多个过滤器时,全部验证通过,才视为通过
3、部分过滤器可指定参数,如perms,roles
Shiro内置的FilterChain

  • anon:所有url都都可以匿名访问
  • authc: 需要认证才能进行访问
  • user:配置记住我或认证通过可以访问

第四步:准备 DAO 层和 Service 层

新建【dao】包,在下面创建【UserInfoDao】接口:

public interface UserInfoDao extends JpaRepository<UserInfo, Long> {
    /** 通过username查找用户信息*/
    public UserInfo findByUsername(String username);
}

新建【service】包,创建【UserInfoService】接口:

public interface UserInfoService {
    /** 通过username查找用户信息;*/
    public UserInfo findByUsername(String username);
}

并在该包下再新建一个【impl】包,新建【UserInfoServiceImpl】实现类:

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Resource
    UserInfoDao userInfoDao;

    @Override
    public UserInfo findByUsername(String username) {
        return userInfoDao.findByUsername(username);
    }
}

新建【controller】包,然后在下面创建以下文件:

第五步:controller层

HomeController:

@Controller
public class HomeController {

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

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

    @RequestMapping("/403")
    public String unauthorizedRole(){
        System.out.println("------没有权限-------");
        return "403";
    }
}

这里边的地址对应我们在设置 Shiro 时设置的地址
UserInfoController:

@RestController
public class UserInfoController {

    @Resource
    UserInfoService userInfoService;

    /**
     * 按username账户从数据库中取出用户信息
     *
     * @param username 账户
     * @return
     */
    @GetMapping("/userList")
    @RequiresPermissions("userInfo:view") // 权限管理.
    public UserInfo findUserInfoByUsername(@RequestParam String username) {
        return userInfoService.findByUsername(username);
    }

    /**
     * 简单模拟从数据库添加用户信息成功
     *
     * @return
     */
    @PostMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String addUserInfo() {
        return "addUserInfo success!";
    }

    /**
     * 简单模拟从数据库删除用户成功
     *
     * @return
     */
    @DeleteMapping("/userDelete")
    @RequiresPermissions("userInfo:delete")
    public String deleteUserInfo() {
        return "deleteUserInfo success!";
    }
}

第六步:准备页面

新建三个页面用来测试:
index.html:首页

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
index - 首页
</body>
</html>

login.html:登录页

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
错误信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
    <p>账号:<input type="text" name="username" value="wmyskxz"/></p>
    <p>密码:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登录"/></p>
</form>
</body>
</html>

403.html:没有权限的页面

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>403错误页</title>
</head>
<body>
错误页面
</body>
</html>

第七步:测试

  1. 编写好程序后就可以启动,首先访问http://localhost:8080/userList?username=wmyskxz页面,由于没有登录就会跳转到我们配置好的http://localhost:8080/login页面。登陆之后就会看到正确返回的JSON数据,上面这些操作时候触发MyShiroRealm.doGetAuthenticationInfo()这个方法,也就是登录认证的方法。
  2. 登录之后,我们还能访问http://localhost:8080/userAdd页面,因为我们在数据库中提前配置好了权限,能够看到正确返回的数据,但是我们访问http://localhost:8080/userDelete时,就会返回错误页面.

参考

https://zhuanlan.zhihu.com/p/54176956

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/94117.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[C/C++/初学者]万年历(输入年份和月份,输出对应的日历表——数组)

//本代码不考虑历法问题&#xff0c;设定1年1月1日为星期一。 //以星期日为始。 //完整代码在最下方。 根据题意&#xff0c;我们首先需要接收年份的月份的数据。 printf("请输入年份&#xff1a;");scanf("%d",&Year);printf("请输入月份&#x…

Unity Addressables资源管理 资源组设置

Addressables资源管理总目录 1.Schema 翻译为&#xff1a;模式&#xff0c;计划。 目前理解为每个组的打包设置。 默认设置是只有两个Content Update Restriciton 和Content Packing& Loading 2.Content Update Restriction 内容更新方式 Can Change Post Release&…

Qt扫盲-QToolBox理论总结

QToolBox理论总结1. 简述2. 常用功能3. 信号&用途1. 简述 QToolBox 是一个类似前端的抽屉容器&#xff0c;它有一组选项卡&#xff0c;每个选项卡会在其下方自带有一个QWidget 来会显示一些内容。每个选项卡在选项卡列表中都有一个索引位置。这个选项卡的位置也是有顺序的…

Kerberos的概述和认证原理

什么是 Kerberos ​ Kerberos 是一种计算机网络认证协议&#xff0c;用来在非安全网络中&#xff0c;对个人通信以安全的手段进行身份认证。这个词又指麻省理工学院为这个协议开发的一套计算机软件。软件设计上采用客户端/服务器结构&#xff0c;并且能够进行相互认证&#xff…

国产三维gis软件的行业赋能情况

自二十世纪六十年代世上第一个GIS——加拿大地理信息系统&#xff08;CGIS&#xff09;面世至今&#xff0c;短短的40多年&#xff0c;GIS技术性从咿呀学语迈向了健康成长&#xff0c;在土地规划、电力工程、电信网、大城市管道网、水利工程、消防安全、交通出行及其城乡规划等…

Webug靶场搭建详解

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是Webug靶场搭建详解。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对未授权设…

C++ Reference: Standard C++ Library reference: Containers: map: map: find

C官网参考链接&#xff1a;https://cplusplus.com/reference/map/map/find/ 公有成员函数 <map> std::map::find iterator find (const key_type& k); const_iterator find (const key_type& k) const;获取指向元素的iterator 在容器中搜索键值等于k的元素&…

助力软件供应链安全 蚂蚁集团多项产品入选信通院优秀案例

近日&#xff0c;由中国信息通信研究院&#xff08;以下简称“中国信通院”&#xff09;主办&#xff0c;中国通信标准化协会云计算标准和开源推进委员会承办的3SCON软件供应链安全会议以线上直播形式召开。会上主办方发布了首期《软件供应链厂商和产品名录》&#xff0c;蚂蚁集…

mongoose 的打印改造

若要 mongoose.c 里的打印输出则需要在 main 函数里显式调用 mg_log_set(level)&#xff0c;参数是打印等级&#xff0c;如&#xff1a; mg_log_set(MG_LL_INFO);这样设置之后&#xff0c;程序执行起来就可以实现打印输出了&#xff0c;但这个输出着实让人看得不太明白&#x…

基于Python多元线性回归、机器学习、深度学习在近红外光谱分析中的实践应用

【详情链接】&#xff1a;基于Python多元线性回归、机器学习、深度学习在近红外光谱分析中的实践应用https://mp.weixin.qq.com/s?__bizMzU5NTkyMzcxNw&mid2247527433&idx2&sn4a73e8451b57819dce1b660b55f7befa&chksmfe68aa63c91f2375d992e418ea6006ab8ea835e…

[附源码]Nodejs计算机毕业设计基于的企业人事管理系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

使用Springboot框架手撸一个安全、可靠的本地缓存工具

前言&#x1f353; 在实现本地缓存的时候&#xff0c;我们经常使用线程安全的ConcurrentHashMap来暂存数据&#xff0c;然后加上SpringBoot自带的Scheduled定时刷新缓存。虽然这样可以实现本地缓存&#xff0c;但既不优雅也不安全&#xff0c;一个好的本地缓存工具应该是这样搭…

python 之 numpy图片处理 矩阵操作

目录 一&#xff1a;垂直方向翻转(行逆序) 二&#xff1a;水平方向翻转(列逆序) 三&#xff1a;垂直、水平方向翻转(行、列逆序) 四&#xff1a;调整亮度&#xff0c;变明亮*2.0 五&#xff1a;调整亮度&#xff0c;变暗 六&#xff1a;垂直方向裁剪 七&#xff1a;水平…

ESP32 ESP-IDF LVGL8 显示中文

陈拓 2022/12/15-2022/12/16 1. 开发环境 《ESP32 ESP-IDF LVGL8.3.3移植》 ESP32 ESP-IDF LVGL8.3.3移植_晨之清风的博客-CSDN博客ESP32 ESP-IDF LVGL8.3.3移植。https://blog.csdn.net/chentuo2000/article/details/128269394?spm1001.2014.3001.5501 2. 使用LVGL自带的中…

C# 创建WebService接口并连接

创建WebService项目 首先安装下.NET Framework4.6.2-4.7.1开发工具。 然后就是新建 ASP.NET Web应用程序 项目。 输入项目名称WebServiceDemo 选择空&#xff0c;然后先去掉HTTPS配置。 项目创建好之后&#xff0c;开始添加asmx文件. 添加好之后在添加一个有参数的名为Hel…

牛客java刷题知识点总结(六)

内存引用地址 内存引用地址&#xff0c;是指栈中存放的地址&#xff0c;来指向堆中的某个位置。 int 是基本类型&#xff0c;数据直接存放在栈中&#xff0c;不存在内存引用地址的说法 A对 指向常量池里的"hello"。 B对 题中没说声明的a是局部变量。 C错 int a 1;并…

DSPE-PEG-N3,磷脂-聚乙二醇-叠氮 点击化学PEG试剂,可用于药物传递、基因转染和生物分子修饰

中文名称 叠氮聚乙二醇磷脂、磷脂聚乙二醇叠氮 简称 N3-PEG-DSPE、DSPE-PEG-N3 物理性质&#xff1a;米白色/白色固体或粘性液体取决于分子量。 溶剂&#xff1a; 溶于大部分有机溶剂&#xff0c;和水有很好的溶解性。 活性基团&#xff1a; N3 反应基…

深度学习(17)—— 度量学习

深度学习&#xff08;17&#xff09;—— 度量学习 文章目录深度学习&#xff08;17&#xff09;—— 度量学习一、What?二、paired-based loss1. Contrastive loss2. Triplet loss3. Triplet center loss4.N-pair loss5. Quadruplet loss6. Lifted Structure Loss昨天讨论的时…

Linux安装tomcat 保姆级教程

一、安装前提 安装tomcat&#xff0c;前提需要安装jdk&#xff0c;如果没有安装 linux安装jdk教程(手动安装/yum安装) 保姆级教程_ 来杯咖啡的博客-CSDN博客 二、安装教程 1、下载tomcat tar包 官网下载地址: Apache Tomcat - Welcome! 2、将下载的压缩包上…

实验4 路由配置

实验4 路由配置一、实验目的二、实验要求三、实验步骤&#xff0c;数据记录及处理四&#xff0e;实验总结一、实验目的 1、路由配置&#xff1b; 2、配置静态路由&#xff1b; 3、掌握RIP协议配置。 二、实验要求 写出PC0与PC1连通的基本要求和配置过程 三、实验步骤&#…