SpringBoot integrates shiro's salt value encryption in detail

what is the salt value

First, let's talk about password security.

In a system, we usually store the user name and password in the database. If we directly store the password in the database, there will be great security risks, such as: the database is stolen, and hackers intercept it during transmission. This is very common. The problem is that the service where the database is located is not absolutely safe, and hackers may intercept the data you transmit during the transmission process, obtain some private data, and send it to the server after tampering.

So how do we solve this kind of problem? Security is being upgraded, and hacker technology is also improving. If a website is for users to visit, it is inevitable that some people will take advantage of loopholes. For passwords, we can use irreversible hashing algorithms . encryption. Although it is irreversible, it is not absolute. For example, some websites have very powerful libraries, such as: CMD5, PMD5, SOMD5 and other websites. They cannot be cracked, but they have a large number of key-values ​​that have been encrypted and stored in the database for decryption. value. And what we have to do is to increase their cracking difficulty, how to increase, this is the focus of this chapter.

Usually we use hash encryption: MD5, SHA. And salt value encryption, give a chestnut (pseudo code)

Username: admin

Password: 123

Salt value: qwe666

Encrypt with md5: md5( admin 123 qwe666 )

For the convenience of distinguishing, I add colors here. We can use md5 to encrypt such a string, and the difficulty of cracking is obvious. Of course, you can also freely match this encrypted string, such as

md5(adminmd5(123)qwe666)

You can also encrypt multiple layers. The salt value is a string of random strings. It is recommended that each user's salt value is different, and the length should be controlled by yourself. It is recommended to make it longer, and then iteratively encrypt several times to increase the difficulty of cracking. Encrypt and compare in the same way during verification.

Shiro implements encryption

When we call the login method of the Subject (you can refer to my previous article about the Subject), shiro will call the SecurityManage security manager, try to compare the password and log in, and what we have to do is to customize the Realm, our role, user, permission They are all data obtained from Realm, that is to say, SecurityManage authentication will definitely go to Realm to obtain some user information, which can be understood as a data source.

MyShiroRealm.java

public class MyShiroRealm extends AuthorizingRealm{

    @Autowired
    private AppUserService appUserService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        AppUser appUser  = (AppUser)principals.getPrimaryPrincipal();
        for(AppRole role:appUser.getRoleList()){
            //添加角色
            authorizationInfo.addRole(role.getRoleKey());
            for(AppFn p:role.getAppFnList()){
                //添加拥有的权限
                authorizationInfo.addStringPermission(p.getFnKey());
            }
        }
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //UsernamePasswordToken对象用来存放提交的登录信息
        UsernamePasswordToken token=(UsernamePasswordToken) authenticationToken;
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        AppUser user = appUserService.findByUsername(token.getUsername());

        //用户是否存在
        if(user==null){
            throw new UnknownAccountException();
        }
        //是否激活
        if(user!=null&&user.getStatus().equals(ConstantsUser.ZERO.getCode())){
            throw new  DisabledAccountException();
        }
        //是否锁定
        if(user!=null&&user.getStatus().equals(ConstantsUser.THREE.getCode())){
            throw new  LockedAccountException();
        }
        //若存在,将此用户存放到登录认证info中,无需自己做密码对比Shiro会为我们进行密码对比校验
        if(user!=null&&user.getStatus().equals(ConstantsUser.ONE.getCode())) {
            //这里盐值可以自定义
            ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername+user.getSalt());
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    user.getUsername(), //用户名
                    user.getPassword(), //密码
                    credentialsSalt,//salt=username+salt
                    getName()  //realm name
            );
            return authenticationInfo;
        }
        return null;
    }

}

SimpleAuthenticationInfo(username, password, salt value, current Realm)

We store the basic information of the user name in this class. Here we pass in our salt value. The salt value can be freely defined. It is recommended to set it more complicated. When we call the login method of the Subject and go to the AuthenticatingRealm#getAuthenticationInfo method

Here we will get our own defined Realm, and the next step is the method of AuthenticatingRealm#assertCredentialsMatch

The doCredentialsMatch here is the method of verifying the password. The CredentialsMatcher here provides the get/set method. Click on the implementation of the CredentialsMatcher to see me. We actually want to do hash encryption and choose

There are already defined methods in this class, including the parameters that need to be specified for encryption, and we only use the doCredentialsMatch method to limit the number of logins for verification. At this time, I choose to inherit this class and rewrite the doCredentialsMatch method.

RetryLimitHashedCredentialsMatcher.java

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

    private Cache<String, Integer> cache;
    @Getter
    @Setter
    /**
     * 自定义密码错误上限
     */
    private Integer retryMax;

    public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
        cache = cacheManager.getCache("passwordRetryCache");
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws ExcessiveAttemptsException {
        String username = (String)token.getPrincipal();
        Integer retryCount = cache.get(username);
        if(retryCount == null) {
            retryCount = new Integer(1);
            cache.put(username, retryCount);
        }


        if(retryCount > retryMax) {
            throw new ExcessiveAttemptsException("您已连续错误达" + retryMax + "次!请N分钟后再试");
        }

        if( cache.getClass().getName().contains("RedisCache")){
            cache.put(username, ++retryCount);
        }
        //调用父类的校验方法
        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            cache.remove(username);
        }else {
            throw new IncorrectCredentialsException("密码错误,已错误" + retryCount + "次,最多错误" + retryMax + "次");
        }
        return true;
    }

}

There is a section of super.doCredentialsMatch(token, info) in the code that calls the method of the parent class again, which enhances its original code. In the design mode

We call it the decorator mode. Of course, this is not the end. We just defined this class and did not inject it into it. To make it practical, we need to modify ShiroConfig.java

/**
     * 自定义Realm创建
     * @return
     */
    @Bean
    public MyShiroRealm myShiroRealm(CredentialsMatcher credentialsMatcher){
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        //将自定义的令牌set到了Realm
        myShiroRealm.setCredentialsMatcher(credentialsMatcher);
        return myShiroRealm;
    }

    /**
     * 交由SecurityManage管理
     * @return
     */
    @Bean
    @DependsOn("credentialsMatcher")
    public SecurityManager securityManager(CredentialsMatcher credentialsMatcher){
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm(credentialsMatcher));
        return securityManager;
    }

    /**
     * 功能增强
     * @param cacheManager
     * @return
     */
    @Bean(name = "credentialsMatcher")
    public CredentialsMatcher credentialsMatcher(CacheManager cacheManager) {
        RetryLimitHashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher(cacheManager);
        //加密方式
        credentialsMatcher.setHashAlgorithmName(properties.getAlgorithmName());
        //加密迭代次数
        credentialsMatcher.setHashIterations(properties.getIteration());
        //true加密用的hex编码,false用的base64编码
        credentialsMatcher.setStoredCredentialsHexEncoded(properties.getHexEncoded());
        //重新尝试的次数(自己定义的)
        credentialsMatcher.setRetryMax(properties.getRetryMax());
        return credentialsMatcher;
    }

So far our integration is complete, there is still a question, what should I do to register or add a user name? Here we continue to browse the source code, which is the HashedCredentialsMatcher#doCredentialsMatch method just now, let’s go inside

1、

2、

3、

Here we understand SimpleHash (encryption method, salt value, number of encryption times, number of iterations)

        String hashAlgorithmName = "MD5";//加密方式
        Object crdentials = "123456";//密码原值
        Object salt = "qwe";//盐值
        int hashIterations = 1024;//加密1024次
        String result = new SimpleHash(hashAlgorithmName,crdentials,salt,hashIterations).toBase64();
        System.out.println(result);

After getting the password, remember to call the toBase64() method. Shiro will also use toBase64 when verifying. So far, the analysis and implementation of the encrypted source code is over.

Please pay attention to me, I will often write some wonderful blog posts, share them with you, and make progress together.

Guess you like

Origin blog.csdn.net/qq_21046665/article/details/79782865