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判断所属的角色, 以及拥有的权限,最终返回权限