GitHub:github.com/baiyuliang/…

Generally, the commonly used Security modules of SpringBoot include Security under Spring and Shiro under Apache. Security is powerful but complex, while Shiro is relatively small and simple. Generally, Shiro can meet most of the requirements in our actual development. So more and more developers are using Shiro!

Functions of security module:

  • Authentication (login authentication/encryption)
  • Authorization (granting permissions, roles)
  • Session management
  • encryption
  • Remember that I
  • .

So, security module, is an essential thing for a Web site! Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro: Shiro

Since this article deals with roles and permissions, the two tables mentioned in the previous article must be created.

Role table:

Permission table:

The users table:

Note that the user table, the previous we use password, are clear text, this is certainly not recommended, here I change back to encryption and is salt (salt encryption, later will talk, data you can add their own, the corresponding Role and Permission javabean do not forget to add!

First, analyze the login authentication process:

  1. The user belongs to the account and password login;
  2. After receiving the parameters, the Controller enters the Shiro authentication process.
  3. Shiro authenticates the user name and password and saves the login information and enters the authorization process without returning an error message.
  4. During authorization, you need to obtain the user’s role and corresponding permissions according to the login information, and bind the role through Shiro.

According to the above analysis, we need to create Dao and Service corresponding to Role and Permission tables first:

RoleRepository:

package com.byl.springbootdemo.dao;

import com.byl.springbootdemo.bean.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleRepository extends JpaRepository<Role.Integer> {}Copy the code

RoleService:

package com.byl.springbootdemo.service;

import com.byl.springbootdemo.bean.Role;

public interface RoleService {

    Role getRoleById(Integer id);
}

Copy the code

RoleServiceImpl:

package com.byl.springboottest.service;

import com.byl.springboottest.bean.Role;
import com.byl.springboottest.dao.RoleRepository;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class RoleServiceImpl implements RoleService {

    @Resource
    RoleRepository roleRepository;

    @Override
    public Role getRoleById(Integer id) {
        returnroleRepository.findById(id).get(); }}Copy the code

PermissionRepository:

package com.byl.springbootdemo.dao;

import com.byl.springbootdemo.bean.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface PermissionRepository extends JpaRepository<Permission.Integer> {

    Permission findByRoleId(Integer role_id);

}

Copy the code

PermissionService:

package com.byl.springbootdemo.service;

import com.byl.springbootdemo.bean.Permission;

public interface PermissionService {

    Permission getPermissionByRoleId(Integer role_id);
}

Copy the code

PermissionServiceImpl:

package com.byl.springboottest.service;

import com.byl.springboottest.bean.Permission;
import com.byl.springboottest.dao.PermissionRepository;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class PermissionServiceImpl implements PermissionService {

    @Resource
    PermissionRepository permissionRepository;

    @Override
    public Permission getPermissionByRoleId(Integer role_id) {
        returnpermissionRepository.findByRoleId(role_id); }}Copy the code

Pom. XML is introduced into:

        <! -- Shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.6.0</version>
        </dependency>
        <!-- shiro整合redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.3.1</version>
        </dependency>
        <! -- Thymeleaf -- Shiro -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>
Copy the code

Application add attribute:

Session timeout (default 30 minutes) Shro.session. expireTime=30 shro.jessionId =bylCopy the code

Note that we are using Shiro, so the interceptor used in the previous article is no longer needed, because the interceptor is already provided by Shiro, so you can comment it out.

Create PermissionRealm:

package com.byl.springboottest.shiro;

import com.byl.springboottest.bean.Permission;
import com.byl.springboottest.bean.Role;
import com.byl.springboottest.bean.User;
import com.byl.springboottest.service.PermissionService;
import com.byl.springboottest.service.RoleService;
import com.byl.springboottest.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

public class PermissionRealm extends AuthorizingRealm {

    @Resource
    UserService userService;
    @Resource
    RoleService roleService;
    @Resource
    PermissionService permissionService;

    /** * authorized **@param principalCollection
     * @return* /
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println(Enter Authorization >>);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        Role role = roleService.getRoleById(user.getRoleId());
        System.out.println(Role "> >"+role.getName());
        simpleAuthorizationInfo.addRole(role.getName());// Roles: superadmin,admin,user
        Permission permission = permissionService.getPermissionByRoleId(role.getId());
        System.out.println("> >"+permission.getName());
        simpleAuthorizationInfo.addStringPermission(permission.getName());// Add permissions
        return simpleAuthorizationInfo;
    }

    /** * Authentication **@param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
            return null;
        }
        String username = (String) authenticationToken.getPrincipal();
        // Get user information
        User user = userService.getUserByName(username);
        if (user == null) {
            return null;
        } else {
            ByteSource salt = ByteSource.Util.bytes(user.getUsername() + user.getSalt());// The parameter must be the same as the encryption method (username + salt value)
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
            clearCachedAuthorizationInfo();
            returnsimpleAuthenticationInfo; }}/** * Cache clearance permission */
    public void clearCachedAuthorizationInfo(a) {
        this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals()); }}Copy the code

Rewriting AuthorizingRealm’s two methods: doGetAuthorizationInfo authorization and doGetAuthorizationInfo authentication. Since we use the salting method, we need an Util class to handle salt generation and password encryption logic:

SaltUtil:

package com.byl.springbootdemo.utils;

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

public class SaltUtil {

    public static String HASHALGORITHMNAME = "md5";// Encryption mode
    public static int HASHITERATIONS = 1024;// Encryption times

    public static String randomSalt(a) {
        // A Byte is two bytes. The generated Byte is 3 bytes, and the string length is 6
        SecureRandomNumberGenerator secureRandom = new SecureRandomNumberGenerator();
        String hex = secureRandom.nextBytes(3).toHex();
        return hex;
    }

    public static String encryptPassword(String username, String password) {
        String salt= randomSalt();
        String newPassword = new SimpleHash(HASHALGORITHMNAME, password,
                ByteSource.Util.bytes(username +salt),
                HASHITERATIONS
        ).toHex();
        System.out.println("salt>>"+salt);
        System.out.println("newPassword>>"+newPassword);
        returnnewPassword; }}Copy the code

What is salt? You know, salt spreader put some salt on the steak? Not too! Salt is actually the common MD5 encryption mode to do further processing, the purpose is to prevent violent cracking, basically no solution! We know that the common way of encryption, is in cleartext passwords for 1 to 2 times the md5 encryption, although not reversible, but in order to as secure as possible (to prevent brute force password library), at this time you can on the basis of the original text passwords, add some “impurities mixed in, and the impurity is dynamic, so even if he Jin Xian come, also may not be able to crack! In the code above, the password generation method is:

  1. Mr. Is a random salt value, each user’s salt value is not the same;
  2. When encrypting, the user name + salt value is mixed with the password entered by the user to obtain the final password.
  3. The salt value and the final password were recorded and stored in the database.

During login verification, the salt value and password saved in the database can be verified with the user’s account and password:

ByteSource salt = ByteSource.Util.bytes(user.getUsername() + user.getSalt());// The parameter must be the same as the encryption method (username + salt value)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName())
Copy the code

Create ShiroConfig:

package com.byl.springboottest.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.byl.springboottest.shiro.PermissionRealm;
import com.byl.springboottest.shiro.RolesAuthorizationFilter;
import com.byl.springboottest.utils.SaltUtil;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.timeout}")
    private int redisTimeout;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Value("${spring.redis.password}")
    private String redisPassword;

    @Value("${shiro.session.expireTime}")
    private int expireTime;

    @Value("${shiro.jessionid}")
    private String jessionId;


    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        // Authenticate all users
        map.put("/ * *"."authc");
        // Allow access
        map.put("/css/**"."anon");
        map.put("/images/**"."anon");
        map.put("/js/**"."anon");
        map.put("/lib/**"."anon");
        map.put("/login.html"."anon");
        map.put("/user/login"."anon");
        / / login
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // Error page, authentication failed to redirect
        shiroFilterFactoryBean.setUnauthorizedUrl("/error/403.html");
        // Page role permission control
        map.put("/level1/**"."anyRoleFilter[user,admin,superadmin]");
        map.put("/level2/**"."anyRoleFilter[admin,superadmin]");
        map.put("/level3/**"."anyRoleFilter[superadmin]");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("anyRoleFilter".new RolesAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        return shiroFilterFactoryBean;
    }

    /** * Custom password validator **@return* /
    @Bean
    public CredentialsMatcher credentialsMatcher(a) {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName(SaltUtil.HASHALGORITHMNAME);
        credentialsMatcher.setHashIterations(SaltUtil.HASHITERATIONS);
        return credentialsMatcher;
    }

    // Add your own validation method to the container
    @Bean
    public PermissionRealm permissionRealm(CredentialsMatcher credentialsMatcher) {
        PermissionRealm customRealm = new PermissionRealm();
        customRealm.setCredentialsMatcher(credentialsMatcher);
        return customRealm;
    }

    /** * Configure the SecurityManager **@return* /
    @Bean
    public SecurityManager securityManager(CredentialsMatcher credentialsMatcher) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(permissionRealm(credentialsMatcher));  / / set the realm
        securityManager.setSessionManager(sessionManager());    / / set up our sessionManager
        securityManager.setCacheManager(myRedisCacheManager()); / / set the cacheManager
        return securityManager;
    }


    /** * redisCacheManager Cache redis implementation * shiro-redis * We need a field to identify this Cache Object in redis to defined an id field which you can get unique id to identify this principal. * For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. For example, getUserId(), getUserName(), getEmail(), etc. * Default value is "id", that means your principal object has a method called "getId()" * *@return* /
    @Bean
    public RedisCacheManager myRedisCacheManager(a) {
        RedisCacheManager cacheManager = new RedisCacheManager();
        cacheManager.setRedisManager(redisManager());
        cacheManager.setPrincipalIdFieldName("username");// Primary key name (default ID)
        return cacheManager;
    }

    /** * configure shiro redisManager * shiro-redis **@return* /
    @Bean
    public RedisManager redisManager(a) {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisHost + ":" + redisPort);
        redisManager.setTimeout(redisTimeout);
        redisManager.setPassword(redisPassword);
        return redisManager;
    }

    /** * SessionManager * shiro-redis */
    @Bean
    public DefaultWebSessionManager sessionManager(a) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(expireTime * 60 * 1000);
        sessionManager.setSessionIdUrlRewritingEnabled(false);// Disable URL rewriting. Otherwise, the browser automatically adds xx/login after the URL. JSESSIONID=xxx
        sessionManager.setSessionIdCookie(getSessionIdCookie());
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /** * Change shiro's default JSSESSIONID name **@return* /
    @Bean
    public SimpleCookie getSessionIdCookie(a) {
        SimpleCookie simpleCookie = new SimpleCookie(jessionId);
        return simpleCookie;
    }

    /** * RedisSessionDAO shiro sessionDao layer is implemented via redis * shiro-redis */
    @Bean
    public RedisSessionDAO redisSessionDAO(a) {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        sessionDAO.setSessionIdGenerator(sessionIdGenerator());
        return sessionDAO;
    }

    /** * Session ID generator **@return* /
    @Bean
    public JavaUuidSessionIdGenerator sessionIdGenerator(a) {
        return new JavaUuidSessionIdGenerator();
    }

    /** * Turn on Shiro's annotations (e.g@RequiresRoles.@RequiresPermissions), using SpringAOP to scan classes using Shiro annotations and perform security logic validation if necessary * Configure the following the two beans (DefaultAdvisorAutoProxyCreator and AuthorizationAttributeSourceAdvisor) * * can implement this function@return* /
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(a) {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /*** * make authorization annotations work *@param securityManager
     * @return* /
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    // Configure ShiroDialect to work with thymeleaf and shiro tags
    @Bean
    public ShiroDialect shiroDialect(a) {
        return newShiroDialect(); }}Copy the code

RolesAuthorizationFilter:

package com.byl.springbootdemo.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.List;

/** * Role authorization filter */
public class RolesAuthorizationFilter extends AuthorizationFilter {

    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            return false;
        }

        List<String> roles = CollectionUtils.asList(rolesArray);
        boolean[] hasRoles = subject.hasRoles(roles);
        for (boolean hasRole : hasRoles) {
            if (hasRole) {
                return true; }}return false; }}Copy the code

I made some changes to the resource file:

Here you can download the source code for your reference: download.csdn.net/download/ba…

The login logic in UserController needs to be rewritten:

    @PostMapping("/login")
    public ResponseData login(@RequestParam Map<String, String> params) {
        logger.error(params.toString());
        if (params.get("username") = =null || params.get("password") = =null) return new ResponseData(-1."Account number or account number cannot be empty");
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(params.get("username"), params.get("password"));
// usernamePasswordToken.setRememberMe(true); // Remember the password
        try {
            subject.login(usernamePasswordToken);
            subject.hasRole("admin");
        } catch (UnknownAccountException e) {
            return new ResponseData(-1."User does not exist");
        } catch (AuthenticationException e) {
            return new ResponseData(-1."Wrong account or password");
        } catch (AuthorizationException e) {
            return new ResponseData(-1."No access");
        }
        return new ResponseData(1."Login successful");
    }
Copy the code

Parameters are commonly received in the following ways:

  • JavaBean: for example, login(@requestBody User User){}, you can fetch the field directly from the object, but remember to have @requestBody annotation (submit the parameter to the field);
  • Map: code like above, with @requestParam annotation (submit parameters corresponding to keys);
  • Specific parameters: for example, login(String username,String password){} (parameter annotation can be omitted, and the submitted parameter must correspond to the method parameter);

At this point, we can do login verification, we can through the form of web page registration, the encrypted password into the database, but I did not achieve this implementation to you! I use test method, directly generate salt and password, and stored in the database, you can also do the same test stage, can save a lot of time!

    @Test
    void testPwdSalt(a){
        SaltUtil.encryptPassword("admin"."admin");
    }
Copy the code

Execution Result:

salt>>d1af77
newPassword>>c4b33995b676a712c5b48a3c4fa38e85
Copy the code

Record these two values, save account for admin database, you can generate!

The browser into the login page, http://localhost:8080/byl/login, began to verify:

Login successful, view the console:

Authorization successful!

Note in Controller:

subject.login(usernamePasswordToken);
subject.hasRole("admin");
Copy the code

These two lines of code, the first is login authentication, the second is authorization! Open the Redis manager:

Prove that redis and Redis have been integrated successfully!