20. Use the controller to forward the registration page
Move the register.html file registered by the user to the templates folder.
In SystemController
adding:
@GetMapping("/register.html")
public String register() {
return "register";
}
In SecurityConfig
, add the registration-related "/register.html"
and "/portal/user/student/register"
these 2 URLs to the whitelist.
21. Handling user permissions
21.1. Completion: Assign roles when students register
In the "student registration" business, the id of the newly inserted user data should be obtained in time, and the user id and role id (the id of the student role is fixed to 2) should be inserted into the user_role
data table to record the newly registered student Character.
First UserServiceImpl
add in:
@Autowired
private UserRoleMapper userRoleMapper;
Then, in the original "student registration" business, finally add:
// 向“用户角色表”中插入数据,为当前学生账号分配角色
UserRole userRole = new UserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(2); // 学生角色的id固定为2,具体可参见user_role数据表
rows = userRoleMapper.insert(userRole);
// 判断返回值(受影响的行数)是否不为1
if (rows != 1) {
// 是:受影响的行数不是1,则插入用户角色数据失败,抛出InsertException
throw new InsertException("注册失败!服务器忙,请稍后再次尝试!");
}
After completion, you need to add a @Transactional
note before the business method of "student registration" to enable the transaction.
Regarding transactions, it is a mechanism provided by the database, which can ensure that a series of write operations (including insertion, deletion, and modification) will either succeed or fail!
Assuming there are data:
account number | Balance |
---|---|
Cang Song | 1000 |
Guobin | 8000 |
If you want to realize "Guobin transfers 5000 yuan to Cangsong", the data operations that need to be performed are:
UPDATE 账户表 SET 余额=余额-5000 WHERE 账号='国斌';
UPDATE 账户表 SET 余额=余额+5000 WHERE 账号='苍松';
In case, during the execution process, due to some uncontrollable factors, the previous SQL statement is successfully executed, but the latter SQL statement cannot be executed, which will lead to data security issues. In this case, you need to use transactions. If the two SQL statements are executed successfully, they are successfully completed. If any one of the SQL statements is executed incorrectly, as long as you ensure that all are failed (even if some SQL statements have been successfully executed before, also Will fail) and data security will not be affected!
Based on Spring JDBC transaction processing, you only need to add @Transactional
annotations before the business method . The processing mechanism is roughly:
try {
开启事务:BEGIN
执行若干个数据访问操作(增、删、改、查)
提交事务(保存数据):COMMIT
} catch (RuntimeException e) {
回滚事务:ROLLBACK
}
Therefore, in order to ensure the effective execution of the transaction mechanism, you must:
- If a business involves 2 or more write operations (for example, 2 INSERT operations, or 1 INSERT plus 1 DELETE, etc.), you must add an
@Transactional
annotation before the business method to enable the transaction; - Every time the write operation of the persistence layer is called, the returned "number of rows affected" must be obtained in time, and the return value must be determined whether it is consistent with the expected value. If it does not, it must throw
RuntimeException
or its descendant class exception object !
When developing a project, the reason why business exceptions need to be inherited from
RuntimeException
is because:
- Easy to write code to avoid using exception requires strict syntax for declaring thrown or caught, because
RuntimeException
its descendants, is not mandatory for an exceptiontry...catch
orthrow/throws
, and, after the service layer throw an exception, the controller layer is also all throw again, Handed over to a unified exception handling mechanism for processing;- Ensure the normal use of the transaction mechanism.
In addition, @Transactional
annotations can also be added before the declaration of the business class, which will make all the methods in the current class run based on the transaction mechanism. However, this is generally not necessary, so it is not recommended!
You should also understand: ACID characteristics of transactions, transaction isolation, and transaction propagation.
21.2. Obtaining permissions when handling logins
In the above registration process, "Assign Role" is added, and each role corresponds to certain permissions, so the process of "Assigning Role" is the process of "Assigning Authority"! When the user logs in, the user's permissions should be read to complete the authorization of Spring Security in the verification process to ensure that the correct judgment can be given when certain accesses are made in the future, so that certain users can perform certain operations. Other users may not be able to perform these operations because they do not have permission!
First, you need to implement the function of "query the user's permissions based on user id". The SQL statement that needs to be executed is roughly:
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;
PermissionMapper
Add an abstract method to the persistence layer interface that handles permission data :
/**
* 查询某用户的权限
* @param userId 用户的id
* @return 该用户的权限的列表
*/
List<Permission> selectByUserId(Integer userId);
Then, PermissionMapper.xml
configure the SQL statement corresponding to the above abstract method in:
<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>
After completion, create a PermissionMapperTests
test class at the test location , write and execute unit tests:
package cn.tedu.straw.portal.mapper;
@SpringBootTest
@Slf4j
public class PermissionMapperTests {
@Autowired
PermissionMapper mapper;
@Test
void selectByUserId() {
Integer userId = 1;
List<Permission> permissions = mapper.selectByUserId(userId);
log.debug("permissions count={}", permissions.size());
for (Permission permission : permissions) {
log.debug("permission > {}", permission);
}
}
}
Next, in the business of processing login, that is UserServiceImpl
, add it first:
@Autowired
private PermissionMapper permissionMapper;
And login()
add in the method:
// 权限字符串数组
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();
}
// 组织“用户详情”对象
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();
Due to the modification of the registered business (the "Assign role to student account" has just been added), the original test data may not be available. In order to facilitate subsequent test use, all the original data should be cleared first:
TRUNCATE user;
And register some new accounts again through the registration business or registration page.
At the same time, some data should be identified as teachers:
UPDATE user SET type=1 WHERE id IN (1, 2, 3);
In the user role assignment table, clear the original data, and change the roles of some accounts to administrators and teachers:
-- 清空用户角色分配表
TRUNCATE user_role;
-- 将某些用户分配为管理员、老师、学生
INSERT INTO user_role (user_id, role_id) VALUES (1, 1), (1, 2), (1, 3);
-- 将某些用户分配为老师
INSERT INTO user_role (user_id, role_id) VALUES (2, 3), (3, 3);
-- 将某些用户分配为学生
INSERT INTO user_role (user_id, role_id) VALUES (4, 2), (5, 2), (6, 2);
22. Get the information of the currently logged-in user through Spring Security
After the user successfully logs in, he needs to obtain the user's information to perform subsequent operations, such as obtaining a user's authority, obtaining a user's question list, obtaining a user's personal information, and so on.
Spring Security provides a simple way to obtain the information of the currently logged in user. In the method of processing the request of the controller, add Authentication
type parameters or add Principal
type parameters to obtain the information of the currently 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;
}
The output results of the above two methods are exactly the same, because they Authentication
are inherited from Principal
, when the Spring MVC framework tries to inject parameter values, the same object is injected!
The above methods output a lot of content, and you can also use the following methods to obtain user information:
// http://localhost:8080/test/user/current/details
@GetMapping("/user/current/details")
public UserDetails getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
23. Extend UserDetails
After the above injection @AuthenticationPricipal UserDetails userDetails
, the user's information can be obtained, but the information encapsulated in the object may not be enough to meet the programming needs, such as no user's id
or some other attributes! If these attributes need to exist, you need to customize the class and extend it UserDetails
!
cn.tedu.straw.portal.security
Create a UserInfo
class under the package , inherit from the User
class, and declare the required custom attributes in this class:
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<? extends GrantedAuthority> 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);
}
}
Note: Since User
there is no parameterless construction method in the parent class , a construction method with matching parameters needs to be added after inheritance!
Note: Since the parent User
non-parametric methods do not exist in the configuration, it can not be used in the Lombok @Data
annotation can be added on demand @Setter
, @Getter
and other annotations.
Then, when processing user login in the business layer, use the UserInfo
type of object created above as the return value 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;
In the future, when you need to obtain the information of the currently logged in user, you can directly inject the UserInfo
type of parameter object into the method of processing the request of the controller :
// 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;
}