20. Use the controller to forward the registration page

Move the register. HTML file that the user is registered with to the Templates folder.

Add to SystemController:

@GetMapping("/register.html")
public String register(a) {
    return "register";
}
Copy the code

In SecurityConfig, registers the related “/ register. HTML” and “/ portal/user/student/register” these two urls are added to the whitelist.

21. Handle user permissions

21.1. Completion: Students are assigned roles when they register

In the Student Registration service, you should obtain the id of the newly inserted user data and insert the user ID and role ID (student role ID is fixed at 2) into the user_Role data table to record the newly registered student’s role.

Start with UserServiceImpl:

@Autowired
private UserRoleMapper userRoleMapper;
Copy the code

Then, in the original “student registration” business finally added:

// Insert data into "User Role table" to assign a role to the current student account
UserRole userRole = new UserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(2); // The student role id is fixed to 2. For details, see the user_Role data table
rows = userRoleMapper.insert(userRole);
// Determine if the return value (the number of affected rows) is not 1
if(rows ! =1) {
    // Yes: If the number of affected rows is not 1, insert user role data fails and InsertException is thrown
    throw new InsertException("Registration failed! Server busy, please try again later!");
}
Copy the code

Once complete, you need to add the @Transactional annotation before the business method of “Student Registration” to enable the transaction.

In terms of transactions, it is a mechanism provided by the database to guarantee that a series of write operations (including inserts, deletes, and modifiers) will either all succeed or all fail!

Suppose there is data:

account The balance of
pines 1000
The bin 8000

If “Guobin transfers 5000 YUAN to Cangsong” is to be realized, the following data operations need to be performed:

UPDATEAccount tableSETBalance = balance- 5000. WHEREId ='the bin';
UPDATEAccount tableSETBalance = balance +5000 WHEREId ='set off';
Copy the code

If the first SQL statement is executed successfully but the second SQL statement cannot be executed due to some uncontrollable factors during execution, data security problems may occur. In this case, you need to use transactions. If both SQL statements are executed successfully, then it is complete. If any one of the SQL statements fails, as long as all of them fail (even if some SQL statements have been executed successfully before, they will fail), data security will not be affected!

Spring JDBC-based transactions simply require the @Transactional annotation to precede the business method. Its processing mechanism is roughly as follows:

BEGIN Perform several data access operations (add, delete, modify, and query) COMMIT a transaction (save data) : COMMIT} Catch (RuntimeException e) {ROLLBACK}Copy the code

Therefore, in order to ensure the effective execution of transaction mechanism, it is necessary to:

  • If two or more write operations (such as two INSERT operations or one INSERT and DELETE operations) are involved in a service, they must be added before the service method@TransactionalAnnotations to enable transactions;
  • Each time a write operation of the persistence layer is calledMust beGet the number of affected rows returned in time and determine whether the returned value matches the expected value. If not,Must bethrowRuntimeExceptionOr its descendants class exception object!

When developing a project, you need to inherit business exceptions from RuntimeException because:

  • Easy to write code to avoid the need to use strict syntax to declare throws or catches when using exceptions, becauseRuntimeExceptionExceptions and descendants are not mandatorytry... catchorthrow/throwsAnd, after the business layer throws an exception, the controller layer also throws it all again, which is processed by the unified exception processing mechanism;
  • Ensure the normal use of transaction mechanism.

The @Transactional annotation can also be used to make all methods in a class run ona Transactional basis before a business class is declared. However, this is not always necessary and is therefore not recommended.

You should also understand the ACID nature of transactions, transaction isolation, and transaction propagation.

21.2. Obtaining permissions during login

The above registration process added “assign roles”, and each role is corresponding to some rights, so “assign roles” process is “assign rights” process! When the user logs in, the user’s permissions should be read to complete Spring Security’s authorization during the authentication process to ensure that subsequent access decisions are made so that some users can perform certain operations that others may not be able to perform because they do not have permissions!

First, you need to implement the function of “query the permission of the user based on the user ID”. The SQL statement to be executed is roughly as follows:

SELECT 
	DISTINCT permission.*
FROM
	permission
LEFT JOIN role_permission ON permission.id=role_permission.permission_id
LEFT JOIN role ON role_permission.role_id=role.id
LEFT JOIN user_role ON role.id=user_role.role_id
LEFT JOIN user ON user_role.user_id=user.id
WHERE 
	user.id=1;
Copy the code

Add abstract methods to the PermissionMapper interface of the persistence layer that handles permission data:

/** * Query the permission of a user *@paramUserId userId *@returnList of permissions for this user */
List<Permission> selectByUserId(Integer userId);
Copy the code

Then, configure the SQL statement corresponding to the above abstract method in permissionmapper.xml:

<select id="selectByUserId" resultMap="BaseResultMap">
    SELECT
        DISTINCT permission.id, permission.name, permission.description
    FROM
        permission
    LEFT JOIN role_permission ON permission.id=role_permission.permission_id
    LEFT JOIN role ON role_permission.role_id=role.id
    LEFT JOIN user_role ON role.id=user_role.role_id
    LEFT JOIN user ON user_role.user_id=user.id
    WHERE
        user.id=#{userId}
</select>
Copy the code

When done, create the PermissionMapperTests test class at the test location and write and execute the unit tests:

package cn.tedu.straw.portal.mapper;

@SpringBootTest
@Slf4j
public class PermissionMapperTests {

    @Autowired
    PermissionMapper mapper;

    @Test
    void selectByUserId(a) {
        Integer userId = 1;
        List<Permission> permissions = mapper.selectByUserId(userId);
        log.debug("permissions count={}", permissions.size());
        for (Permission permission : permissions) {
            log.debug("permission > {}", permission); }}}Copy the code

Next, in the business that handles login, in UserServiceImpl first add:

@Autowired
private PermissionMapper permissionMapper;
Copy the code

And add in the login() method:

// An array of permission strings
List<Permission> permissions = permissionMapper.selectByUserId(user.getId());
String[] authorities = new String[permissions.size()];
for (int i = 0; i < permissions.size(); i++) {
    authorities[i] = permissions.get(i).getName();
}
// Organize the User Details object
UserDetails userDetails = org.springframework.security.core.userdetails.User
        .builder()
        .username(user.getUsername())
        .password(user.getPassword())
        .authorities(authorities)
        .disabled(user.getEnabled() == 0)
        .accountLocked(user.getLocked() == 1)
        .build();
Copy the code

Due to the modification of the registered service (” Assigning roles to student accounts “was just added), the original test data may be unavailable. In order to facilitate the use of subsequent tests, the original data should be cleared first:

TRUNCATE user;
Copy the code

And through the registration business or registration page to register some new accounts.

Also, some data should be identified as teacher:

UPDATE user SET type=1 WHERE id IN (1.2.3);
Copy the code

In the user role assignment table, clear the original data and change the roles of some accounts to administrator and teacher:

Clear the user role assignment table
TRUNCATE user_role;
-- Assign some users to administrators, teachers, and students
INSERT INTO user_role (user_id, role_id) VALUES (1.1), (1.2), (1.3);
-- Assign some users as teachers
INSERT INTO user_role (user_id, role_id) VALUES (2.3), (3.3);
-- Assign some users to students
INSERT INTO user_role (user_id, role_id) VALUES (4.2), (5.2), (6.2);
Copy the code

22. Use Spring Security to obtain information about the currently logged in user

After a user logs in successfully, the user can perform subsequent operations only after obtaining the user information, such as the user permission, problem list, and personal information.

Spring Security provides a simple way to obtain the information of the current logged-in user. Add the parameter of Authentication type or the parameter of Principal type to the controller’s request processing method to obtain the information of the current logged-in user, for example:

// http://localhost:8080/test/user/current/authentication
@GetMapping("/user/current/authentication")
public Authentication getAuthentication(Authentication authentication) {
    return authentication;
}

// http://localhost:8080/test/user/current/principal
@GetMapping("/user/current/principal")
public Principal getPrincipal(Principal principal) {
    return principal;
}
Copy the code

The output is exactly the same because Authentication is inherited from Principal, and when the Spring MVC framework tries to inject parameter values, the same object is injected!

The output content of the above method is more, you can also use the following method to obtain user information:

// http://localhost:8080/test/user/current/details
@GetMapping("/user/current/details")
public UserDetails getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {
    return userDetails;
}
Copy the code

23. The extension populated UserDetails

You can retrieve user information by injecting @authenticationPricipal UserDetails above. However, the information encapsulated in the object may not be enough to satisfy programming requirements, such as the lack of user ID or some other attributes! If you want these attributes to exist, you need a custom class that extends from UserDetails!

In cn. Tedu. Straw. Portal. The security package created under the UserInfo class, inherited from the User class, and in this class is declared in the custom attributes:

package cn.tedu.straw.portal.security;

@Setter
@Getter
@ToString
public class UserInfo extends User {

    private Integer id;
    private String nickname;
    private Integer gender;
    private Integer type;

    public UserInfo(String username, String password, Collection
        authorities) {
        super(username, password, authorities);
    }

    public UserInfo(String username, String password,
                    boolean enabled, boolean accountNonExpired,
                    boolean credentialsNonExpired, boolean accountNonLocked,
                    Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); }}Copy the code

Note: due to the parent classUserThere is no no-argument constructor, so you need to add matching parameters constructor after inheritance!

Note: due to the parent classUserThere are no parameterless constructors in Lombok, so you can’t use Lombok@DataAnnotations can only be added on demand@Setter,@GetterSuch as annotations.

Then, when the business layer processes the user login, use the object of type UserInfo created above as the return value object:

// Organize the User Details object
UserDetails userDetails = org.springframework.security.core.userdetails.User
        .builder()
        .username(user.getUsername())
        .password(user.getPassword())
        .authorities(authorities)
        .disabled(user.getEnabled() == 0)
        .accountLocked(user.getLocked() == 1)
        .build();
UserInfo userInfo = new UserInfo(
        userDetails.getUsername(),
        userDetails.getPassword(),
        userDetails.isEnabled(),
        userDetails.isAccountNonExpired(),
        userDetails.isCredentialsNonExpired(),
        userDetails.isAccountNonLocked(),
        userDetails.getAuthorities()
);
userInfo.setId(user.getId());
userInfo.setNickname(user.getNickname());
userInfo.setGender(user.getGender());
userInfo.setType(user.getType());
return userInfo;
Copy the code

In the future, when you need to obtain the information of the currently logged user, you can directly inject the parameter object of UserInfo into the controller’s request processing method:

// http://localhost:8080/test/user/current/info
@GetMapping("/user/current/info")
public UserInfo getUserInfo(@AuthenticationPrincipal UserInfo userInfo) {
    System.out.println("user id = " + userInfo.getId());
    System.out.println("user nickname = " + userInfo.getNickname());
    return userInfo;
}
Copy the code