电竞比分网-中国电竞赛事及体育赛事平台

分享

項(xiàng)目之通過Spring Security獲取當(dāng)前登錄的用戶的信息(6)

 海擁 2021-11-30

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ù):

賬號(hào)余額
蒼松1000
國斌8000

如果要實(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...catchthrow/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;
}

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多