Slightly more complex background systems will involve user rights management. What are user rights? My understanding is that permission is the centralized management of data (the entity class of the system) and the operations that can be performed on the data (addition, deletion, search, and modification). To build a usable permission management system, three core classes are involved: one is the user User, the other is the role, and the last is the permission Permission. Next, this article will introduce how to build an interface-level permission management system step by step based on Spring Security 4.0.
- Related concepts
Permission = Resource + Privilege
Role = a set of low-level permissions
User = collection of roles (high-level roles) - Spring Security’s maven dependencies
Although the Spring Boot version has reached 2.0, some pits were found when it was used before, so it is recommended to use the relatively stable version 1.5 for the time being.
- Define the permission set of the system
Permissions are a collection of resources and the operations that can be performed on them. For our system, almost all entity classes can be regarded as a resource, and the common operations are adding, deleting, checking, and modifying four categories. Of course, according to our actual business needs, there may be other special operations, such as ours here Added an action to import users. Here is a brief list of two basic permission sets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[ { "resourceId":"permission", "resourceName":"Permissions", "privileges": { "read":"View", "write":"Add", "update":"Update", "delete":"Delete" } }, { "resourceId":"user", "resourceName":"User", "privileges": { "read":"View user list", "write":"Add User", "import":"Import users", "update":"Modify user information", "delete":"Delete user" } } ] |
In the definition of permissions, the key is the key of resourceId and privileges, and the combination of the two will be used to judge the user’s permissions later. I use the form of resourceId-privilege here to uniquely represent an operation on a resource.
- Role-related operations
The resource and operation permission collection class defines JsonPermissions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Data public class JsonPermissions { private List<SimplePermission> permissions; @Data public static class SimplePermission { /** * resource id */ private String resourceId; /** * resource name */ private String resourceName; /** * list of permissions */ private Map<String, String> privileges; /** * Is it abandoned */ private boolean abandon = false; } } |
The role class defines Role:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
import lombok.Data; import org.springframework.data.mongodb.core.mapping.Document; import java.util.List; @Document(collection = "role") @Data public class Role { @Id private String id; /** * Creation time */ private Long createdTime = System.currentTimeMillis(); /** * is it removed */ private Boolean isRemoved = false; /** * Role name, used for permission verification */ private String name; /** * Character Chinese name for display */ private String nickname; /** * Character description information */ private String description; /** * Is it built-in */ private boolean builtIn = false; /** * Role status, is it disabled */ private Boolean banned = false; /** * A list of actions that the character can perform */ private List<JsonPermissions.SimplePermission> permissions; /** * Character creator */ private String proposer; /** * The roles of Spring Security 4.0 and above all start with 'ROLE_' by default * @param name */ public void setName(String name) { if (name.indexOf("ROLE_") == -1) { this.name = "ROLE_" + name; } else { this.name = name; } } } |
- Assign roles to users
The Spring Security framework provides a basic user interface UserDetails, which provides basic user-related operations, such as obtaining username/password, whether the user account has expired, and whether user authentication has expired, etc. We need to implement this when we define our own User class. interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.*; @Data @NoArgsConstructor public class User implements UserDetails { public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); @Id private String id; /** * Creation time */ private Long createdTime = System.currentTimeMillis(); /** * User login name */ private String username; /** * User's real name */ private String realName; /** * User login password, the user's password should not be exposed to the client */ @JsonIgnore private String password; /** * user type */ private String type; /** * The enterprise/block id associated with the user */ private Map<String, Object> associatedResources = new HashMap<>(); /** * List of companies that users follow */ private List<String> favourite = new ArrayList<>(); /** * The user's role list in the system, and the user's operation authority will be restricted according to the role */ private List<String> roles = new ArrayList<>(); public void setPassword(String password) { this.password = PASSWORD_ENCODER.encode(password); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } |
- Create initial roles and super administrators for the system
If we impose access restrictions on all interfaces of the system, who will log in to the system as the initial user and create other users? Therefore, we need to define the initial role and initial user of the system, and automatically enter the initial role and initial user into the system when the system starts, and then use the initial user to log in to the system to create other business-related users. Define the super administrator role of the system: roles.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
[ { "name":"ROLE_ADMINISTRATOR", "nickname":"Administrator", "description":"System super administrator, users are not allowed to change", "banned": false, "state":"normal", "permissions":[ { "resourceId":"permission", "resourceName":"Permissions", "privileges": { "read":"View", "write":"Add", "update":"Update", "delete":"Delete" } }, { "resourceId":"user", "resourceName":"User", "privileges": { "read":"View user list", "write":"Add User", "import":"Import users", "update":"Modify user information", "delete":"Delete user" } } ] } ] |
Defines the initial admin user for the system: users.json
1 2 3 4 5 6 7 8 9 10 11 |
[ { "username":"admin", "realName":"Super Super Administrator", "password":"$2a$10$GhI1umKcTHysip4iSFXPXOQG1x9U.4eCWMEFwF/h3LBAt98K4o1B.", "number":"admin", "type":"system", "activated": true, "roles":["ROLE_ADMINISTRATOR"] } ] |
- Load system initialization role and user data
When the system is deployed, the initialization roles and users of the system need to be automatically loaded into the database, so that they can be used for normal login. Use the @Component and @PostConstruct annotations to automatically import initial roles and users at system startup.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
import com.google.gson.reflect.TypeToken; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.beans.factory.annotation.Value; import javax.annotation.PostConstruct; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.ArrayList; /** * System initialization configuration class, mainly used to load built-in data to the target database */ @Component public class SystemInitializer { @Value("${initialzation.file.users:users.json}") private String userFileName; @Value("${initialzation.file.roles:roles.json}") private String roleFileName; @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; @PostConstruct public boolean initialize() throws Exception { try { InputStream userInputStream = getClass().getClassLoader().getResourceAsStream(userFileName); if(userInputStream == null){ throw new Exception("initialzation user file not found: " + userFileName); } InputStream roleInputStream = getClass().getClassLoader().getResourceAsStream(roleFileName); if(roleInputStream == null){ throw new Exception("initialzation role file not found: " + roleFileName); } //Import the initial system super administrator role Type roleTokenType = new TypeToken<ArrayList<Role>>(){}.getType(); ArrayList<Role> roles = CommonGsonBuilder.create().fromJson(new InputStreamReader(roleInputStream, StandardCharsets.UTF_8), roleTokenType); for (Role role: roles) { if (roleRepository.findByName(role.getName()) == null) { roleRepository.save(role); } } //Import the initial sysadmin user Type teacherTokenType = new TypeToken<ArrayList<User>>(){}.getType(); ArrayList<User> users = CommonGsonBuilder.create().fromJson(new InputStreamReader(userInputStream, StandardCharsets.UTF_8), teacherTokenType); for (User user : users) { if (userRepository.findByUsername(user.getUsername()) == null) { userRepository.save(user); } } } catch (Exception e) { e.printStackTrace(); } return true; } } |
- Implement your own UserDetailsService
Customize user information in UserDetailService, and set all Permissions related to user role role to Authorities of Authentication for PermissionEvaluator to judge user permissions. Note that the form of resourceId-privilege is used here for splicing and storage. My user information here is stored in the MongoDB database, or it can be replaced with other databases.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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; import java.util.ArrayList; import java.util.List; @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private IUserService userService; @Autowired private MongoTemplate mongoTemplate; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userService.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(String.format("No user found with username: %s", username)); } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); List<String> roles = user.getRoles(); for (String roleName : roles) { Role role = mongoTemplate.findOne(Query.query(Criteria.where("name").is(roleName)), Role.class); if (role == null) { continue; } for (JsonPermissions.SimplePermission permission : role.getPermissions()) { for (String privilege : permission.getPrivileges().keySet()) { authorities.add(new SimpleGrantedAuthority(String.format("%s-%s", permission.getResourceId(), privilege))); } } } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), authorities); } } |
9.Config UserDetailsService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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.authentication.builders.AuthenticationManagerBuilder; 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.web.authentication.logout.LogoutHandler; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .authorizeRequests().antMatchers( "/js/**", "/css/**", "/img/**", "/login/**").permitAll() .anyRequest().authenticated() .and().formLogin().permitAll() .cors(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } } |
- The back-end interface implements access restrictions based on permissions
Add the PreAuthorize annotation to the interface method that requires access restrictions. In this annotation, we can use a variety of verification methods, the more common ones are hasPermisson and hasRole. Similar to PreAuthorize, there is the PostAuthorize annotation. It is also better to understand from the literal meaning that PreAuthorize performs verification before accessing the interface, and PostAuthorize performs verification when the result is returned after accessing the interface.
1 2 3 4 5 6 7 8 |
@GetMapping(value = "/list") @PreAuthorize("hasPermission('user', 'read') or hasRole('ROLE_ADMINISTRATOR')") public List<?> getUserList(@RequestParam(value = "text", defaultValue = "") String text, @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "20") int size) { return userService.list(text, page, size); } |
By analogy, corresponding access restrictions can be added to the interfaces that need to restrict user access.
- Implement your own PermissionEvaluator
After adding the PreAuthorize annotation to the interface method, you also need to implement your own PermissionEvaluator. Spring Security will verify the validity of the resource that the currently logged-in user is accessing and the operations performed on the resource in the hasPermission() method.
Note that the targetDomainObject here is the resourceId we defined before, and the permission is the privilege. When verifying, it should be combined into a format consistent with the format stored in the UserDetailsService. Here we use – dash to connect.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import java.io.Serializable; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @Configuration public class MyPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { boolean accessable = false; if(authentication.getPrincipal().toString().compareToIgnoreCase("anonymousUser") != 0){ String privilege = targetDomainObject + "-" + permission; for(GrantedAuthority authority : authentication.getAuthorities()){ if(privilege.equalsIgnoreCase(authority.getAuthority())){ accessable = true; break; } } return accessable; } return accessable; } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { // TODO Auto-generated method stub return false; } } |
- Annotation support
After implementing PermissionEvaluator, the annotation of globalMethodSecurity must be added, otherwise the permission judgment added on the interface will not take effect. Add this annotation to the inherited class of SpringBootServletInitializer to enable method security.
- Access test: 403
Since the user I am currently logged in has not set the role and access rights for it, I do not have access to the list interface, and the following 403 error occurs when I forcibly access it.
- Front-end pages implement personalized pages according to permissions
It doesn’t end when the backend implements interface-level access restrictions. For the part of the user-visible interface, users with different roles should see different interfaces according to their roles when they log in to the system. Our current experience is that after the user logs in successfully, the user’s permission list is returned to the front-end, and then the front-end judges the permissions. If there is no permission, the corresponding button or function module is hidden. Through the combination of front and back ends, users can only see the operation interface and data within the scope of their permissions. At the same time, even if some users directly modify the interface parameters to obtain data, they will be judged twice at the back end. Make sure that users can see their own data and can only perform operations within the scope of their authority!