Single sign-on 3: Add RBAC permission verification model function understanding and implementation demo

1. RBAC authority model

RBAC (Role-Based Access Control) is a role-based access control model, which is used to manage user permissions and access control in the system. It clearly defines the relationship among users, roles and permissions to achieve flexible permission management and control.

1.1. The RBAC model mainly includes the following core concepts:

1. Model concept:

User (User): The actual operator in the system has a unique identifier.

Role: A collection of permissions that can be assigned to users, and a user can have one or more roles.

Permission: the specific operation permission in the system, which defines the operations that the user can perform, which can be the interface path URL of the function menu, or other fine-grained operation permissions.

2. Operating concept:

Authorization: the process of granting permissions to users or roles, that is, assigning corresponding roles or permissions to users.

Authentication: Verify the user’s identity and authority to ensure that the user has the legal authority to access system resources.

The basic principle of the RBAC model is: permission authorization should be based on roles, rather than directly authorized to specific users. Through the intermediary of roles, permissions are decoupled from users, and centralized management and flexible allocation of permissions are realized.

1.2 The advantages of the RBAC model include:

  • Simplified rights management: Through the role authorization mechanism, rights can be managed and maintained in a centralized manner, reducing the complexity of rights management.
  • Flexible authority allocation: By assigning appropriate roles to users, users’ authority can be flexibly controlled to achieve fine-grained access control.
  • Easy to expand and maintain: The RBAC model has good scalability and maintainability, and permissions can be added, deleted, and modified according to business needs without affecting the permission control of other parts.

In practical applications, the RBAC model can be realized by combining the authority management framework and the security framework. For example, using the Spring Security framework can easily implement the permission control of the RBAC model, by defining the relationship between roles, permissions and users, and combining annotations and configurations to achieve permission verification and access control.

In summary, the RBAC model is a flexible and extensible permission management model, which realizes the centralized management and flexible distribution of permissions through the authorization mechanism of roles, and provides an efficient, safe and maintainable access control mechanism for the system .

1.3, RBAC level

  • RBAC0: Users and roles are many-to-many, roles and permissions are many-to-many. The permissions owned by a user are the collection of all his roles.
  • RBAC1: Based on RBAC0, it introduces the concept of role hierarchy, that is, a role is divided into multiple levels, and the permissions corresponding to each level are different. When assigning permissions to users, they need to be assigned to the corresponding role levels. A role with a low level has fewer permissions, and a role with a high level of permissions is a collection of all permissions with a low role level.
  • RBAC2: Based on RBAC1, restrict role access.
    • For example, the restriction of mutually exclusive roles: when the same user is assigned to two roles, and the roles are mutually exclusive, the system should remind that only one of the roles can be selected. For example, if an employee has the role of business, he can create a settlement statement and submit it to the financial audit. At this time, the employee cannot be assigned the financial role, otherwise he will submit the settlement statement and review the settlement statement by himself.
    • Limitation on the number of roles: a user has a limited number of roles; a role is assigned a limited number of users.
    • Restrictions on prerequisites: If a user wants to obtain a higher-level role, he must first obtain the lower-level role. For example, if you want to obtain the authority of the product director, you need to start from the role of product assistant, then to the role of product manager, and finally to the role of product director.
  • RBAC3: Based on RBAC0, it integrates RBAC1 and RBAC2 and is the most comprehensive authority management.

1.4, RBAC model complexity

When there are a lot of users, the concept of user group can be designed, and user group can replace the original concept of user, which can play a role in batch operation of user roles;

The concept of permission group is similar. In the case of too many permissions, one by one authorization operation is too cumbersome. You can use permission group to operate in batches, which is very convenient.

1.5, simple steps for practical application in springSecruity

  1. User table, role table, permission table. Then the two association tables associate users with roles, and roles with permissions.
  2. When adding a user, give the user a specific user role.
  3. When logging in, user information will be queried, and role information will be put into token or cache (such as redis, the advantage of external cache is that it can temporarily control permissions).
  4. The operation after login will go through a custom filter. If the verification is passed, the user information can be put into the springSecruity context, and ThreadLocal (here because the login and verification of role permissions are single-threaded operations, using local threads is the fastest, faster than map and external caches are faster).
  5. If we want to use permissions and control without intrusion, we can make a custom annotation, the parameter is the role name, verify whether the role in the user information saved in ThreadLocal contains this role name, and return the boolean type.

2, demo implementation

Combining the two posts I wrote earlier, here is a demo that can implement a simple non-intrusive login and permission control scheme. SpringSecruity, jwt, redis, mysql, threadLocal and other technologies are used here

0, springboot configuration

The data source is configured by yourself, that is, redis and mysql are required in advance

server:
  port: 8083

spring:
  application:
    name: player
  redis:
    host: xxxxxxx
    port: xxxxxxxx

  datasource:
    url: jdbc:mysql://xxxxxxx:xxxx/xxxxxx
    username: root
    password: Aa@123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: none

mybatis:
  mapper-locations: classpath:mappers/*.xml
  type-aliases-package: com.loong.nba.player.pojo

1. Dependency

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

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

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

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.2</version>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.2</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.2</version>
        <scope>runtime</scope>
    </dependency>

    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>

    <!-- database driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.31</version>
    </dependency>
</dependencies>

2, util

package com.loong.nba.player.util;

import io.jsonwebtoken.*;
import io.json webtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @author jilong
 * @date 2023/5/18
 */
@Component
public class JwtUtil {

    private final Key secretKey;
    private final long expirationTime;

    @Autowired
    private StringRedisTemplate redisTemplate;

    public JwtUtil() {
        this.secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        this.expirationTime = 120000;

    }

    /**
     * Generate jwt token
     *
     * @param username username
     * @return token
     */
    public String generateToken(String username) {
        Date now = new Date();
        Date expirationDate = new Date(System. currentTimeMillis() + expirationTime);
        String token = Jwts. builder()
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expirationDate)
                .signWith(secretKey, SignatureAlgorithm.HS256)
                .compact();
        redisTemplate.opsForValue().set(username, token, expirationTime, TimeUnit.MILLISECONDS);

        return token;
    }

    /**
     * parse token
     *
     * @param token token
     * @return content
     */
    public String getUsernameFormToken(String token) {
        return extractClaims(token).getBody().getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = extractClaims(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    private Jws<Claims> extractClaims(String token) {
        JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
        return parser. parseClaimsJws(token);
    }
}
package com.loong.nba.player.util;

import com.loong.nba.player.pojo.LoginUserBO;

public class SessionUtils {
    static ThreadLocal<LoginUserBO> loginUser = new ThreadLocal<>();

    public static LoginUserBO getLoginUser() {
        return loginUser. get();
    }

    public static void setLoginUser(LoginUserBO loginUserBO) {
        loginUser.set(loginUserBO);
    }
}

3, pojo

package com.loong.nba.player.pojo;

public class UserPO {

    private Integer id;
    private String username;
    private String password;
    private String salt;

    public Integer getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

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

    public String getSalt() {
        return salt;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }
}
package com.loong.nba.player.pojo;

import java.util.List;

public class LoginUserBO {
    private String username;
    private Integer userId;
    private String password;
    private List<Role> roleList;

    public String getUsername() {
        return username;
    }

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

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this. userId = userId;
    }

    public String getPassword() {
        return password;
    }

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

    public List<Role> getRoleList() {
        return roleList;
    }

    public void setRoleList(List<Role> roleList) {
        this.roleList = roleList;
    }
}
package com.loong.nba.player.pojo;

public class Role {
    private Integer id;
    private String roleName;

    public Integer getId() {
        return id;
    }

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

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }
}
package com.loong.nba.player.pojo;

/**
 * @author jilong
 * @date 2023/5/19
 */
public class UserDO {
    private String name;
    private String password;

    public UserDO() {
    }

    public UserDO(String name, String password) {
        this.name = name;
        this.password = password;
    }

    public String getName() {
        return name;
    }

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

    public String getPassword() {
        return password;
    }

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

4, config

package com.loong.nba.player.config;

import com.loong.nba.player.filter.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author jilong
 * @date 2023/5/18
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login","/user").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

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

}

5, dao layer

package com.loong.nba.player.dao;

import com.loong.nba.player.pojo.Role;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface RoleMapper {

    List<Role> findByUserId(Integer id);

}
package com.loong.nba.player.dao;

import com.loong.nba.player.pojo.UserPO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface UserMapper {

    UserPO findById(Integer id);

    UserPO findByName(@Param("name") String name);

    Integer addUser(@Param("userPO") UserPO userPO);

}

6, service

package com.loong.nba.player.service;

import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.pojo.UserPO;

public interface UserService {

    /**
     * Add user
     * @return userid
     */
     UserPO addUser(UserDO userDO);

     boolean comparePassword(UserDO userDO);


}
package com.loong.nba.player.service;

import com.loong.nba.player.dao.UserMapper;
import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.pojo.UserPO;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * @author jilong
 * @date 2023/5/19
 */
@Service
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;

    public UserServiceImpl(UserMapper userMapper) {
        this. userMapper = userMapper;
    }

    @Override
    public UserPO addUser(UserDO userDO) {

        // Define the length of the salt (number of bytes)
        int saltLength = 6;

        // create a secure random number generator
        SecureRandom secureRandom = new SecureRandom();

        // generate salt
        byte[] salt = new byte[saltLength];
        secureRandom. nextBytes(salt);

        // convert salt to string or byte array
        String saltString = encodeSalt(salt);

        // Compute password digest
        String password = encryptPassword(userDO. getPassword(), saltString);

        UserPO userPO = new UserPO();
        userPO.setUsername(userDO.getName());
        userPO.setPassword(password);
        userPO. setSalt(saltString);

        userMapper. addUser(userPO);

        System.out.println("Salt (Base64 string): " + saltString);
        return userPO;
    }

    String encodeSalt(byte[] salt) {
        return Base64.getEncoder().encodeToString(salt);
    }

    byte[] decodeSaltString(String saltString) {
        return Base64.getDecoder().decode(saltString);
    }

    String encryptPassword(String password, String salt) {

        String plaintext = password + salt;

        try {
            // Create a MessageDigest instance of the SHA-256 algorithm
            MessageDigest digest = MessageDigest.getInstance("SHA-256");

            // Calculate the digest of the Chinese string
            byte[] hash = digest.digest(plaintext.getBytes(StandardCharsets.UTF_8));

            // Convert the digest byte array to a hex string representation
            String digestHex = bytesToHex(hash);
            System.out.println("SHA-256 Digest:" + digestHex);
            return digestHex;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    // Convert the digest byte array to a hex string representation
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexStringBuilder = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex. length() == 1) {
                hexStringBuilder.append('0');
            }
            hexStringBuilder.append(hex);
        }
        return hexStringBuilder.toString();
    }


    @Override
    public boolean comparePassword(UserDO userDO) {

        UserPO user = userMapper.findByName(userDO.getName());
        String salt = user. getSalt();
        String password = encryptPassword(userDO. getPassword(), salt);
        return password. equals(user. getPassword());
    }

}

7, filter

package com.loong.nba.player.filter;

import com.loong.nba.player.dao.RoleMapper;
import com.loong.nba.player.dao.UserMapper;
import com.loong.nba.player.pojo.LoginUserBO;
import com.loong.nba.player.pojo.Role;
import com.loong.nba.player.pojo.UserPO;
import com.loong.nba.player.util.JwtUtil;
import com.loong.nba.player.util.SessionUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;

/**
 * @author jilong
 * @date 2023/5/18
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final String tokenHeader = "Authorization";
    private final String tokenPrefix = "Bearer ";

    private final JwtUtil jwtUtil;
    private final StringRedisTemplate redisTemplate;
    private final UserMapper userMapper;
    private final RoleMapper roleMapper;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, StringRedisTemplate redisTemplate, UserMapper userMapper, RoleMapper roleMapper) {

        this.jwtUtil = jwtUtil;
        this.redisTemplate = redisTemplate;
        this. userMapper = userMapper;
        this.roleMapper = roleMapper;
    }


    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request. getHeader(tokenHeader);
        if (!ObjectUtils.isEmpty(header) & amp; & amp; header.startsWith(tokenPrefix)) {
            String token = header. replace(tokenPrefix, "");
            if (!ObjectUtils.isEmpty(token) & amp; & amp; jwtUtil.validateToken(token)) {
                String username = jwtUtil. getUsernameFormToken(token);
                UserPO userPO = userMapper.findByName(username);
                List<Role> roleList = roleMapper.findByUserId(userPO.getId());
                LoginUserBO loginUserBO = new LoginUserBO();
                loginUserBO.setUserId(userPO.getId());
                loginUserBO. setUsername(username);
                loginUserBO.setPassword(userPO.getPassword());
                loginUserBO.setRoleList(roleList);

                if (redisTemplate.opsForValue().get(username) != null) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                            username, null, Collections.emptyList());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                    SessionUtils.setLoginUser(loginUserBO);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

8, controller

package com.loong.nba.player.controller;

import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.pojo.UserPO;
import com.loong.nba.player.service.UserServiceImpl;
import com.loong.nba.player.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;

/**
 * @author jilong
 * @date 2023/5/15
 */
@RestController
public class LoginController {

    private final JwtUtil jwtUtil;

    private final UserServiceImpl userService;

    public LoginController(JwtUtil jwtUtil, UserServiceImpl userService) {
        this.jwtUtil = jwtUtil;
        this. userService = userService;
    }


    @PostMapping("/login")
    public ResponseEntity<UserDO> postLogin(@RequestBody UserDO userDO, HttpServletResponse response) {

        if (!userService. comparePassword(userDO)) {
           return ResponseEntity.ok(new UserDO());
        }
        String token = jwtUtil.generateToken(userDO.getName());
        response.addHeader("Authorization", "Bearer " + token);
        return ResponseEntity.ok(userDO);
    }

    @PostMapping("/user")
    public UserPO addUser(@RequestBody UserDO userDO) {
        return userService. addUser(userDO);
    }

    @GetMapping("/test")
    @PreAuthorize("@permission.hasRole(" + "'Administrator'" + ")")
    public String test() {
        return "hello test";
    }
}

9, mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


<mapper namespace="com.loong.nba.player.dao.RoleMapper">
    <select id="findByUserId" resultType="Role">
        SELECT role.id , role.rolename FROM role where id
                  IN (SELECT role_id FROM user_role WHERE user_id = #{id})
    </select>

</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


<mapper namespace="com.loong.nba.player.dao.UserMapper">
    <select id="findById" resultType="UserPO">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <select id="findByName" resultType="UserPO">
        SELECT * FROM user WHERE username = #{name}
    </select>

    <insert id="addUser" parameterType="UserPO">
        INSERT INTO user (username, user_password, salt)
        VALUES (#{userPO.username}, #{userPO.password}, #{userPO.salt})
    </insert>
</mapper>