SpringBoot + Shiro realizes login authentication (implementation process + source code analysis + code display)

1 Overview

1.1 SpringBoot

What we need to do today is to use SpringBoot to cooperate with Shiro to achieve login authentication, so SpringBoot is indispensable. I believe everyone can use Shiro, and SpringBoot must not be bad, so I won't go into too much detail, we will mainly introduce Shiro.

1.2 Shiro

Shiro is a Java security framework. It is powerful and easy to use. Compared with Spring Security, it is more widely used. Shiro has great advantages in simplicity and flexibility while maintaining powerful functions.
Insert picture description here
Shiro's functions also include the following:

  • Authentication: Identity authentication, to verify whether a user has an identity.
  • Authorization: Permission verification, to verify whether an authenticated user has a certain permission. Determine "who" can access "what".
  • Session Management: session management, manage the session after the user logs in,
  • Cryptography: Encryption, use cryptography to encrypt data, such as encrypted passwords.
  • Web Support: Web support can be easily integrated into the Web environment.
  • Caching: Caching, caching user data,
  • Concurrency: Concurrency, Apache Shiro supports multi-threaded applications with concurrent functions, which means that it supports concurrent verification in multi-threaded applications.
  • Testing: Testing, which provides testing support.
  • Run as: Allow users to log in as other users.
  • Remember me: Remember me,
    among which Authentication and Authorization permission verification are better to eat together!
    Today we mainly talk about how to achieve identity authentication

2. Shiro realizes login authentication

Before using Shiro for login authentication, we must first understand a few conceptual issues. First, as an excellent security framework, Shiro must have a central management center, which is the DefaultWebSecurityManager class, which we will customize later All method objects will be added to this management center, which will control the methods we define to replace the default authentication method that comes with the system. Secondly, Realm is also a very important object. It is translated as "domain", which acts as a "bridge" or "connector" between Shiro and application security data. The method we customize needs to be written in a class that inherits AuthorizingRealm before it can be added to the management center. The last concept is Subject . Usually we understand the Subject object as a user. Similarly, it may also be a three-party program. It is an abstract concept and can be understood as any "thing" that interacts with the system is a Subject. We will use it to log in authentication.

Import pom dependencies

First create a SpringBoot project and import shiro and springboot pom dependencies.

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
                <!--shiro-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

After configuration, we go to set the configuration file application.yml, and set our springboot port number inside.

server:
  port: 8080  #自定义

Then set up our SpringBoot startup class.

@SpringBootApplication
public class ShiroApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(ShiroApplication.class);
    }
}

Configure the core method

After that, we configure a Controller method to interface with the page, pass in the account password to test the login situation and test the permissions before and after login.

package com.df.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;


@RestController
public class LoginController {
    
    
    //如果需要使用shiro长期登陆,设置subject的rememberMe属性并且设置允许的范围为user。authc不允许被rememberMe用户访问。
    //这就是我们传入账号密码测试的地方
    @PostMapping(value = "/doLogin")
    public void doLogin(@RequestParam(value = "username") String username,
                        @RequestParam(value = "password") String password){
    
    
        Subject subject = SecurityUtils.getSubject();
        try {
    
    
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
            subject.login(usernamePasswordToken);

            System.out.println("登陆成功");
        }catch (Exception e){
    
    
            e.printStackTrace();
            System.out.println("登陆失败");
        }
    }
	
    @RequestMapping(value = "/index")
    public String index(){
    
    
        System.out.println("欢迎来到主页");
        return "欢迎来到主页";
    }
    
    //我们可以使用postman进行调用测试 登录前后hello的区别
    @GetMapping(value = "/hello")
    public String hello(HttpServletRequest request){
    
    
        Cookie[] cookies = request.getCookies();
        System.out.println(cookies[0].getValue());
        return "hello";
    }
	//用来设置未登录用户跳转的方法
    @GetMapping(value = "/login")
    public String login(){
    
    
        return "Please Login !";
    }
	//注销方法
    @GetMapping(value = "/logout")
    public String logout(){
    
    
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        System.out.println("成功退出");
        return "success to logout";
    }
}

After setting up the Controller method, we go to create a Realm to set up login account authentication. Create a config file and set a MyRealm class to inherit AuthorizingRealm .
Insert picture description here
Then we will implement the authentication process. After inheriting, we need to implement two methods. One is the authentication method and the other is the authorization method. We only need to complete the authentication method in this section.

package com.df.config;

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

/**
 * @author Lin
 * @create 2020/7/15
 * @since 1.0.0
 * (功能):
 */
public class MyRealm extends AuthorizingRealm {
    
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        return null;
    }

    @Override //认证
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
       //从被shiro封装成的token中取出我们传入的username
        String username = (String) authenticationToken.getPrincipal();
		//这里应有一步去缓存或数据库查询的步骤,我省略了
        //我直接定义了一个username,如果用户名不匹配,则报错用户名不存在。
        if(!"LinJy".equals(username)){
    
    
            throw new UnknownAccountException("账号不存在");
        }
        //返回一个新封装的认证实体,传入的是用户名,数据库查出来的密码,和当前Realm的名字
        return new SimpleAuthenticationInfo(username, "123", this.getName());
    }
}

In the above, we have completed the account authentication process. There must be a lot of friends asking, after the account authentication, what about the password? Why is the password not authenticated together. Let’s start writing the authentication password. Shiro divides the account and password into two places for verification. If we don't define it ourselves, then shiro's default verification method will be called.
We recreate a MyCredentialsMatcher class inheriting SimpleCredentialsMatcher to implement our custom password verification method.

package com.df.config;

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;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;


public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
    
    
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    
    
        UsernamePasswordToken tokenResolve = (UsernamePasswordToken) token;
        String tokenPwd = new String(tokenResolve.getPassword());
        String infoPwd =(String) info.getCredentials();
        //调用当前类重写的equals方法来对比两个password是否一致,返回对比结果
        return super.equals(tokenPwd, infoPwd);
    }
}

After we set up the account and password, we need to set up a manager to bring our account and password verification components closer to shiro. That's right again our configuration class comes out. The main function of the configuration class is to assemble some of the accessories we made before, and set up an interceptor to intercept the login requests we need to log in. Not much to say directly on the code, there are specific comments in the code.

package com.df.config;


import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    
    
	//引入之前定义好的域
    @Bean
    MyRealm myRealm(){
    
    
        return new MyRealm();
    }
	//配置一个安全管理器
    @Bean
    DefaultWebSecurityManager securityManager(){
    
    
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        MyRealm myRealm = myRealm();
        //将我们配置好的密码校验放入域中
        myRealm.setCredentialsMatcher(myCredentialsMatcher());
        //将域添加到我们的安全管理器中
        manager.setRealm(myRealm);
        //设置Session管理器,配置shiro中Session的持续时间
        manager.setSessionManager(getDefaultWebSessionManager());

        return manager;
    }
	//引入密码校验
    @Bean
    public MyCredentialsMatcher myCredentialsMatcher(){
    
    
        return new MyCredentialsMatcher();
    }

    //设置session过期时间
    @Bean
    public DefaultWebSessionManager getDefaultWebSessionManager() {
    
    
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setGlobalSessionTimeout(1000 * 60);// 会话过期时间,单位:毫秒--->一分钟,用于测试
        defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
        defaultWebSessionManager.setSessionIdCookieEnabled(true);
        return defaultWebSessionManager;
    }



	//设置访问拦截器
    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean(){
    
    
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //传入安全管理器
        bean.setSecurityManager(securityManager());
        //传入未登录用户访问登陆用户的权限所跳转的页面
        bean.setLoginUrl("/login");

        //设置成功后返回页面
        bean.setSuccessUrl("/index");

        //访问未授权网页所跳转的页面
        bean.setUnauthorizedUrl("/unauthorized");
        Map<String, String> map = new LinkedHashMap<>();
        //允许  需要设置login为anon 否则登陆成功后无法成功跳转。
        map.put("/login", "anon");
        map.put("/doLogin", "anon");
        map.put("/index", "anon");
        //设置所有的请求未登录不允许进入。
        map.put("/**", "authc");
        bean.setFilterChainDefinitionMap(map);
        return bean;
    }

}

Then we can start SpringBoot for a try. It is recommended to first access the intercepted request such as: /hello, and you will find that it cannot be accessed, and then log in: /doLogin, and then access hello. I will post the source code to github, the address link is at the bottom, and a copy of dubbo+zookeeper integrated shiro is attached.

3. Shiro login authentication source code analysis

Next, let us delve into how shiro implements the verification of account and password step by step. First of all, we should start with the familiar /doLogin method.
Insert picture description here
After entering, we found that shiro gave the token we entered to the security manager for calling
Insert picture description here
. After entering the security manager, we found that the security manager called a login method, passed in our token, and entered this method to make the Realm account Certification.
Insert picture description here
The above is to judge whether the token we passed in is problematic, if not, enter the topic and call the authentication method. From the error report below, we can understand that it will make a judgment on our need to authenticate the account.
Insert picture description here
Then come here, the first step of this.assertRealmConfigured() is a judgment on Realm, which judges whether Realm is a null value, and then gets Realm. Because we configured a Realm before, we will call the only Realm, otherwise we will traverse the Realm, and then call the judgment one by one. The following is the code to call multiple Realms.
Insert picture description here
If we go deeper, we can see that it first judges whether the realm supports the token, and if it does, it calls the realm for authentication.
Insert picture description here
Then enter the realm to obtain the previously logged-in cache, if it is empty, the authentication process will start,
Insert picture description here
oops! It can be considered back to the method we are familiar with. This is the method we defined before to judge the user name. After the judgment is successful, the user name and the password found in the database are repackaged. Then return.
Insert picture description here
After performing the account authentication, the info information is returned, which can be seen on the picture. If the info is not null, then the password will be verified.
Insert picture description here
After entering, we first obtained a CredentialsMatcher object. For this object, we found its interface implementation class and found that we have implemented the password matching method, so we will call our custom object. Of course, if we do not customize, then the default encryption password will be used to compare authentication.
Insert picture description here
Insert picture description here
Then we continue down and enter the stage of password authentication.
Insert picture description here
We have entered a place we are familiar with again, and verify the two passwords. Calling methods of this class to verify the password is actually a single character traversal comparison. If it is not correct, it returns false;
Insert picture description here
then it ends eventually. Finished with flowers~ Put the address of the project below, and the friends who need it will pick it up.

Code address

Code source code address, pick up if you want~

Guess you like

Origin blog.csdn.net/qq_41762594/article/details/107361997