SpringBoot+Security dynamic configuration permission

Keywords: Spring Java Mybatis Database

Preface

In today's web development, the authentication of security rights has always played an important role, so Spring has also produced its own security module, but this is a relatively heavy framework, configuration is quite cumbersome. Later, shiro, a lightweight security framework, emerged. The methods provided in it also basically meet the needs of developers.
With the emergence of springboot, the official provides a series of out of the box starter s, and security gradually returns to people's vision, forming the commonly used technology stacks such as springboot+security or ssm+shiro.

SQL script

The database here uses MySQL 5.7

/*
SQLyog Ultimate v12.4.3 (64 bit)
MySQL - 5.7.17-log : Database - security
*********************************************************************
*/

/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`security` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `security`;

/*Table structure for table `menu` */

DROP TABLE IF EXISTS `menu`;

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `menu` */

insert  into `menu`(`id`,`pattern`) values 
(1,'/db/**'),
(2,'/admin/**'),
(3,'/user/**');

/*Table structure for table `menu_role` */

DROP TABLE IF EXISTS `menu_role`;

CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `menu_role` */

insert  into `menu_role`(`id`,`mid`,`rid`) values 
(1,1,1),
(2,2,2),
(3,3,3);

/*Table structure for table `role` */

DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `role` */

insert  into `role`(`id`,`name`,`nameZh`) values 
(1,'ROLE_dba','Database Administrator'),
(2,'ROLE_admin','system administrator'),
(3,'ROLE_user','user');

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `user` */

insert  into `user`(`id`,`username`,`password`,`enabled`,`locked`) values 
(1,'root','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0),
(2,'admin','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0),
(3,'sang','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0);

/*Table structure for table `user_role` */

DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

/*Data for the table `user_role` */

insert  into `user_role`(`id`,`uid`,`rid`) values 
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

Add dependency

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>leo.study</groupId>
    <artifactId>security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security</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-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.17</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </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>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Connect to database

spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/security
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

Create entity class first

package leo.study.security.bean;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
public class User implements UserDetails
{
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private boolean locked;
    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        List<SimpleGrantedAuthority> authorities=new ArrayList<>();
        for (Role role : roles)
        {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired()
    {
        return true;
    }

    @Override
    public boolean isAccountNonLocked()
    {
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired()
    {
        return true;
    }

    @Override
    public boolean isEnabled()
    {
        return enabled;
    }

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    @Override
    public String getUsername()
    {
        return username;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }

    @Override
    public String getPassword()
    {
        return password;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }


    public void setEnabled(Boolean enabled)
    {
        this.enabled = enabled;
    }


    public void setLocked(boolean locked)
    {
        this.locked = locked;
    }

    public List<Role> getRoles()
    {
        return roles;
    }

    public void setRoles(List<Role> roles)
    {
        this.roles = roles;
    }
}

The user information table stores the user login name, password, account lock, account available flag and other information, so the user details is implemented to save our user information.

The first thing we should pay attention to is:

	@Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
    	return null;
    }

Here is the user's role information, and we have defined a role and role class, so to get the user's corresponding role, we need to put the role into the user object.
How to get the user's role here?
First, create a List, because the return type of the inherited method is Collection, and then the generic type of the Collection is a inheriting class of GrantedAuthority, simplegratedauthority, and then start to traverse the roles, and put the role name by instantiating simplegratedauthority

  public SimpleGrantedAuthority(String role) {
        Assert.hasText(role, "A granted authority textual representation is required");
        this.role = role;
    }

Second, we should pay attention to:
I have enabled and locked fields in the database here, and the implemented UserDetails also returns these two fields to us. This is whether the account has not been locked. Pay attention to Non, so you need to reverse and tell him, yes, it has not been locked! I don't think there's anything else to say.

    @Override
    public boolean isAccountNonLocked()
    {
        return !locked;
    }
    @Override
    public boolean isEnabled()
    {
        return enabled;
    }
package leo.study.security.bean;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
public class Role
{
    private Integer id;
    private String name;
    private String nameZh;

    @Override
    public String toString()
    {
        return "Role{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", nameZh='" + nameZh + '\'' +
                '}';
    }

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public String getNameZh()
    {
        return nameZh;
    }

    public void setNameZh(String nameZh)
    {
        this.nameZh = nameZh;
    }
}

Menu

The menu class should put the roles in, and then look at their corresponding menus according to the user roles

package leo.study.security.bean;

import java.util.List;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
public class Menu
{
    private Integer id;
    private String pattern;
    private List<Role> roles;

    @Override
    public String toString()
    {
        return "Menu{" +
                "id=" + id +
                ", pattern='" + pattern + '\'' +
                ", roles=" + roles +
                '}';
    }

    public List<Role> getRoles()
    {
        return roles;
    }

    public void setRoles(List<Role> roles)
    {
        this.roles = roles;
    }

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public String getPattern()
    {
        return pattern;
    }

    public void setPattern(String pattern)
    {
        this.pattern = pattern;
    }
}

Prepare UserService

UserService needs to implement UserDetailsService

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
    	return null;
    }

As the name implies, loadUserByUsername queries the user information through the user name. First, it determines whether the user exists, if so, it determines what role he has, and if not, it throws an exception prompt.

package leo.study.security.service;

import leo.study.security.bean.User;
import leo.study.security.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
@Service
public class UserService implements UserDetailsService
{
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        User user=userMapper.loadUserByUsername(username);
        //Judge if the query result is empty
        if(user==null){
            throw new UsernameNotFoundException("user does not exist");
        }
        user.setRoles(userMapper.getRolesById(user.getId()));
        return user;
    }
}

Corresponding mapper class

package leo.study.security.mapper;

import leo.study.security.bean.Role;
import leo.study.security.bean.User;

import java.util.List;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
public interface UserMapper
{

    User loadUserByUsername(String username);

    List<Role> getRolesById(Integer id);
}

mapper's profile

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="leo.study.security.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="leo.study.security.bean.User">
        select * from user where username=#{username};
    </select>

    <select id="getRolesById" resultType="leo.study.security.bean.Role">
        select * from role where id in(select rid from user_role where uid=#{id});
    </select>
</mapper>

Start writing configuration class

package leo.study.security.config;

import leo.study.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    UserService userService;

    //encryption
    @Bean
    PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userService);
    }
}

Some of the bags on it have not been removed. I don't care. First of all, BCryptPasswordEncoder is password encryption. The password submitted by the current security must be encrypted. Second, the @ Configuration annotation that I made has forgotten to add. No wonder I can't find the problem for half a day.

Write a controller

@RestController
public class HelloController
{
    @GetMapping("/hello")
    public String hello(){
        return "hello";
}

Start testing

security will default to its login page

Request after login, no problem!

Of course, this is the most basic. What we want to achieve is to enable the corresponding users to access the path they should access.

Query the menu path that the corresponding user can access

We have prepared the entity class before, and now write the service layer

menuservice

package leo.study.security.service;

import leo.study.security.bean.Menu;
import leo.study.security.bean.Role;
import leo.study.security.mapper.MenuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
@Service
public class MenuService
{
    @Autowired
    MenuMapper menuMapper;
    public List<Menu> getAllMenus(){
        return menuMapper.getAllMenus();
    }
}

MenuMapper

package leo.study.security.mapper;

import leo.study.security.bean.Menu;

import java.util.List;

public interface MenuMapper
{
    List<Menu> getAllMenus();
}

configuration file

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="leo.study.security.mapper.MenuMapper">
    <resultMap id="BaseResultMap" type="leo.study.security.bean.Menu">
        <id property="id" column="id"></id>
        <result property="pattern" column="pattern"></result>
        <collection property="roles" ofType="leo.study.security.bean.Role">
            <id property="id" column="rid"></id>
            <result property="name" column="rname"></result>
            <result property="nameZh" column="rnameZh"></result>
        </collection>
    </resultMap>
    <select id="getAllMenus" resultMap="BaseResultMap">
        select m.*,r.id rid,r.name rname,r.nameZh rnameZh 
        from menu m left join menu_role mr on m.id=mr.mid left join role r on mr.rid=r.id
    </select>
</mapper>

After preparation, write a filter in the configuration class to obtain the request address of the current user and compare it with all his permissions to see whether it is consistent. He only makes comparisons here. There will be a processing class later

package leo.study.security.config;

import leo.study.security.bean.Menu;
import leo.study.security.bean.Role;
import leo.study.security.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

/**
 * @description:
 * Main functions:
 * Analysis request address
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource
{
    //Path matching
    AntPathMatcher antPathMatcher=new AntPathMatcher();

    @Autowired
    MenuService menuService;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException
    {
        //Get the requested address
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        List<Menu> list = menuService.getAllMenus();//Get the request path corresponding to all user roles
        for (Menu menu : list)
        {
            //Start to compare whether the path obtained by the database is consistent with the request path
            if(antPathMatcher.match(menu.getPattern(),requestUrl)){
                //When the path is completely consistent, it depends on which roles the path needs to request
                List<Role> roles = menu.getRoles();
                //Create an array and place roles
                String[] roleStr=new String[roles.size()];
                //Consider that a person may have multiple roles and need to traverse
                for (int i = 0; i < roles.size(); i++)
                {
                    //Get role name
                    roleStr[i]=roles.get(i).getName();
                }
                //Return to character object
                return SecurityConfig.createList(roleStr);
            }
        }
        //If the requested method is not judged by the above methods, a role login flag will be returned. When you request the above methods, you cannot
        //When identifying the path, automatically jump to login
        return SecurityConfig.createList("ROLE_login");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes()
    {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass)
    {
        return false;
    }
}

MyAccessDecisionManager

package leo.study.security.config;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
@Component
public class MyAccessDecisionManager implements AccessDecisionManager
{
    //authentication saves the login information of the current user
    //o get current request object
    //Return value of collection public collection < configattribute > getattributes (object o)
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException
    {
        //Traversal collection
        for (ConfigAttribute attribute : collection)
        {

            if("ROLE_login".equals(attribute.getAttribute())){
                if(authentication instanceof AnonymousAuthenticationToken){
                    throw new AccessDeniedException("Illegal request");
                }else {
                    return;
                }
            }
            //This is the role of the login user
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities)
            {
                if(authority.getAuthority().equals(attribute.getAttribute())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("Illegal request");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute)
    {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass)
    {
        return true;
    }
}

Add configuration class

package leo.study.security.config;

import leo.study.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

/**
 * @description:
 * @author: Leo
 * @createDate: 2020/2/11
 * @version: 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    UserService userService;

    @Autowired
    MyFilter myFilter;

    @Autowired
    MyAccessDecisionManager myAccessDecisionManager;

    //encryption
    @Bean
    PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>()
        {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O o)
            {
                o.setSecurityMetadataSource(myFilter);
                o.setAccessDecisionManager(myAccessDecisionManager);
                return o;
            }
        }).and().formLogin().permitAll().and().csrf().disable();
    }
}

Published 18 original articles, won praise 12, visited 4364
Private letter follow

Posted by spaze on Tue, 11 Feb 2020 05:32:51 -0800