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最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)
最基本的三张表是:
用户表 (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接口层)
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); }
复制
<?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.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执行的结果
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验证用户是否存在的核心两步是:
- 定义一个VO类作为中介, 存在于后端三层结构与数据库之间
- 定义一个类实现UserDetailsService接口, 用来判断是否存在用户
定义一个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是学生
项目结构如图:
相比与之前 , 无非是多了两个东西:
SecurityUser类实现UserDetails, 实现数据的中转交互
UserServiceImpl实现UserDetailsService, 用于登录前前判断用户是否存在
3.4 查询数据库得到权限
3.4.1 存在的问题
根据表内容:
sys_role_permission和sy_permission
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
这说明数据库中的权限并没有被我们查询出来
因此现在的目标是编写动态SQL语句, 根据传入的用户ID判断所属的角色, 以及拥有的权限,最终返回权限