项目之通过Spring Security获取当前登录的用户的信息(6)

20. 使用控制器转发注册页面

将用户注册的register.html文件移动到templates文件夹下。

SystemController中添加:

@GetMapping("/register.html")
public String register() {
    
    
    return "register";
}

SecurityConfig中,将注册相关的"/register.html""/portal/user/student/register"这2个URL添加到白名单中。

21. 处理用户的权限

21.1. 补全:学生注册时分配角色

在“学生注册”的业务中,应该及时获取新插入的用户数据的id,并将该用户id和角色id(学生角色的id固定为2)插入到user_role数据表中,以记录新注册的学生的角色。

先在UserServiceImpl中添加:

@Autowired
private UserRoleMapper userRoleMapper;

然后,在原有的“学生注册”的业务最后补充:

// 向“用户角色表”中插入数据,为当前学生账号分配角色
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("注册失败!服务器忙,请稍后再次尝试!");
}

完成后,需要在“学生注册”的业务方法之前添加@Transactional注解,以启用事务。

关于事务,它是数据库提供的一种机制,它可以保证一系列的写操作(包括插入、删除、修改)要么全部成功,要么全部失败!

假设存在数据:

账号 余额
苍松 1000
国斌 8000

如果要实现“国斌向苍松转账5000元”,需要执行的数据操作有:

UPDATE 账户表 SET 余额=余额-5000 WHERE 账号='国斌';
UPDATE 账户表 SET 余额=余额+5000 WHERE 账号='苍松';

万一,在执行过程中,因为某些不可控的因素,导致前一条SQL语句成功的执行了,但是后一条SQL语句却无法执行,就会导致数据安全问题。在这种情况下,就需要使用事务,如果2条SQL语句都执行成功,则圆满完成,如果任何1条执行出错,只要保证全部是失败的(哪怕之前已经执行成功了某些SQL语句,也将失败),数据安全也不会受到影响!

基于Spring JDBC的事务处理,只需要在业务方法之前添加@Transactional注解即可。其处理机制大致是:

try {
	开启事务:BEGIN
	执行若干个数据访问操作(增、删、改、查)
	提交事务(保存数据):COMMIT
} catch (RuntimeException e) {
	回滚事务:ROLLBACK
}

所以,为了保证事务机制的有效执行,必须:

  • 如果某个业务中涉及2次或以上的写操作(例如2次INSERT操作,或1次INSERT加1次DELETE等),都必须在业务方法之前添加@Transactional注解,以启用事务;
  • 每次调用了持久层的写操作后,都必须及时获取返回的“受影响的行数”,并且判断返回值是否与预期值相符合,如果不符合,必须抛出RuntimeException或其子孙类异常的对象!

在开发项目时,之所以需要将业务异常继承自RuntimeException,是因为:

  • 便于编写代码,避免使用异常时需要使用严格的语法声明抛出或捕获,因为RuntimeException及其子孙类异常都不强制要求try...catchthrow/throws,并且,业务层抛出异常后,在控制器层也是全部再次抛出,交由统一处理异常的机制进行处理的;
  • 保证事务机制的正常使用。

另外,@Transactional注解还可以添加在业务类的声明之前,会使得当前类中所有的方法都是基于事务机制来运行的,但是,一般并没有这个必要性,所以,不推荐这样使用!

还应该了解:事务的ACID特性,事务的隔离,事务的传播。

21.2. 处理登录时获取权限

以上注册过程中添加了“分配角色”,而各角色是对应某些权限的,所以,“分配角色”的过程就是“分配权限”的过程!在用户登录时,应该读取用户的权限,以完成Spring Security在验证过程中的授权,以保证后续在进行某些访问时,能给出正确的判断,使得某些用户可以执行某些操作,而另一些用户可能因为没有权限而不能执行这些操作!

首先,需要实现“根据用户id查询该用户的权限”的功能,需要执行的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;

在处理权限数据的持久层PermissionMapper接口中添加抽象方法:

/**
 * 查询某用户的权限
 * @param userId 用户的id
 * @return 该用户的权限的列表
 */
List<Permission> selectByUserId(Integer userId);

然后,在PermissionMapper.xml中配置以上抽象方法对应的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>

完成后,在测试位置创建PermissionMapperTests测试类,编写并执行单元测试:

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);
        }
    }

}

接下来,在处理登录的业务中,也就是在UserServiceImpl中先添加:

@Autowired
private PermissionMapper permissionMapper;

并在login()方法中补充:

// 权限字符串数组
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();

由于修改了注册的业务(刚刚添加了“为学生账号分配角色”),原本的测试数据可能会不可用,为了便于后续的测试使用,应该先将原有数据全部清空:

TRUNCATE user;

并通过注册业务或注册页面再次注册一些新的账号。

同时,还应该将一些数据标识为老师:

UPDATE user SET type=1 WHERE id IN (1, 2, 3);

在用户角色分配表中,清空原有数据,将一部分账号的角色改为管理员、老师:

-- 清空用户角色分配表
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. 通过Spring Security获取当前登录的用户的信息

当用户成功登录后,需要获取用户的信息才可以执行后续的操作,例如获取某用户的权限、获取某用户的问题列表、获取某用户的个人信息等等。

Spring Security提供了简便的获取当前登录用户信息的做法,在控制器的处理请求的方法中,添加Authentication类型的参数,或添加Principal类型的参数,均可获得当前登录用户的信息,例如:

// 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种做法输出的结果是完全相同的,因为Authentication是继承自Principal的,当Spring MVC框架尝试注入参数值时,注入的是同一个对象!

以上做法输出的内容比较多,还可以使用以下做法来获取用户信息:

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

23. 扩展UserDetails

通过以上注入@AuthenticationPricipal UserDetails userDetails后可以获取用户的信息,但是,对象中封装的信息可能不足以满足编程需求,例如没有用户的id或其它的某些属性!如果需要存在这些属性,就需要自定义类,扩展自UserDetails

cn.tedu.straw.portal.security包下创建UserInfo类,继承自User类,并在这个类中声明所需的自定义属性:

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中不存在无参数构造方法,所以继承后需要添加匹配参数的构造方法!

注意:由于父类User中不存在无参数构造方法,所以不可以使用Lombok中的@Data注解,只能按需添加@Setter@Getter等注解。

然后,在业务层处理用户登录时,使用以上创建的UserInfo类型的对象作为返回值对象:

// 组织“用户详情”对象
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;

以后,当需要获取当前登录的用户信息时,直接在控制器的处理请求的方法中注入UserInfo类型的参数对象即可:

// 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;
}

猜你喜欢

转载自blog.csdn.net/qq_44273429/article/details/107601658