20. 使用控制器轉(zhuǎn)發(fā)注冊(cè)頁面
將用戶注冊(cè)的register.html 文件移動(dòng)到templates 文件夾下。
在SystemController中添加:
@GetMapping ( "/register.html" )
public String register ( ) {
return "register" ;
}
在SecurityConfig中,將注冊(cè)相關(guān)的"/register.html"和"/portal/user/student/register"這2個(gè)URL添加到白名單中。
21. 處理用戶的權(quán)限
21.1. 補(bǔ)全:學(xué)生注冊(cè)時(shí)分配角色
在“學(xué)生注冊(cè)”的業(yè)務(wù)中,應(yīng)該及時(shí)獲取新插入的用戶數(shù)據(jù)的id,并將該用戶id和角色id(學(xué)生角色的id固定為2)插入到user_role數(shù)據(jù)表中,以記錄新注冊(cè)的學(xué)生的角色。
先在UserServiceImpl中添加:
@Autowired
private UserRoleMapper userRoleMapper;
然后,在原有的“學(xué)生注冊(cè)”的業(yè)務(wù)最后補(bǔ)充:
// 向“用戶角色表”中插入數(shù)據(jù),為當(dāng)前學(xué)生賬號(hào)分配角色
UserRole userRole = new UserRole ( ) ;
userRole. setUserId ( user. getId ( ) ) ;
userRole. setRoleId ( 2 ) ; // 學(xué)生角色的id固定為2,具體可參見user_role數(shù)據(jù)表
rows = userRoleMapper. insert ( userRole) ;
// 判斷返回值(受影響的行數(shù))是否不為1
if ( rows != 1 ) {
// 是:受影響的行數(shù)不是1,則插入用戶角色數(shù)據(jù)失敗,拋出InsertException
throw new InsertException ( "注冊(cè)失敗!服務(wù)器忙,請(qǐng)稍后再次嘗試!" ) ;
}
完成后,需要在“學(xué)生注冊(cè)”的業(yè)務(wù)方法之前添加@Transactional注解,以啟用事務(wù)。
關(guān)于事務(wù),它是數(shù)據(jù)庫提供的一種機(jī)制,它可以保證一系列的寫操作(包括插入、刪除、修改)要么全部成功,要么全部失敗!
假設(shè)存在數(shù)據(jù):
如果要實(shí)現(xiàn)“國斌向蒼松轉(zhuǎn)賬5000元”,需要執(zhí)行的數(shù)據(jù)操作有:
UPDATE 賬戶表 SET 余額=余額-5000 WHERE 賬號(hào)='國斌';
UPDATE 賬戶表 SET 余額=余額+5000 WHERE 賬號(hào)='蒼松';
萬一,在執(zhí)行過程中,因?yàn)槟承┎豢煽氐囊蛩?#xff0c;導(dǎo)致前一條SQL語句成功的執(zhí)行了,但是后一條SQL語句卻無法執(zhí)行,就會(huì)導(dǎo)致數(shù)據(jù)安全問題。在這種情況下,就需要使用事務(wù),如果2條SQL語句都執(zhí)行成功,則圓滿完成,如果任何1條執(zhí)行出錯(cuò),只要保證全部是失敗的(哪怕之前已經(jīng)執(zhí)行成功了某些SQL語句,也將失敗),數(shù)據(jù)安全也不會(huì)受到影響!
基于Spring JDBC的事務(wù)處理,只需要在業(yè)務(wù)方法之前添加@Transactional注解即可。其處理機(jī)制大致是:
try {
開啟事務(wù):BEGIN
執(zhí)行若干個(gè)數(shù)據(jù)訪問操作(增、刪、改、查)
提交事務(wù)(保存數(shù)據(jù)):COMMIT
} catch (RuntimeException e) {
回滾事務(wù):ROLLBACK
}
所以,為了保證事務(wù)機(jī)制的有效執(zhí)行,必須:
如果某個(gè)業(yè)務(wù)中涉及2次或以上的寫操作(例如2次INSERT操作,或1次INSERT加1次DELETE等),都必須在業(yè)務(wù)方法之前添加@Transactional注解,以啟用事務(wù); 每次調(diào)用了持久層的寫操作后,都必須 及時(shí)獲取返回的“受影響的行數(shù)”,并且判斷返回值是否與預(yù)期值相符合,如果不符合,必須 拋出RuntimeException或其子孫類異常的對(duì)象!
在開發(fā)項(xiàng)目時(shí),之所以需要將業(yè)務(wù)異常繼承自RuntimeException,是因?yàn)?#xff1a;
便于編寫代碼,避免使用異常時(shí)需要使用嚴(yán)格的語法聲明拋出或捕獲,因?yàn)?code>RuntimeException及其子孫類異常都不強(qiáng)制要求try...catch或throw/throws,并且,業(yè)務(wù)層拋出異常后,在控制器層也是全部再次拋出,交由統(tǒng)一處理異常的機(jī)制進(jìn)行處理的; 保證事務(wù)機(jī)制的正常使用。
另外,@Transactional注解還可以添加在業(yè)務(wù)類的聲明之前,會(huì)使得當(dāng)前類中所有的方法都是基于事務(wù)機(jī)制來運(yùn)行的,但是,一般并沒有這個(gè)必要性,所以,不推薦這樣使用!
還應(yīng)該了解:事務(wù)的ACID特性,事務(wù)的隔離,事務(wù)的傳播。
21.2. 處理登錄時(shí)獲取權(quán)限
以上注冊(cè)過程中添加了“分配角色”,而各角色是對(duì)應(yīng)某些權(quán)限的,所以,“分配角色”的過程就是“分配權(quán)限”的過程!在用戶登錄時(shí),應(yīng)該讀取用戶的權(quán)限,以完成Spring Security在驗(yàn)證過程中的授權(quán),以保證后續(xù)在進(jìn)行某些訪問時(shí),能給出正確的判斷,使得某些用戶可以執(zhí)行某些操作,而另一些用戶可能因?yàn)闆]有權(quán)限而不能執(zhí)行這些操作!
首先,需要實(shí)現(xiàn)“根據(jù)用戶id查詢?cè)撚脩舻臋?quán)限”的功能,需要執(zhí)行的SQL語句大致是:
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;
在處理權(quán)限數(shù)據(jù)的持久層PermissionMapper接口中添加抽象方法:
/**
* 查詢某用戶的權(quán)限
* @param userId 用戶的id
* @return 該用戶的權(quán)限的列表
*/
List < Permission > selectByUserId ( Integer userId) ;
然后,在PermissionMapper.xml中配置以上抽象方法對(duì)應(yīng)的SQL語句:
< 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>
完成后,在測(cè)試位置創(chuàng)建PermissionMapperTests測(cè)試類,編寫并執(zhí)行單元測(cè)試:
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) ;
}
}
}
接下來,在處理登錄的業(yè)務(wù)中,也就是在UserServiceImpl中先添加:
@Autowired
private PermissionMapper permissionMapper;
并在login()方法中補(bǔ)充:
// 權(quán)限字符串?dāng)?shù)組
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 ( ) ;
}
// 組織“用戶詳情”對(duì)象
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 ( ) ;
由于修改了注冊(cè)的業(yè)務(wù)(剛剛添加了“為學(xué)生賬號(hào)分配角色”),原本的測(cè)試數(shù)據(jù)可能會(huì)不可用,為了便于后續(xù)的測(cè)試使用,應(yīng)該先將原有數(shù)據(jù)全部清空:
TRUNCATE user;
并通過注冊(cè)業(yè)務(wù)或注冊(cè)頁面再次注冊(cè)一些新的賬號(hào)。
同時(shí),還應(yīng)該將一些數(shù)據(jù)標(biāo)識(shí)為老師:
UPDATE user SET type=1 WHERE id IN (1, 2, 3);
在用戶角色分配表中,清空原有數(shù)據(jù),將一部分賬號(hào)的角色改為管理員、老師:
-- 清空用戶角色分配表
TRUNCATE user_role;
-- 將某些用戶分配為管理員、老師、學(xué)生
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);
-- 將某些用戶分配為學(xué)生
INSERT INTO user_role (user_id, role_id) VALUES (4, 2), (5, 2), (6, 2);
22. 通過Spring Security獲取當(dāng)前登錄的用戶的信息
當(dāng)用戶成功登錄后,需要獲取用戶的信息才可以執(zhí)行后續(xù)的操作,例如獲取某用戶的權(quán)限、獲取某用戶的問題列表、獲取某用戶的個(gè)人信息等等。
Spring Security提供了簡便的獲取當(dāng)前登錄用戶信息的做法,在控制器的處理請(qǐng)求的方法中,添加Authentication類型的參數(shù),或添加Principal類型的參數(shù),均可獲得當(dāng)前登錄用戶的信息,例如:
// 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;
}
以上2種做法輸出的結(jié)果是完全相同的,因?yàn)?code>Authentication是繼承自Principal的,當(dāng)Spring MVC框架嘗試注入?yún)?shù)值時(shí),注入的是同一個(gè)對(duì)象!
以上做法輸出的內(nèi)容比較多,還可以使用以下做法來獲取用戶信息:
// http://localhost:8080/test/user/current/details
@GetMapping ( "/user/current/details" )
public UserDetails getUserDetails ( @AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
23. 擴(kuò)展UserDetails
通過以上注入@AuthenticationPricipal UserDetails userDetails后可以獲取用戶的信息,但是,對(duì)象中封裝的信息可能不足以滿足編程需求,例如沒有用戶的id或其它的某些屬性!如果需要存在這些屬性,就需要自定義類,擴(kuò)展自UserDetails!
在cn.tedu.straw.portal.security包下創(chuàng)建UserInfo類,繼承自User類,并在這個(gè)類中聲明所需的自定義屬性:
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) ;
}
}
注意:由于父類User中不存在無參數(shù)構(gòu)造方法,所以繼承后需要添加匹配參數(shù)的構(gòu)造方法!
注意:由于父類User中不存在無參數(shù)構(gòu)造方法,所以不可以使用Lombok中的@Data注解,只能按需添加@Setter、@Getter等注解。
然后,在業(yè)務(wù)層處理用戶登錄時(shí),使用以上創(chuàng)建的UserInfo類型的對(duì)象作為返回值對(duì)象:
// 組織“用戶詳情”對(duì)象
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;
以后,當(dāng)需要獲取當(dāng)前登錄的用戶信息時(shí),直接在控制器的處理請(qǐng)求的方法中注入UserInfo類型的參數(shù)對(duì)象即可:
// 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;
}