Day3 权限管理
这里会总结构建项目过程中遇到的问题,以及一些个人思考!!
学习方法:
1 github源码 + 文档 + 官网
2 内容复现 ,实际操作
项目源码同步更新到github 欢迎大家star~ 后期会更新并上传前端项目
创建管理员服务模块
提供对角色和权限的管理— 操作数据库
- pom文件(父子关系、 依赖注入)
- 配置文件修改 nacos注册
- 服务和common接口
新增管理员
/**
* 管理员实现类
*
* @author bootsCoder
* @date created on 2024/4/15
*/
@DubboService
@Transactional
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;
@Override
public void add(Admin admin) {
adminMapper.insert(admin);
}
@Override
public void update(Admin admin) {
}
@Override
public void delete(Long id) {
}
@Override
public Admin findById(Long id) {
return null;
}
@Override
public Page<Admin> search(int page, int size) {
return null;
}
@Override
public void updateRoleToAdmin(Long aid, Long[] rids) {
}
}
/**
* 管理员服务
*
* @author bootsCoder
* @date created on 2024/4/15
*/
public interface AdminService {
/**
* 新增管理员
*/
void add(Admin admin);
/**
* 更新管理员
*/
void update(Admin admin);
/**
* 删除管理员
*/
void delete(Long id);
/**
* 查找管理员
*/
Admin findById(Long id);
/**
* 新增管理员
*/
Page<Admin> search(int page, int size);
/**
* 更新角色
*/
void updateRoleToAdmin(Long aid, Long[] rids);
}
/**
* 管理员api
*
* @author bootsCoder
* @date created on 2024/4/15
*/
@RestController
@RequestMapping("/admin")
public class AdminController {
@DubboReference
private AdminService adminService;
@PostMapping("/add")
public BaseResult add(@RequestBody Admin admin) {
adminService.add(admin);
return BaseResult.ok();
}
}
删除管理员
删除管理员时需要删除对应的角色
/**
* 管理员的数据库映射类
*
* @author bootsCoder
* @date created on 2024/4/15
*/
public interface AdminMapper extends BaseMapper<Admin> {
/**
* Deletes all roles associated with the specified administrator ID.
*
* @param aid the administrator ID
*/
@Delete("DELETE FROM boots_admin_role WHERE aid = #{aid}")
void deleteAdminAllRole(@Param("aid") Long aid);
}
查询管理员 – 编写多表联查
xml 格式,有时间研究一下 @Select 的方法
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bootscoder.shopping_admin_service.mapper.AdminMapper">
<resultMap id="adminMapper" type="com.bootscoder.shopping_common.pojo.Admin">
<id property="aid" column="aid"></id>
<result property="username" column="username"></result>
<collection property="roles" column="aid" ofType="com.bootscoder.shopping_common.pojo.Role">
<id property="rid" column="rid"></id>
<result property="roleName" column="roleName"></result>
<result property="roleDesc" column="roleDesc"></result>
<collection property="permissions" column="rid" ofType="com.bootscoder.shopping_common.pojo.Permission">
<id property="pid" column="pid"></id>
<result property="permissionName" column="permissionName"></result>
<result property="url" column="url"></result>
</collection>
</collection>
</resultMap>
<select id="findById" parameterType="long" resultMap="adminMapper">
SELECT * FROM boots_admin
LEFT JOIN boots_admin_role on boots_admin.aid = boots_admin_role.aid
LEFT JOIN boots_role on boots_admin_role.rid = boots_role.rid
LEFT JOIN boots_role_permission on boots_role.rid = boots_role_permission.rid
LEFT JOIN boots_permission on boots_role_permission.pid = boots_permission.pid
WHERE boots_admin.aid = #{aid}
</select>
<insert id="addRoleToAdmin">
INSERT INTO boots_admin_role values (#{aid},#{rid});
</insert>
<select id="findAllPermission" parameterType="string" resultType="com.bootscoder.shopping_common.pojo.Permission">
SELECT DISTINCT boots_permission.*
FROM
boots_admin
LEFT JOIN boots_admin_role ON boots_admin.aid = boots_admin_role.aid
LEFT JOIN boots_role on boots_admin_role.rid = boots_role.rid
LEFT JOIN boots_role_permission on boots_role.rid = boots_role_permission.rid
LEFT JOIN boots_permission on boots_role_permission.pid = boots_permission.pid
WHERE boots_admin.username = #{username}
</select>
</mapper>
这个真的是有一手了,我感觉得好好学,不开玩笑
分页查询管理员
@GetMapping("/search")
public BaseResult<Page<Admin>> search(int page, int size) {
Page<Admin> adminPage = adminService.search(page, size);
return BaseResult.ok(adminPage);
}
@Override
public Page<Admin> search(int page, int size) {
return adminMapper.selectPage(new Page<>(page, size), null);
}
//在启动类配置分页
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
修改管理员角色
修改管理员的角色,即修改
boots_admin_role
表中的记录。修改管理员角色时,先将管理员的所有角色删除,再将其新角色添加到boots_admin_role
表中。
/**
* 管理员实现类
*
* @author bootsCoder
* @date created on 2024/4/15
*/
@DubboService
@Transactional //配置事务
public class AdminServiceImpl implements AdminService {
@Override
@Transactional
public void updateRoleToAdmin(Long aid, Long[] rids) {
if (aid == null || aid <= 0) {
//这个异常我想让我的异常捕获器捕捉到该怎么办呢?
throw new IllegalArgumentException("Invalid administrator ID");
}
if (rids == null || rids.length == 0) {
throw new IllegalArgumentException("Roles array must not be empty");
}
// 删除用户的所有角色
adminMapper.deleteAdminAllRole(aid);
// 重新添加管理员角色
for (Long rid : rids) {
adminMapper.addRoleToAdmin(aid, rid);
}
}
}
提问:
- 方法【updateRoleToAdmin】需要在Transactional注解指定rollbackFor或者在方法中显式的rollback。 why
- 类的@Transaction 注释和 方法的有什么不同
- how to add this exception in my own exception handler?
认真比对了一下参数,成功了
生成接口文档 – easyYAPi
连接前端工程 测试
修改角色? – > 如果用户本来没有角色需要添加怎么办?
修改逻辑错误
@Override
public void updateRoleToAdmin(Long aid, Long[] rids) {
if (aid == null || aid <= 0) {
//这个异常我想让我的异常捕获器捕捉到该怎么办呢?
throw new IllegalArgumentException("Invalid administrator ID");
}
// 删除用户的所有角色
adminMapper.deleteAdminAllRole(aid);
if (rids != null || rids.length > 0) {
for (Long rid : rids) {
// 重新添加管理员角色
adminMapper.addRoleToAdmin(aid, rid);
}
}
}
测试:
删除成功~
JDBC Connection [HikariProxyConnection@1234983148 wrapping com.mysql.cj.jdbc.ConnectionImpl@6fb35126] will be managed by Spring
= => Preparing: DELETE FROM boots_admin_role WHERE aid = ?
= = > Parameters: 33(Long)
<== Updates: 1
编写角色相关功能
和admin 类似 还是一样 最难的部分是xml中sql的编写和结果的封装
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bootscoder.shopping_admin_service.mapper.PermissionMapper">
<delete id="deletePermissionAllRole" parameterType="long">
DELETE FROM boots_role_permission WHERE pid = #{pid}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bootscoder.shopping_admin_service.mapper.RoleMapper">
<resultMap id="roleMapper" type="com.bootscoder.shopping_common.pojo.Role">
<id property="rid" column="rid"></id>
<result property="roleName" column="roleName"></result>
<result property="roleDesc" column="roleDesc"></result>
<collection property="permissions" column="rid" ofType="com.bootscoder.shopping_common.pojo.Permission">
<id property="pid" column="pid"></id>
<result property="permissionName" column="permissionName"></result>
<result property="url" column="url"></result>
</collection>
</resultMap>
<select id="findById" parameterType="long" resultMap="roleMapper">
SELECT * FROM boots_role
LEFT JOIN boots_role_permission on boots_role.rid = boots_role_permission.rid
LEFT JOIN boots_permission on boots_role_permission.pid = boots_permission.pid
WHERE boots_role.rid = #{rid}
</select>
<delete id="deleteRoleAllPermission" parameterType="long">
DELETE FROM boots_role_permission where rid = #{rid}
</delete>
<delete id="deleteRoleAllAdmin" parameterType="long">
DELETE FROM boots_admin_role where rid = #{rid}
</delete>
<insert id="addPermissionToRole">
INSERT INTO boots_role_permission values (#{rid},#{pid});
</insert>
</mapper>
编写角色相关功能
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bootscoder.shopping_admin_service.mapper.PermissionMapper">
<delete id="deletePermissionAllRole" parameterType="long">
DELETE FROM boots_role_permission WHERE pid = #{pid}
</delete>
</mapper>
修改角色权限时报错
@Override
public void updatePermissionToRole(Long rid, Long[] pids) {
// 删除角色的所有权限
roleMapper.deleteRoleAllPermission(rid);
if (pids != null && pids.length > 0){
// 给角色添加权限
for (Long pid : pids) {
roleMapper.addPermissionToRole(rid,pid);
}
}
}
删除所有权限 写成所有admin了
Spring Security 核心组件
在Spring Security 3.0中,spring-security-core
jar的内容被剥离到最低限度。它不再包含与web相关的任何代码 - 应用程序安全性,LDAP或命名空间配置。我们将在这里看一下您在核心模块中可以找到的一些Java类型。它们代表了框架的构建块,因此如果您需要超越简单的命名空间配置,那么即使您实际上不需要直接与它们进行交互,您也必须了解它们是什么。
SecurityContextHolder,SecurityContext和Authentication Objects
最基本的对象是SecurityContextHolder
。这是我们存储应用程序当前安全上下文的详细信息的地方,其中包括当前使用该应用程序的主体的详细信息。默认情况下,SecurityContextHolder
使用ThreadLocal
来存储这些详细信息,这意味着安全上下文始终可用于同一执行线程中的方法,即使安全上下文未作为参数显式传递那些方法。如果在处理当前委托人的请求之后小心地清除线程,以这种方式使用ThreadLocal
是非常安全的。当然,Spring Security会自动为您解决这个问题,因此无需担心。
某些应用程序并不完全适合使用ThreadLocal
,因为它们使用线程的特定方式。例如,Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。SecurityContextHolder
可以在启动时配置策略,以指定您希望如何存储上下文。对于独立应用程序,您将使用SecurityContextHolder.MODE_GLOBAL
策略。其他应用程序可能希望安全线程生成的线程也采用相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
来实现的。您可以通过两种方式从默认SecurityContextHolder.MODE_THREADLOCAL
更改模式。第一个是设置系统属性,第二个是在SecurityContextHolder
上调用静态方法。大多数应用程序不需要更改默认值,但如果这样做,请查看JavaDoc for SecurityContextHolder
以了解更多信息。
获取有关当前用户的信息
在SecurityContextHolder
内,我们存储了当前与应用程序交互的主体的详细信息。Spring Security使用Authentication
对象来表示此信息。您通常不需要自己创建Authentication
对象,但用户查询Authentication
对象是相当常见的。您可以使用以下代码块(从应用程序的任何位置)获取当前经过身份验证的用户的名称,例如:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
调用getContext()
返回的对象是SecurityContext
接口的实例。这是保存在线程本地存储中的对象。正如我们将在下面看到的,Spring Security中的大多数认证机制都返回UserDetails
的实例作为主体。
UserDetailsService
上面代码片段中需要注意的另一个问题是,您可以从Authentication
对象获取主体。校长只是Object
。大多数情况下,这可以转换为UserDetails
对象。UserDetails
是Spring Security中的核心界面。它代表一个主体,但是以可扩展和特定于应用程序的方式。可以将UserDetails
视为您自己的用户数据库与SecurityContextHolder
内Spring Security所需的适配器之间的适配器。作为来自您自己的用户数据库的东西的表示,您经常会将UserDetails
转换为您的应用程序提供的原始对象,因此您可以调用特定于业务的方法(如getEmail()
,getEmployeeNumber()
和等等)。
到现在为止你可能想知道,所以我什么时候提供UserDetails
对象?我怎么做?我以为你说这个东西是声明性的,我不需要编写任何Java代码 - 是什么给出的?简短的回答是有一个名为UserDetailsService
的特殊界面。此接口上唯一的方法接受基于String
的用户名参数并返回UserDetails
:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
这是在Spring Security内为用户加载信息的最常用方法,只要需要有关用户的信息,您就会看到它在整个框架中使用。
上成功的认证,UserDetails
被用来建立存储在SecurityContextHolder
(关于这一点的Authentication
对象下面)。好消息是我们提供了许多UserDetailsService
实现,包括一个使用内存映射(InMemoryDaoImpl
)和另一个使用JDBC(JdbcDaoImpl
)的实现。但是,大多数用户倾向于自己编写,他们的实现通常只是位于代表其员工,客户或应用程序其他用户的现有数据访问对象(DAO)之上。记住使用上面的代码片段始终可以从SecurityContextHolder
获得UserDetailsService
返回的优点。
hello springSecurity!!! |
---|
UserDetailsService 经常有些混乱。它纯粹是用户数据的DAO,除了将数据提供给框架内的其他组件之外,不执行任何其他功能。特别是,它不会对用户进行身份验证,这是由AuthenticationManager 完成的。在许多情况下,如果您需要自定义身份验证过程,直接实现AuthenticationProvider 会更有意义。 |
一个GrantedAuthority
除了校长之外,Authentication
提供的另一个重要方法是getAuthorities()
。此方法提供GrantedAuthority
个对象的数组。毫不奇怪,GrantedAuthority
是授予校长的权力。这些权力通常是“角色”,例如ROLE_ADMINISTRATOR
或ROLE_HR_SUPERVISOR
。稍后将为web授权,方法授权和域对象授权配置这些角色。Spring Security的其他部分能够解释这些权威,并期望它们存在。GrantedAuthority
对象通常由UserDetailsService
加载。
通常GrantedAuthority
对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,你不可能有一个GrantedAuthority
代表Employee
对象编号54的权限,因为如果有数千个这样的权限,你很快就会耗尽内存(或者,至少,因为应用程序需要很长时间来验证用户身份)。当然,Spring Security专门用于处理这个常见要求,但您可以使用项目的域对象安全功能来实现此目的。
摘要
Spring Security的主要构建块是:
SecurityContextHolder
,提供SecurityContext
的访问权限。SecurityContext
,保存Authentication
和可能的特定于请求的安全信息。Authentication
,以特定于Spring Security的方式代表校长。GrantedAuthority
,以反映授予主体的应用程序范围的权限。UserDetails
,提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息。UserDetailsService
,在基于String
的用户名(或证书ID等)中传递时创建UserDetails
。
既然您已经了解了这些重复使用的组件,那么让我们仔细看看身份验证过程。
Spring Security的整体原理
让我们考虑一个每个人都熟悉的标准身份验证方案。
- 提示用户使用用户名和密码登录。
- 系统(成功)验证用户名的密码是否正确。
- 获取该用户的上下文信息(他们的角色列表等)。
- 为用户建立安全上下文
- 用户继续进行,可能执行一些可能受访问控制机制保护的操作,该访问控制机制针对当前安全上下文信息检查操作所需的许可。
前三项构成了身份验证过程,因此我们将在Spring Security内查看这些过程是如何发生的。
- 获取用户名和密码并将其合并到
UsernamePasswordAuthenticationToken
的实例中(我们之前看到的Authentication
接口的实例)。 - 令牌被传递给
AuthenticationManager
的实例以进行验证。 AuthenticationManager
在成功验证后返回完全填充的Authentication
实例。- 通过调用
SecurityContextHolder.getContext().setAuthentication(…)
建立安全上下文,传入返回的身份验证对象。
编写Security处理器
中文教程
接下来我们使用Spring Security编写管理员认证和授权功能。Spring Security在访问接口时进行认证和授权,所以Spring Security的相关代码编写在管理员API模块。
在springboot中使用Spring Security时,登录后会配置跳转页面。但boots商城是前后端分离项目,所有认证和授权的结果,只是返回json字符串让前端去处理。所以我们要创建认证成功处理器
、认证失败处理器
、未登录处理器
、权限不足处理器
、登出成功处理器
处理不同的结果,Spring Security通过实现接口编写结果处理器。
主要解决两个问题:
- 认证(Authentication)
解决你是谁的问题,具体表现为注册与登录
- 授权(Authorization)
解决你能干什么的问题,你登录后有哪些权限。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
/**
* 登出成功处理器
*
* @author bootsCoder
* @date created on 2024/4/18
*/
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
BaseResult result = new BaseResult(200, "注销成功", null);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
编写配置类
/**
* Spring Security配置
*
* @author bootsCoder
* @date created on 2024/4/18
*/
@Configuration
public class SecurityConfig {
//
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
/**
* 自定义表单登录
*/
http.formLogin(
form -> {
form.usernameParameter("username") // 用户名项
.passwordParameter("password") // 密码项
.loginProcessingUrl("/admin/login") // 登录提交路径
.successHandler(new LoginSuccessHandler()) // 登录成功处理器
.failureHandler(new LoginFailHandler()); // 登录失败处理器
}
);
// 权限拦截配置
http.authorizeHttpRequests(
resp -> {
resp.requestMatchers("/login", "/admin/login").permitAll(); // 登录请求不需要认证
resp.anyRequest().authenticated();// 其余请求都需要认证
}
);
/**
* 退出登录配置
*/
http.logout(
logout -> {
logout.logoutUrl("/admin/logout") // 注销的路径
.logoutSuccessHandler(new MyLogoutSuccessHandler()) // 登出成功处理器
.clearAuthentication(true) // 清除认证数据
.invalidateHttpSession(true);// 清除session
}
);
// 异常处理
http.exceptionHandling(
exception -> {
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint())// 未登录处理器
.accessDeniedHandler(new MyAccessDeniedHandler()); // 权限不足处理器
}
);
// 跨域访问
http.cors();
// 关闭csrf防护
http.csrf(csrf ->{
csrf.disable();
});
return http.build();
}
/**
* 加密工具
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
编写认证授权相关的服务方法
<select id="findAllPermission" resultType="com.bootscoder.shopping_common.pojo.Permission" parameterType="string">
SELECT
DISTINCT boots_permission.*
FROM
boots_admin
LEFT JOIN boots_admin_role
ON boots_admin.aid = boots_admin_role.aid
LEFT JOIN boots_role
ON boots_admin_role.rid = boots_role.rid
LEFT JOIN boots_role_permission
ON boots_role.rid = boots_role_permission.rid
LEFT JOIN boots_permission
ON boots_role_permission.pid = boots_permission.pid
WHERE boots_admin.username = #{username}
</select>
编写认证授权逻辑
/**
* 认证授权逻辑
*
* @author bootsCoder
* @date created on 2024/4/18
*/
@Service
public class MyUserDetailService implements UserDetailsService {
@DubboReference
private AdminService adminService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.认证
Admin admin = adminService.findByAdminName(username);
if(admin == null){
throw new UsernameNotFoundException("用户不存在");
}
// 2.授权
List<Permission> permissions = adminService.findAllPermission(username);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (permissions.get(0) != null){
for (Permission permission : permissions) {
grantedAuthorities.add(new SimpleGrantedAuthority(permission.getUrl()));
}
}
// 3.封装为UserDetails对象
UserDetails userDetails = User.withUsername(admin.getUsername())
.password(admin.getPassword())
.authorities(grantedAuthorities)
.build();
// 4.返回封装好的UserDetails对象
return userDetails;
}
}
修改新增修改管理员方法-- 添加加密
@Override
public void update(Admin admin) {
// 如果前端传来空密码,则密码还是原来的密码
if(!StringUtils.hasText(admin.getPassword())){
// 查询原来的密码
String password = adminMapper.selectById(admin.getAid()).getPassword();
admin.setPassword(password);
}
adminMapper.updateById(admin);
}
@PutMapping("/update")
public BaseResult update(@RequestBody Admin admin) {
String password = admin.getPassword();
// 密码不为空加密
if (StringUtils.hasText(password)){
password = encoder.encode(password);
admin.setPassword(password);
}
adminService.update(admin);
return BaseResult.ok();
}
权限管理_获取登录管理员名&接口鉴权配置
/**
* 分页查询角色
* @param page 页码
* @param size 每页条数
* @return 查询结果
*/
@GetMapping("/search")
@PreAuthorize("hasAnyAuthority('/role/search')")
public BaseResult<Page<Role>> search(int page, int size){
Page<Role> rolePage = roleService.search(page, size);
return BaseResult.ok(rolePage);
}
mybatis 封装的时候 为空时判list长度 1
// 2.授权
List<Permission> permissions = adminService.findAllPermission(username);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (permissions.get(0) != null){
for (Permission permission : permissions) {
grantedAuthorities.add(new SimpleGrantedAuthority(permission.getUrl()));
}
}
测试认证功能成功