项目之通过Spring Security获取当前登录的用户的信息(6)
🌊 作者主页:海拥
🌊 简介:🏆CSDN全栈领域优质创作者、🥇HDZ核心组成员、🥈蝉联C站周榜前十
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...catch
或throw/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;
}
🌊 面试题库:Java、Python、前端核心知识点大全和面试真题资料
🌊 电子图书:图灵程序丛书 300本、机械工业出版社6000册免费正版图书
🌊 办公用品:精品PPT模板几千套,简历模板一千多套
🌊 学习资料:2300套PHP建站源码,微信小程序入门资料
公众号【海拥】内回复【资源】获取以上所有资料
我已经写了很长一段时间的技术博客,这是我的一篇关于整合基于注解的SSM框架小结。我乐于通过文章分享技术与快乐。您可以访问我的博客主页: 华为云-海拥、我的个人博客:haiyong.site 以了解更多信息。希望你们会喜欢!
- 点赞
- 收藏
- 关注作者
评论(0)