Shiro - Introduction to Shiro; Difference between Shiro and Spring Security; Spring Boot integrates Shiro

One, Shiro

The following is quoted from Baidu Encyclopedia

shiro (java security framework) - Baidu Encyclopedia

Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, password and session management. Using Shiro's easy-to-understand API, any application, from the smallest mobile application to the largest web and enterprise application, can be quickly and easily obtained.

(1) Main functions

Three core components: Subject, SecurityManager and Realms

1、Subject:

That is, the "current operating user". However, in Shiro, the concept of Subject does not only refer to people, it can also be a third-party process, a background account (Daemon Account) or other similar things. It simply means "something that is currently interacting with the software".

Subject represents the security operations of the current user, and SecurityManager manages the security operations of all users.

2、SecurityManager:

It is the core of the Shiro framework, a typical Facade mode, Shiro manages internal component instances through SecurityManager, and provides various services for security management through it.

3、Realm:

Realm acts as a "bridge" or "connector" between Shiro and application security data. That is to say, when performing authentication (login) and authorization (access control) verification on a user, Shiro will look up the user and its permission information from the Realm configured by the application.

In this sense, Realm is essentially a security-related DAO: it encapsulates the connection details of data sources and provides relevant data to Shiro when needed. When configuring Shiro, you must specify at least one Realm for authentication and/or authorization. It is possible to configure multiple Realms, but at least one is required.

Shiro has a built-in Realm that can connect to a large number of secure data sources (aka directories), such as LDAP, relational databases (JDBC), INI-like text configuration resources, and property files. If the default Realm does not meet your needs, you can also plug in your own Realm implementation that represents a custom data source.

(2) Basic function points

  • Authentication : identity authentication/login, verify whether the user has the corresponding identity;
  • Authorization : Authorization, that is, permission verification, verifies whether an authenticated user has a certain permission; that is, judges whether the user can do things, such as: verifying whether a certain user has a certain role. Or fine-grained verification of whether a user has a certain permission to a certain resource;
  • Session  Management : session management, that is, a session after the user logs in, and all its information is in the session before logging out; the session can be in a normal JavaSE environment or in a web environment;
  • Cryptography : Encryption to protect the security of data, such as encrypting and storing passwords in the database instead of plaintext;
  • Web Support : Web support, can be easily integrated into the Web environment;
  • Caching : Caching, for example, after a user logs in, his user information, roles/permissions do not need to be checked every time, which can improve efficiency;
  • Concurrency : Shiro supports concurrent verification of multi-threaded applications, that is, if another thread is opened in one thread, the permissions can be automatically propagated;
  • Testing : provide testing support;
  • Run As : allows a user to pretend to be another user (if they allow it);
  • Remember Me : Remember me, this is a very common function, that is, after logging in once, you don’t need to log in next time

(3) Features

1. Easy-to-understand Java Security API

2. Simple identity authentication (login), support for multiple data sources (LDAP, JDBC, Kerberos, ActiveDirectory, etc.)

3. Simple signing rights (access control) for roles, supporting fine-grained signing rights

4. Support first-level cache to improve application performance

5. Built-in POJO-based enterprise session management, suitable for Web and non-Web environments

6. Heterogeneous client session access

7. Very simple encryption API

8. It is not bundled with any framework or container and can run independently

2. The difference between Shiro and Spring Security

(一)Spring Security

Spring Security is a flexible and powerful authentication and access control framework to secure Spring-based Java web applications.

Spring Security is a heavyweight security framework that ensures authentication and authorization support for Spring-based applications. It is well integrated with Spring MVC and comes bundled with implementations of popular security algorithms.

Spring Security mainly implements Authentication (authentication, solving who are you?) and Access Control (access control, that is, what are you allowed to do?, also known as Authorization). Spring Security separates authentication and authorization architecturally and provides extension points. "Authentication" is the process of establishing a claimed role for a user, which can be a user, a device, or a system. "Authentication" refers to the ability for a user to perform an action in your application. Roles are established during the authentication process before reaching an authorization decision.

features

What Shiro can achieve, Spring Security can basically achieve, relying on the Spring system, but the advantage is that it is a member of the Spring family bucket, and the integration is more suitable. In terms of use, it is slightly more powerful than Shiro (but generally Shiro is enough)

(2) The difference between Shiro and Spring Security

1. Shiro is easier to use than Spring Security, that is, it is simpler to implement, and Shiro is basically sufficient for basic authorization and authentication

2. The Spring Security community has a higher degree of support (but it is difficult to install Spring Security). The Spring community has advantages in support and update maintenance, and it is better combined with Spring.

3. Shiro is powerful, simple and flexible. The project under Apache is more reliable, and it is not bound to any framework or container, and can run independently

personal understanding

Shiro is the first choice, quick to use, enough to use, high degree of freedom, some in Spring Security, Shiro also basically has it (the project does not use Spring, don't worry, just go to Shiro)

If the development project uses Spring, it may be more appropriate to use Spring Security; although Spring Security is more complicated, it has a stronger ability to combine with the Spring family, and it is a framework structure that can be selected with confidence

3. Spring Boot integrates Shiro

(1) Create Shiro environment

1. Create a common SpringBoot web project

2. Add Shiro dependency

<!-- Shiro 依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.8.0</version>
        </dependency>

Complete pom.xml file

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

    <groupId>com.company</groupId>
    <artifactId>shiro-boot</artifactId>
    <version>1.0.0</version>

    <name>shiro-boot</name>
    <description>Demo project for Spring Boot</description>

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

    <dependencies>
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <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>

        <!-- Shiro 依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.8.0</version>
        </dependency>

    </dependencies>

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

(2) Configure Shiro

1. Define the configuration class ShiroConfig

/**
 * /当前类是一个Spring的配置类,用于模拟Spring的配置文件
 *
 * @Configuration: 表示当前类作为配置文件使用(配置容器,放在类的上面;等同 xml 配置文件,可以在其中声明 bean )
 * @Bean: 将对象注入到Spring容器中(类似<bean>标签,放在方法上面);不指定对象的名称,默认是方法名是 id
 */
@Configuration
public class ShiroConfig {
    /**
     * 1、Subject:
     * 即“当前操作用户”。它仅仅意味着“当前跟软件交互的东西”。
     * Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
     *
     * 2、SecurityManager:
     * 它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
     *
     * 3、Realm:
     * Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
     */

    /**
     * 配置Shiro的安全管理器
     * @Bean: 将对象注入到Spring容器中(类似<bean>标签,放在方法上面);不指定对象的名称,默认是方法名是 id
     */
    @Bean
    public SecurityManager securityManager(Realm myRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    /**
     * 配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
     */
    @Bean
    public MyRealm myRealm(){
        MyRealm myRealm = new MyRealm();
        return myRealm;
    }

    /**
     * 配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
     * 如什么样的请求可以访问,什么样的请求不可以访问等等
     */
    @Bean
    public ShiroFilterFactoryBean  shiroFilterFactoryBean(SecurityManager securityManager){
        //创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();

        //设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
        shiroFilter.setSecurityManager(securityManager);
        //用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
        //作用是用于通知Shiro我们可以使用这里路径转向到登录页面,但Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
        //要求用户完成成功
        shiroFilter.setLoginUrl("/");
        //登录成功后转向页面,由于用户的登录后期需要交给Shiro完成,因此就需要通知Shiro登录成功之后返回到那个位置
        shiroFilter.setSuccessUrl("/success");
        //用于指定没有权限的页面,当用户访问某个功能是如果Shiro判断这个用户没有对应的操作权限,那么Shiro就会将请求
        //转向到这个位置,用于提示用户没有操作权限
        shiroFilter.setUnauthorizedUrl("/noPermission");

        //定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问,什么样的请求不可以访问
        Map<String,String> filterChainMap = new LinkedHashMap<String,String>();

        // /login 表示某个请求的名字;anon 表示可以使用游客进行登录(这个请求不需要登录)
        filterChainMap.put("/login","anon");

        //我们可以在这里配置所有的权限规则,这列数据需要从数据库中读取出来

        //或者在控制器中添加Shiro的注解
        /**
         /admin/**  表示一个请求名字的通配, 以admin开头的任意子路径下的所有请求
         authc 表示这个请求需要进行认证(登录),只有认证(登录)通过才能访问
         注:
            ** 表示任意子路径
            *  表示任意的一个路径
            ? 表示 任意的一个字符
         */
        filterChainMap.put("/admin/**","authc");
        filterChainMap.put("/user/**","authc");

        //表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
        //如果没有指定/** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
        filterChainMap.put("/**","authc");

        shiroFilter.setFilterChainDefinitionMap(filterChainMap);

        return shiroFilter;
    }
}

2. Customize the MyRealm class

/**
 * 自定义Realm,用来实现用户的认证和授权
 * Realm:父类抽象类
 */
public class MyRealm implements Realm {

    @Override
    public String getName() {
        return null;
    }

    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        return false;
    }

    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

3. Define the UserController class

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

    @RequestMapping("/login")
    public String login(String username, String password, Model model){
        return "redirect:/success";
    }

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

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

    @RequestMapping("/user/test")
    public @ResponseBody
    String userTest(){
        return "这是userTest请求";
    }

    @RequestMapping("/admin/test")
    public @ResponseBody String adminTest(){
        return "这是adminTest请求";
    }

    @RequestMapping("/admin/add")
    public @ResponseBody String adminAdd(){
        Subject subject= SecurityUtils.getSubject();
        return "这是adminAdd请求";
    }
}

4. Define login.html, nopermission.html, success.html

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>

<form action="login" method="post">
    账号<input type="text" name="username"><br>
    密码<input type="text" name="password" id="password"><br>
    <input type="submit" value="登录" id="loginBut">
</form>
<span style="color: red" th:text="${errorMessage}"></span>

</body>

</html>

success.html 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>

<h1>登录成功</h1>

</body>

</html>

nopermission.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>

<h1>对不起!您没有权限操作!</h1>

</body>

</html>

 5. Start the test, enter   http://localhost:8080/ in the browser

Clicking to log in does not respond, even if you enter  http://localhost:8080/success

There is no response to the visit

At this point, if you comment out the ShiroConfig class 

//表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
//如果没有指定/** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
//filterChainMap.put("/**","authc");

 

Visit localhost:8080 again to jump to the success page 

(3) Configure Shiro authentication account

1. Customize the MyAuthenticatingRealm class

/**
 * 自定义Realm,用来实现用户的认证和授权
 * AuthenticatingRealm 只负责认证(登录)的Realm父类
 */
public class MyAuthenticatingRealm extends AuthenticatingRealm {
    /**
     * Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
     * @param authenticationToken   用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
     * @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证 密码是由Shiro进行验证是否合法
     * @throws AuthenticationException   如果认证失败,Shiro就会抛出AuthenticationException 也可以手动抛出这个AuthenticationException
     *      以及它的任意子异常类,不同的异常类型对应认证过程中的不同错误情况,我们需要根据异常类型来为用户返回特定的响应数据
     * AuthenticationException  异常的子类  可以自己抛出
     *      AccountException  账号异常  可以自己抛出
     *      UnknownAccountException  账号不存在的异常  可以自己抛出
     *      LockedAccountException   账号异常锁定异常  可以自己抛出
     *      IncorrectCredentialsException  密码错误异常(这个异常会在Shiro进行密码验证时抛出)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        //获取用户在浏览器中输入的账号
        String userName = token.getUsername();

        //认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
        String dbUserName = userName;

        if(!"admin".equals(dbUserName) && !"zhangsan".equals(dbUserName)){//判断用户账号是否正确
            throw  new UnknownAccountException("账号错误");
        }
        if("zhangsan".equals(userName)){
            throw  new LockedAccountException("账号被锁定");
        }
        //定义一个密码(这个密码应该来自数据库)
        String dbpassword = "123456";

        /**
         * 创建密码认证对象,由Shiro自动认证密码
         * 参数1  数据库中的账号(页面账号也可)
         * 参数2  数据库中的密码
         * 参数3  当前Relam的名字
         * 如果密码认证成功,会返回一个用户身份对象;如果密码验证失败则抛出异常
         */
        //认证密码是否正确
        return new SimpleAuthenticationInfo(dbUserName,dbpassword,getName());
    }
}

2. Modify the UserController class

@RequestMapping("/login")
    public String login(String username, String password, Model model){
        //创建一个shiro的Subject对象,利用这个对象来完成用户的登录认证
        Subject subject = SecurityUtils.getSubject();

        //判断当前用户是否已经认证过,如果已经认证过着不需要认证;如果没有认证过则完成认证
        if(!subject.isAuthenticated()){
            //创建一个用户账号和密码的Token对象,并设置用户输入的账号和密码
            //这个对象将在Shiro中被获取
            UsernamePasswordToken token = new UsernamePasswordToken(username,password);
            try {
                //如账号不存在或密码错误等等,需要根据不同的异常类型来判断用户的登录状态,并给予友好的信息提示
                //调用login后,Shiro就会自动执行自定义的Realm中的认证方法
                subject.login(token);
            } catch (UnknownAccountException e) {
                //表示用户的账号错误,这个异常是在后台抛出
                System.out.println("---------------账号不存在");
                model.addAttribute("errorMessage","账号不存在");
                return "login";
            }catch (LockedAccountException e){
                //表示用户的账号被锁定,这个异常是在后台抛出
                System.out.println("===============账号被锁定");
                model.addAttribute("errorMessage","账号被冻结");
                return "login";
            }catch (IncorrectCredentialsException e){
                //表示用户的密码错误,这个异常是shiro在认证密码时抛出
                System.out.println("***************密码不匹配");
                model.addAttribute("errorMessage","密码错误");
                return "login";
            }
        }
        return "redirect:/success";
    }

(4) Authentication cache

After successfully logging in once, we click Back and enter any account and password at this time. No matter what information is entered, even if you don’t enter anything and click the "Login" button directly, Shiro will consider the authentication successful. This is because after Shiro successfully logs in It will cause the data to be written into Shiro's cache, so a logout operation should be added in the controller of the login request before judging whether it has been authenticated, and the cache has been cleared so that the login can be repeatedly tested

Modify UserController 

/创建一个shiro的Subject对象,利用这个对象来完成用户的登录认证
Subject subject= SecurityUtils.getSubject();

//登出方法调用,用于清空登录时的缓存信息,否则无法重复登录
subject.logout();

Before logging out, check that the subject.isAuthenticated() status is true

Check subject.isAuthenticated() status is false after logout

(5) Password encryption

1. Simple back-end encryption

1. Modify the MyRealm class

/**
 * 自定义Realm,用来实现用户的认证和授权
 * AuthenticatingRealm 只负责认证(登录)的Realm父类
 */
public class MyAuthRealm extends AuthenticatingRealm {

/**
     * AuthenticatingRealm
     * Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
     * @param authenticationToken   用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
     * @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证 密码是由Shiro进行验证是否合法
     * @throws AuthenticationException   如果认证失败,Shiro就会抛出AuthenticationException 也可以手动抛出这个AuthenticationException
     *      以及它的任意子异常类,不同的异常类型对应认证过程中的不同错误情况,我们需要根据异常类型来为用户返回特定的响应数据
     * AuthenticationException  异常的子类  可以自己抛出
     *      AccountException  账号异常  可以自己抛出
     *      UnknownAccountException  账号不存在的异常  可以自己抛出
     *      LockedAccountException   账号异常锁定异常  可以自己抛出
     *      IncorrectCredentialsException  密码错误异常(这个异常会在Shiro进行密码验证时抛出)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        //获取用户在浏览器中输入的账号
        String userName = token.getUsername();
        String password = new String(token.getPassword());//获取页面中的用户密码实际工作中基本不需要获取
        System.out.println("账号:" + userName + " ----- 密码:" + password);

        //认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
        String dbUserName = userName;

        if(!"admin".equals(dbUserName) && !"zhangsan".equals(dbUserName)){//判断用户账号是否正确
            throw  new UnknownAccountException("账号错误");
        }
        if("zhangsan".equals(userName)){
            throw  new LockedAccountException("账号被锁定");
        }
        //定义一个密码(这个密码应该来自数据库)
        String dbpassword = "123456";

        /**
         * 数据密码加密主要是防止数据在浏览器访问后台服务器之间进行数据传递时被篡改或被截获,因此应该在前端到后台的过程中
         *      进行加密,而这里的加密方式是将浏览器中获取后台的明码加密和对数据库中的数据进行加密
         * 这就丢失了数据加密的意义 因此不建议在这里进行加密,应该在页面传递传递时进行加密
         * 注:
         *   建议浏览器传递数据时就加密数据,数据库中存在的数据也是加密数据,必须保证前端传递的数据
         *      和数据主库中存放的数据加密次数以及盐规则都是完全相同的,否则认证失败
         */
        //设置让当前登录用户中的密码数据进行加密
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1);
        this.setCredentialsMatcher(credentialsMatcher);

        /**
         * 密码加密
         * 参数 1 为加密算法 我们选择MD5加密
         * 参数 2 为被加密的数据的数据
         * 参数 3 为加密时的盐值 ,用于改变加密后数据结果
         *     通常这个盐值需要选择一个表中唯一的数据,如表中的账号
         * 参数 4 为需要对数据使用指定的算法加密多少次
         */
        //对数据库中的密码进行加密
        Object obj = new SimpleHash("MD5",dbpassword,"",1);

        /**
         * 创建密码认证对象,由Shiro自动认证密码
         * 参数1  数据库中的账号(页面账号也可)
         * 参数2  数据库中的密码
         * 参数3  当前Relam的名字
         * 如果密码认证成功,会返回一个用户身份对象;如果密码验证失败则抛出异常
         */
        //认证密码是否正确
        return new SimpleAuthenticationInfo(dbUserName,obj.toString()/*dbpassword*/,getName());
    }



public static void main(String[] args) {
        //使用Shiro提供的工具类对数据进行加密
        //参数 1 为加密算法名 我们使用MD5这是一个不可逆的加密算法
        //参数 2 为需要加密的数据
        //参数 3 加密的盐值 用于改变加密结果的 不同的盐加密的数据是不一致的
        //参数 4 为加密的次数
        Object obj = new SimpleHash("MD5","123456","",1);
        System.out.println("123456使用MD5加密1次----   "+obj);
        Object obj2 = new SimpleHash("MD5","123456","",2);
        System.out.println("123456使用MD5加密2次----   "+obj2);
        Object obj3 = new SimpleHash("MD5","e10adc3949ba59abbe56e057f20f883e","",1);
        System.out.println("123456使用MD5加密1次后在对这个数据加密1次----   "+obj3);

        Object obj4 = new SimpleHash("MD5","123456","admin",1);
        System.out.println("123456使用MD5 加盐admin 加密1次----   "+obj4);
        Object obj5 = new SimpleHash("MD5","123456","admin1",1);
        System.out.println("123456使用MD5 加盐admin1 加密1次----   "+obj5);
    }
}

2. Front-end and back-end encryption 

1. Modify the MyRealm class

/**
 * 自定义Realm,用来实现用户的认证和授权
 * AuthenticatingRealm 只负责认证(登录)的Realm父类
 * AuthorizingRealm    负责认证(登录)和授权 的Realm父类
 */
public class MyAuthRealm extends AuthenticatingRealm/*AuthorizingRealm*/ {
    /**
     * AuthenticatingRealm
     * Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
     * @param authenticationToken   用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
     * @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证 密码是由Shiro进行验证是否合法
     * @throws AuthenticationException   如果认证失败,Shiro就会抛出AuthenticationException 也可以手动抛出这个AuthenticationException
     *      以及它的任意子异常类,不同的异常类型对应认证过程中的不同错误情况,我们需要根据异常类型来为用户返回特定的响应数据
     * AuthenticationException  异常的子类  可以自己抛出
     *      AccountException  账号异常  可以自己抛出
     *      UnknownAccountException  账号不存在的异常  可以自己抛出
     *      LockedAccountException   账号异常锁定异常  可以自己抛出
     *      IncorrectCredentialsException  密码错误异常(这个异常会在Shiro进行密码验证时抛出)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        //获取用户在浏览器中输入的账号
        String userName = token.getUsername();
        String password = new String(token.getPassword());//获取页面中的用户密码实际工作中基本不需要获取
        System.out.println("账号:" + userName + " ----- 密码:" + password);

        //认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
        String dbUserName = userName;

        if(!"admin".equals(dbUserName) && !"zhangsan".equals(dbUserName)){//判断用户账号是否正确
            throw  new UnknownAccountException("账号错误");
        }
        if("zhangsan".equals(userName)){
            throw  new LockedAccountException("账号被锁定");
        }
        //定义一个密码(这个密码应该来自数据库)
        //String dbpassword = "123456";

        /**
         * 数据密码加密主要是防止数据在浏览器访问后台服务器之间进行数据传递时被篡改或被截获,因此应该在前端到后台的过程中
         *      进行加密,而这里的加密方式是将浏览器中获取后台的明码加密和对数据库中的数据进行加密
         * 这就丢失了数据加密的意义 因此不建议在这里进行加密,应该在页面传递传递时进行加密
         * 注:
         *   建议浏览器传递数据时就加密数据,数据库中存在的数据也是加密数据,必须保证前端传递的数据
         *      和数据主库中存放的数据加密次数以及盐规则都是完全相同的,否则认证失败
         */
        //设置让当前登录用户中的密码数据进行加密
        /*HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1);
        this.setCredentialsMatcher(credentialsMatcher);*/

        /**
         * 密码加密
         * 参数 1 为加密算法 我们选择MD5加密
         * 参数 2 为被加密的数据的数据
         * 参数 3 为加密时的盐值 ,用于改变加密后数据结果
         *     通常这个盐值需要选择一个表中唯一的数据,如表中的账号
         * 参数 4 为需要对数据使用指定的算法加密多少次
         */
        //对数据库中的密码进行加密
        //Object obj = new SimpleHash("MD5",dbpassword,"",1);

        //数据库中 MD5 加密之后密码
        String dbMD5Password = "e10adc3949ba59abbe56e057f20f883e";

        /**
         * 创建密码认证对象,由Shiro自动认证密码
         * 参数1  数据库中的账号(页面账号也可)
         * 参数2  数据库中的密码
         * 参数3  当前Relam的名字
         * 如果密码认证成功,会返回一个用户身份对象;如果密码验证失败则抛出异常
         */
        //认证密码是否正确
        return new SimpleAuthenticationInfo(dbUserName,dbMD5Password/*obj.toString()*//*dbpassword*/,getName());
    }


    public static void main(String[] args) {
        //使用Shiro提供的工具类对数据进行加密
        //参数 1 为加密算法名 我们使用MD5这是一个不可逆的加密算法
        //参数 2 为需要加密的数据
        //参数 3 加密的盐值 用于改变加密结果的 不同的盐加密的数据是不一致的
        //参数 4 为加密的次数
        Object obj = new SimpleHash("MD5","123456","",1);
        System.out.println("123456使用MD5加密1次----   "+obj);
        Object obj2 = new SimpleHash("MD5","123456","",2);
        System.out.println("123456使用MD5加密2次----   "+obj2);
        Object obj3 = new SimpleHash("MD5","e10adc3949ba59abbe56e057f20f883e","",1);
        System.out.println("123456使用MD5加密1次后在对这个数据加密1次----   "+obj3);

        Object obj4 = new SimpleHash("MD5","123456","admin",1);
        System.out.println("123456使用MD5 加盐admin 加密1次----   "+obj4);
        Object obj5 = new SimpleHash("MD5","123456","admin1",1);
        System.out.println("123456使用MD5 加盐admin1 加密1次----   "+obj5);
    }
}

2. Modify login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script th:src="@{|/js/jquery-1.11.3.min.js|}"></script>
    <script th:src="@{|/js/jQuery.md5.js|}"></script>
    <script>
        $(function(){
            $("#loginBut").bind("click",function(){
                var v_md5password=$.md5($("#password").val());
                alert(v_md5password)
                $("#md5Password").val(v_md5password)
            })
        })
    </script>
</head>
<body>
<form action="login" method="post">
    账号<input type="text" name="username" id="username"><br>
<!--    密码<input type="text"name="password" id="password"><br>-->
    密码<input type="text" id="password"><br>
    <input type="hidden" name="password" id="md5Password">
    <input type="submit" value="登录" id="loginBut">
</form>
<span style="color: red" th:text="${errorMessage}"></span>

</body>
</html>

Note:

1. Normally, the password data stored in the database should not be plain code 123456, but encrypted data.

For example, e10adc3949ba59abbe56e057f20f883e, this is 123456 after using MD5 encryption. If the password in the database is already encrypted, you can choose not to encrypt it here.

2. If the password in the database has been encrypted, the password must be encrypted before the data is transmitted on the page, otherwise the login may fail

3. If encrypted transmission is selected, the password encryption times and salt in the page and database must be the same, otherwise the login must fail

(6) Authority distribution

To determine whether a user's permission can be used, we must first assign a permission to the user before it can be assigned. It needs to be configured in the MyRealm class and modify the inherited parent class

1. Modify the parent class inherited by the MyRealm class to the AuthorizingRealm class, and implement the abstract method doGetAuthorizationInfo

/**
 * 自定义Realm,用来实现用户的认证和授权
 * AuthenticatingRealm 只负责认证(登录)的Realm父类
 * AuthorizingRealm    负责认证(登录)和授权 的Realm父类
 */
public class MyAuthRealm extends AuthorizingRealm {

    /**
     * AuthorizingRealm
     * Shiro用户授权的回调方法
     * @param principalCollection
     * @return
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //从Shiro中获取用户名
        Object username = principalCollection.getPrimaryPrincipal();
        //创建一个SimpleAuthorizationInfo类的对象,利用这个对象需要设置当前用户的权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        //创建角色信息的集合
        Set<String> roles = new HashSet<String>();

        //根据账号到数据库中获取用户所对应的所有角色信息,并初始化到roles集合中
        if("admin".equals(username)){
            roles.add("admin");
            roles.add("user");
        }else if ("zhangsan".equals(username)){
            roles.add("user");
        }
        Set<String>psermission = new HashSet<String>();
        if("admin".equals(username)){
            psermission.add("admin:add");
        }
        //设置角色信息
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setStringPermissions(psermission);
        return simpleAuthorizationInfo;
    }
}

2. Modify ShiroConfig

    /**
     * 配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
     * 如什么样的请求可以访问,什么样的请求不可以访问等等
     */
    @Bean
    public ShiroFilterFactoryBean  shiroFilterFactoryBean(SecurityManager securityManager){
        //创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();

        //设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
        shiroFilter.setSecurityManager(securityManager);
        //用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
        //作用是用于通知Shiro我们可以使用这里路径转向到登录页面,但Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
        //要求用户完成成功
        shiroFilter.setLoginUrl("/");
        //登录成功后转向页面,由于用户的登录后期需要交给Shiro完成,因此就需要通知Shiro登录成功之后返回到那个位置
        shiroFilter.setSuccessUrl("/success");
        //用于指定没有权限的页面,当用户访问某个功能是如果Shiro判断这个用户没有对应的操作权限,那么Shiro就会将请求
        //转向到这个位置,用于提示用户没有操作权限
        shiroFilter.setUnauthorizedUrl("/noPermission");

        //定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问,什么样的请求不可以访问
        Map<String,String> filterChainMap = new LinkedHashMap<String,String>();

        // /login 表示某个请求的名字;anon 表示可以使用游客进行登录(这个请求不需要登录)
        filterChainMap.put("/login","anon");

        //我们可以在这里配置所有的权限规则,这列数据需要从数据库中读取出来

        //或者在控制器中添加Shiro的注解
        /**
         1、 /admin/**  表示一个请求名字的通配, 以admin开头的任意子路径下的所有请求
         authc 表示这个请求需要进行认证(登录),只有认证(登录)通过才能访问
         注:
            ** 表示任意子路径
            *  表示任意的一个路径
            ? 表示 任意的一个字符
         2、roles[admin] 表示 以/admin/**开头的请求需要拥有admin角色才可以访问否   则返回没有权限的页面
            perms[admin:add] 表示 /admin/test的请求需要拥有 admin:add权限才可访问
         注:admin:add仅仅是一个普通的字符串用于标记某个权限功能
         */
        filterChainMap.put("/admin/test","authc,perms[admin:add]");
        filterChainMap.put("/admin/**","authc,roles[admin]");
        filterChainMap.put("/user/**","authc,roles[user]");

        //表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
        //如果没有指定/** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
        //filterChainMap.put("/**","authc");

        shiroFilter.setFilterChainDefinitionMap(filterChainMap);

        return shiroFilter;
    }
}

After users have roles and permissions, they need to configure Shiro's permission rules

(7) Annotation-based permission control

1. Modify ShiroConfig, add

/**
     * 开启Shiro注解支持(例如@RequiresRoles()和@RequiresPermissions())
     * shiro的注解需要借助Spring的AOP来实现
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启AOP的支持
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

Note:

After starting the permission control of annotations, you need to delete the configuration rules for permission interception in the Shiro configuration class

filterChainMap.put("/admin/test","authc,perms[admin:add]");
filterChainMap.put("/admin/**","authc,roles[admin]");
filterChainMap.put("/user/**","authc,roles[user]");

2. Modify UserController 

@RequiresRoles(value = {"user"})
    @RequestMapping("/user/test")
    public @ResponseBody String userTest(){
        return "这个userTest请求";
    }

    //RequiresRoles  Shiro的注解 表示访问这功能必须要拥有 admin角色
    //注:如果需要支持多个角色,就直接填写多个角色名称即可;如 "admin","user"
    //RequiresRoles 属性 logical 用于在拥有多个角色时使用 取值为Logical.AND 表示并且的意思必须同时拥有多个角色 或
    //               Logical.OR 或者的意思,只要拥有多个角色中的其中一个即可
    //注:使用了注解以后需要配置Spring声明式异常捕获,否则将在浏览器中直接看到Shiro的错误信息而不是友好的信息提示
    @RequiresRoles(value = {"admin"})
    @RequestMapping("/admin/test")
    public @ResponseBody String adminTest(){
        return "这个adminTest请求";
    }

    //@RequiresPermissions 注解用于指定当前请求必须要拥有指定的权限名字为 admin:add才能访问
    //admin:add 只是一个普通的权限名称字符串,表示admin下的add功能
    @RequiresPermissions(value = {"admin:add"})
    @RequestMapping("/admin/add")
    public @ResponseBody String adminAdd(){
        Subject subject = SecurityUtils.getSubject();
        //验证当前用户是否拥有这个权限
        //subject.checkPermission();
        //验证当前用户是否拥有这个角色
        //subject.checkRole();
        return "这个adminAdd请求";
    }

    //配置一个Spring的异常监控,当工程抛出了value所指定的所以异常类型以后将直接进入到当前方法中
    @ExceptionHandler(value = {Exception.class})
    public String myError(Throwable throwable){
        //获取异常的类型,应该根据不同的异常类型进入到不通的页面显示不同提示信息
        System.out.println(throwable.getClass());
        System.out.println("---------------------------------");
        return "noPermission";
    }

Note:

An exception will be thrown after Shiro authentication fails, so you must configure a Spring exception monitoring method myError at this time, otherwise you will not be able to redirect to the error page after the current Shiro authority authentication fails

(8) Shiro label

Integrating Shiro tags with Thymeleaf

1. Add maven dependency

        <!-- 使用Thymeleaf整合Shiro标签 -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

2. ShiroConfig configures beans, Shiro tags and Thymeleaf integration

/**
     * 配置Shiro标签与Thymeleaf的集成
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

3. Success introduces the namespace

xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

Shiro tag syntax 

As an attribute control
< button   type = "button" shiro :authenticated = "true" >
     Access control
</ button >
As a label
< shiro :hasRole name = "admin" >
    < button type = "button" >
         Access control
    </ button >
</ shiro :hasRole >

Common label instructions

Guest tag
< shiro :guest >
    </ shiro :guest >
  When the user has no identity verification, the corresponding information is displayed, that is, the visitor access information.

user tag
< shiro :user >
    </ shiro :user >
  Displays the corresponding information after the user has been authenticated/remember me logged in.

The authenticated tag
< shiro :authenticated >
    </ shiro :authenticated >
  The user has been authenticated, that is, the Subject.login login is successful, not remembering me to log in.


notAuthenticated tag
< shiro :notAuthenticated >
    </ shiro :notAuthenticated >
  The user has been authenticated, that is, the user has not called Subject.login to log in, including remembering my automatic login is also not authenticated.

principal tag

< shiro :principal property = "username" >

< shiro :principal / >
  is equivalent to ((User)Subject.getPrincipals()).getUsername().

lacksPermission tag
< shiro :lacksPermission name ="org:create" >
    </ shiro :lacksPermission >
  If the current Subject has no permission, the body content will be displayed.

hasRole tag
< shiro :hasRole name = "admin" >
    </ shiro :hasRole >
 If the current Subject has a role, the body content will be displayed.

hasAnyRoles tag
< shiro :hasAnyRoles name ="admin,user" >
     <shiro :hasAnyRoles >
  If the current Subject has any role (or relationship), the body content will be displayed.

lacksRoletag
< shiro :lacksRole name ="abc" >
    </ shiro :lacksRole >
  If the current Subject has no role, the body content will be displayed.

hasPermissiontag
< shiro :hasPermission name ="user:create" >
    </ shiro :hasPermission >
  If the currentSubjecthas permission,the bodycontent

<shiro:hasAnyPermissions name="admin:add,admin:update">

</shiro:hasAnyPermissions>

If the current Subject has any permission (or relationship), the body content will be displayed.

<shiro:hasAllRoles name=""></shiro:hasAllRoles>

Must have the Select All role specified

<shiro:hasAllPermissions name=""></shiro:hasAllRoles>

Must have Select All permission specified

Guess you like

Origin blog.csdn.net/MinggeQingchun/article/details/126414384#comments_26193191