03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC

1. 返回JSON

为什么要返回JSON

前后端分离成为企业应用开发中的主流,前后端分离通过json进行交互,登录成功和失败后不用页面跳转,而是给前端返回一段JSON提示, 前端根据JSON提示构建页面.

需求: 对于登录的各种状态 , 给前端返回JSON数据

1.1 在vo包下创建一个HttpResult对象, 存储返回的信息

vo即 value object值对象, 所有不存储在数据库中的对象就放在vo包下

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult {
    private Integer code;
    private String msg;
    private Object data;
    public HttpResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

1.2 创建认证(登录)成功处理器AuthenticationSuccessHandler

当且仅当认证(登录)成功后, 该处理器开始工作, 给前端返回一个JSON

@Component
public class MyAuthenticationSuccessHandle implements AuthenticationSuccessHandler {

    //注入一个序列化器, 可以将JSON序列化, 反序列化
    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        //借助Lombok实现建造者模式, 并通过建造者模式创建对象

        HttpResult httpResult = HttpResult.builder()
                .code(1)
                .msg("登陆成功")
                .build();
               /*与普通new方法一样
                HttpResult httpResult = new HttpResult(200, "登录成功");
                *HttpResult中自定义的有参构造方法只写了code和msg,因此data可以选择性传
                HttpResult httpResult = new HttpResult(200, "登录成功",authentication);
                */
        //将对象转化为JSON
        String responseJson = objectMapper.writeValueAsString(httpResult);

        PrintWriter writer = response.getWriter();
        writer.println(responseJson);
    }
}

在安全配置类中注入登录成功处理器

@Configuration
public class MySSWebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 注入登陆成功的处理器
    @Autowired
    private MyAuthenticationSuccessHandle successHandler;
    
    
        //放开登录页面权限,任何人都能尝试登录,否则登录界面都见不到
        http.formLogin().permitAll();
    }

1.3 登录失败处理器 , 无权限处理器都如法炮制

 @Resource
    private ObjectMapper objectMapper;


    /**
     * @param request 当前的请求对象
     * @param response 当前的响应对象
     * @param exception 失败的原因的异常
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.err.println("登陆失败");
        //设置响应编码
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        //返回JSON出去
        HttpResult result=new HttpResult(-1,"登陆失败");
 
        //把result序列化为JSON字符串
        String responseJson = objectMapper.writeValueAsString(result);
        //响应出去
        PrintWriter out = response.getWriter();
        out.write(responseJson);
        out.flush();
    }
}

/**
 * 无权限的处理器
 */
@Component
public class AppAccessDeniedHandler implements AccessDeniedHandler {

    //声明一个把对象转成JSON的对象
@Resource
    private ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //设置响应编码
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        //返回JSON出去
        HttpResult result=new HttpResult(-1,"您没有权限访问");
        //把result转成JSON
        String json = objectMapper.writeValueAsString(result);
        //响应出去
        PrintWriter out = response.getWriter();
        out.write(json);
        out.flush();
    }
}
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 注入登陆成功的处理器
    @Autowired
    private AutheticationSuccessHandle successHandler;

    // 注入登陆失败的处理器
    @Autowired
    private AppAuthenticationFailureHandler failureHandler;

    // 注入没有权限的处理器
    @Autowired
    private AppAccessDeniedHandler accessDeniedHandler;

    //  注入退出成功的处理器
    @Autowired
    private AppLogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        http.formLogin().successHandler(successHandler).failureHandler(failureHandler).permitAll();
        http.logout().logoutSuccessHandler(logoutSuccessHandler);
        http.authorizeRequests().mvcMatchers("/teacher/**").hasRole("teacher").anyRequest().authenticated();
    }
}

2. UserDetail接口

UserDetails接口是Spring Security进行身份验证和授权的核心接口之一。

通过实现UserDetails接口,可以定义如何存储和操作用户的详情信息,比如用户名、密码、权限等。

下面是几个编写一个实现了UserDetails接口的VO(Value Object)类的原因:

定制用户信息:Spring Security需要从应用程序获取用户的认证信息(如用户名、密码和权限)。大多数应用程序会将这些信息存储在数据库中,并且每个应用程序的用户模型都可能不同。通过实现UserDetails接口,可以根据应用程序的用户模型定制用户信息,使其包含Spring Security所需的任何必要信息。

权限和角色管理:UserDetails提供了获取用户权限(GrantedAuthority)的方法,这对于角色基于的访问控制至关重要。通过实现这个接口,可以在用户实体中很方便地管理用户的角色和权限,并且可以灵活地控制用户的访问权限。

灵活性和可扩展性:通过实现UserDetails接口,可以根据需要添加额外的属性和方法,从而为应用程序提供更大的灵活性和可扩展性。例如,可以添加手机号码、电子邮件地址或其他自定义的用户属性。

集成Spring Security的认证和授权机制:实现UserDetails接口是将应用程序的用户模型与Spring Security框架集成的一种方式。这样做可以利用Spring Security提供的强大的安全特性,如密码加密、会话管理、CSRF保护等,而无需从头开始实现这些安全特性。

提高代码的可读性和维护性:通过创建一个明确的VO类来实现UserDetails接口,可以提高代码的组织性、可读性和维护性。这样做有助于将安全框架的实现细节与应用程序的业务逻辑分离,使得代码更加清晰和易于维护。

3.基于数据库的认证

前面的自定义用户过程中, 代码是写死的, 这在实际使用中显然不现实.

基于数据库认证的核心思想是 : 定义一个VO类(这里起名SecurtiyUser),该类实现UserDeatails接口, 今后前端–后端–数据库交互用户信息就通过这个类

3.1 项目设计

3.1.1 导入数据库文件

最终包含5张表, 即第一章中提到的实现RBAC最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)
03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第1张图片

最基本的三张表是:

用户表 (Users):存储用户信息,每个用户都可以被分配一个或多个角色。至少包含用户ID和用户名等基本信息。

角色表 (Roles):存储角色信息,角色代表了一组权限的集合,用于控制访问权限。至少包含角色ID和角色名等基信息。

权限表 (Permissions):存储权限信息,权限定义了对系统资源的访问能力,比如读取、写入、修改等操作。至少包含权限ID和权限描述等基本信息。

然而,仅有上述三张表是不足以实现完整的RBAC功能的,因为它们没有建立用户、角色和权限之间的关联。因此,通常还需要额外的关联表来建立这些实体之间的多对多关系:

用户-角色关联表 (User_Roles):建立用户和角色之间的关系。这张表至少包含用户ID和角色ID,表示哪些用户属于哪些角色。

角色-权限关联表 (Role_Permissions):建立角色和权限之间的关系。这张表至少包含角色ID和权限ID,表示哪些角色拥有哪些权限。

3.1.2 中间件选择

存取数据需要一个中间件 , 这里选择MB

3.2 需求

根据用户名获取用户信息, 能获取到框架再自动比对密码

3.3 实现

3.3.1 根据用户名获取用户信息, 能获取到框架再自动比对密码

本次开发采用由下到上开发, 并逐层单元测试的方法进行

3.3.1.1 开发并测试dao(mapper接口层)

03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第2张图片

package com.sunsplanter.entity;

@Data
//实现Serializable接口, 方便对象序列化和反序列化
public class SysUser implements Serializable {
    private Integer userId;
    private String username;
    private String password;
    private String sex;
    private String address;
    private Integer enabled;
    private Integer accountNoExpired;
    private Integer credentialsNoExpired;
    private Integer accountNoLocked;
}
//实际就是之前的mapper接口SysUser
//要么在每个mapper中使用@Mapper声明, 要么在启动类中用@MapperScan指定扫描位置
@Mapper
public interface SysUserDao {
    /**
     * 根据用户名获取用户信息
     * 建议每个参数都使用@Param绑定,确保准确传递到xml文件中
     */
    SysUser getUserByUserName(@Param("username") String username);
}

DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.sunsplanter.dao.SysUserDao">
    <select id="getUserByUserName" resultType="sysUser">
        select user_id,username,password,sex,address,enabled,account_no_expired,credentials_no_expired,account_no_locked
        from sys_user where username=#{username}
    select>

mapper>

单元测试

	@Resource
	private SysUserDao sysUserDao;

	@Test
	void getUserByUserName() {
		SysUser sysUser = sysUserDao.getUserByUserName("obama");
		assertNotNull(sysUser);
	}

启动后左侧提示通过测试, 控制台输出信息SQL语句与SQL执行的结果
03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第3张图片

3.3.1.2 开发并测试service
public interface SysUserService{
	//根据用户名获取用户信息,以便确认存在此用户
	//此处不用@Param注解,因为MB注解只在DAO层
	SysUser.getUserByUserName(String userName)
}
@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private SysUserDao sysUserDao;

    @Override
    public SysUser geUsertUserByName(String userName){
        return sysUserDao.getUserByUserName(userName);
    }
}

单元测试:

 @Resource
    private SysUserService sysUserService;
    
    @Test
    void getUserByUsername() {
        SysUser sysUser = SysUserService.getUserByname("obama");
        assertNotNull(sysUser);
    }

在这里插入图片描述

3.3.1.3 整合SS实现Service

本步骤最终效果最终与3.3.1.2一致, 只是本步骤额外整合了SS

整合SS验证用户是否存在的核心两步是:

  1. 定义一个VO类作为中介, 存在于后端三层结构与数据库之间
  2. 定义一个类实现UserDetailsService接口, 用来判断是否存在用户

03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第4张图片

定义一个VO类(这里起名SecurtiyUser),该类实现UserDeatails接口, 今后前端–后端–数据库交互用户信息就通过这个类

public class SecurityUser implements UserDetails {

    //这个类作为数据库与后端代码交互用户信息的中转站,自然要获取一个实体对象才能操作
    private  final SysUser sysUser;

    public SecurityUser(SysUser sysUser) {
        this.sysUser=sysUser;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        String userPassword=this.sysUser.getPassword();
        //注意清除密码
        this.sysUser.setPassword(null);
        return userPassword;

    }

    @Override
    public String getUsername() {
        return sysUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return sysUser.getAccountNoExpired().equals(1);
    }

    @Override
    public boolean isAccountNonLocked() {
        return sysUser.getAccountNoLocked().equals(1);
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return sysUser.getCredentialsNoExpired().equals(1);
    }

    @Override
    public boolean isEnabled() {
        return sysUser.getEnabled().equals(1);
    }
}

新建UserServiceImpl 实现UserDetailService接口, 判断是否存在该用户

@Service
//UserDetailsService接口只有一个方法,这个接口的唯一作用就是判断用户存不存在
public class UserServiceImpl implements UserDetailsService {
    @Resource
    private SysUserDao sysUserDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserDao.getByUserName(username);
        if(null==sysUser){
            throw new UsernameNotFoundException("账号不存在");
        }

        //若通过username一步一步向下调用到数据库最终查到该用户, 
        //则利用中介SecurityUser将其返回
        return new SecurityUser(sysUser);
    }
}

单元测试:

	@Resource
	private UserServiceImpl userService;

	@Test
	void loadUserByUsername() {
		UserDetails userDetails = userService.loadUserByUsername("obama");
		assertNotNull(userDetails);
	}
3.3.1.4 controller
@RestController
@Slf4j
@RequestMapping("/student")
public class StudentController {
    @GetMapping("/query")
    public String queryInfo(){
        return "query student";
    }
    
    @GetMapping("/add")
    public String addInfo(){
        return "add  student!";
    }


@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
    @GetMapping("/query")
    @PreAuthorize("hasAuthority('teacher:query')")
    public String queryInfo(){
        return "I am a teacher!";
    }
}

最终符合权限的人可以正常查询, 即之前写入数据库中三张表确定权限 , tomas是教师, eric是学生
在这里插入图片描述
03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第5张图片
03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第6张图片
项目结构如图:
03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第7张图片
相比与之前 , 无非是多了两个东西:
SecurityUser类实现UserDetails, 实现数据的中转交互
UserServiceImpl实现UserDetailsService, 用于登录前前判断用户是否存在

3.4 查询数据库得到权限

3.4.1 存在的问题

根据表内容:
sys_role_permission和sy_permission
03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第8张图片

1号角色(管理员)拥有1,3,4,5,9,10,17号功能的权限
2号角色(教师)拥有2,3,4,5,9号功能的权限

表中我们预设的内容 , eric作为学生, 其应具有1,2,6,9号功能的权限, 即 学生管理/查询, 导出学生信息, 教师查询功能的权限

在上例中再拷贝一个之前写过的查询用户信息的类:

@RestController
@Slf4j
public class CurrentLoginUserInfoController {

    @GetMapping("/getLoginUserInfo")
    public Principal getLoginUserInfo(Principal principle){
        return principle;
    }

}

并且之后进入http://localhost:8080/getLoginUserInfo查询
发现eric作为学生, 并没有任何权限,
因此若我们进入teacher/quey, 会报403
03 SS之返回JSON+UserDetail接口+基于数据库实现RBAC_第9张图片

这说明数据库中的权限并没有被我们查询出来

因此现在的目标是编写动态SQL语句, 根据传入的用户ID判断所属的角色, 以及拥有的权限,最终返回权限

你可能感兴趣的:(SpringSecurity,json)