学习记录


双token机制

参考

https://zhuanlan.zhihu.com/p/699850219

简答易懂的springsecurity认证流程!

1. 各文件作用

LoginSuccessHandler.java

  • 功能:处理用户登录成功后的操作。
  • 代码含义
    • 获取登录用户信息。
    • 生成accessTokenrefreshToken
    • 将生成的token通过ResponseResult.write方法返回给前端。

RequestAuthenticationFilter.java

  • 功能:过滤和验证每个请求的token。
  • 代码含义
    • 从请求头或请求参数中获取accessToken
    • 如果accessToken存在且有效,解析token并设置用户的认证信息。
    • 如果accessToken过期,尝试从请求头中获取refreshToken,并使用refreshToken生成新的accessTokenrefreshToken,然后将新的token保存到Redis中。

JwtUtils.java

  • 功能:生成和解析JWT(JSON Web Token)。
  • 代码含义
    • generateToken方法:使用私钥生成JWT。
    • getUsernameFromToken方法:使用公钥解析JWT并获取用户名。

RsaUtils.java

  • 功能:处理RSA加密和解密。
  • 代码含义
    • 生成公钥和私钥文件。
    • 从文件中读取公钥和私钥。

ResponseResult.java

  • 功能:统一返回结果的工具类。
  • 代码含义
    • 提供了多种静态方法,用于生成不同类型的响应结果。
    • write方法:将响应结果写入HttpServletResponse

SecurityConfig.java

  • 功能:Spring Security的核心配置。
  • 代码含义
    • 配置了自定义的登录成功处理器LoginSuccessHandler
    • 配置了自定义的请求过滤器RequestAuthenticationFilter

BUserController.java

  • 功能:用户相关的前端控制器。
  • 代码含义
    • addUser方法:处理用户注册请求,保存用户信息。

UserDetailServiceImpl.java

  • 功能:从数据库查询登录用户信息及其权限。
  • 代码含义
    • loadUserByUsername方法:根据用户名查询用户信息,并返回UserDetails对象。

在 Spring Security 中,用户提交登录表单后,身份验证的过程是由 Spring Security 自动处理的。具体来说,Spring Security 会使用配置的 UserDetailsServiceBCryptPasswordEncoder 来验证用户的用户名和密码。如果验证成功,Spring Security 会调用 LoginSuccessHandler 中的 onAuthenticationSuccess 方法。

以下是身份验证的关键步骤:

  1. 用户提交登录表单,表单数据会被发送到 SecurityConfig 中配置的 loginProcessingUrl
  2. Spring Security 会使用配置的 UserDetailsService 加载用户信息。
  3. Spring Security 会使用配置的 BCryptPasswordEncoder 验证用户密码。
  4. 如果验证成功,Spring Security 会调用 LoginSuccessHandler 中的 onAuthenticationSuccess 方法。

在这个配置中,/login URL 会被 Spring Security 拦截并处理,UserDetailsServiceImpl 会加载用户信息,BCryptPasswordEncoder 会验证密码。如果验证成功,LoginSuccessHandler 会被调用。

AuthorityUtils.commaSeparatedStringToAuthorityList(auths)方法将权限字符串”echoes”转换为GrantedAuthority对象的列表,供Spring Security使用

RefreshTokenAspect.java

  • 功能:AOP切面,处理token的刷新
  • 代码含义
    • addToken方法:在控制器方法执行前后,检查并刷新token。

RefreshTokenAspect.javaRequestAuthenticationFilter.java的区别

  1. RefreshTokenAspect.java

    • 作用:这是一个AOP切面,用于在控制器方法执行后,检查并添加新的访问令牌(access token)和刷新令牌(refresh token)。它会从Redis或请求头中读取token,并将其添加到响应结果中。
    • 应用场景:当用户成功登录或刷新token时,系统会在响应中返回新的access token和refresh token。
    • 请求例子:用户登录成功后,系统会返回一个包含新的access token和refresh token的响应。
  2. RequestAuthenticationFilter.java

    • 作用:这是一个过滤器,用于在每个请求到达服务器时,验证请求中的access token。如果access token过期,则尝试使用refresh token生成新的access token和refresh token,并将其保存到Redis中。
    • 应用场景:当用户发起需要身份验证的请求时,系统会检查请求中的access token,如果过期则使用refresh token重新生成token。
    • 请求例子:用户访问受保护的资源时,系统会检查请求头中的access token,如果过期则使用refresh token重新生成并返回新的token。

通过这两个文件的配合,系统能够在每次请求时验证用户身份,并在必要时刷新token,确保用户的会话持续有效。

2. 详=各文件之间的关系

  • SecurityConfig配置了LoginSuccessHandlerRequestAuthenticationFilter,确保登录成功后生成token,并在每个请求中验证token。
  • LoginSuccessHandler在用户登录成功后生成accessTokenrefreshToken
  • RequestAuthenticationFilter在每个请求中验证accessToken,如果过期则使用refreshToken生成新的token。
  • JwtUtilsRsaUtils提供了生成和解析JWT的工具方法。
  • ResponseResult统一了返回结果的格式。
  • BUserController处理用户注册请求。
  • UserDetailServiceImpl从数据库查询用户信息。
  • RefreshTokenAspect在控制器方法执行前后,检查并刷新token。

3. 在登录、注册等请求发生时的工作流、

登录请求

  1. 用户提交登录请求。
  2. SecurityConfig配置的登录处理器拦截请求。
  3. 用户认证成功后,LoginSuccessHandler生成accessTokenrefreshToken,并返回给前端。
  4. 前端保存accessTokenrefreshToken

注册请求

  1. 用户提交注册请求。
  2. BUserControlleraddUser方法处理请求,保存用户信息到数据库。

其他请求

  1. 用户提交请求,携带accessToken
  2. RequestAuthenticationFilter拦截请求,验证accessToken
  3. 如果accessToken有效,解析token并设置用户的认证信息。
  4. 如果accessToken过期,使用refreshToken生成新的accessTokenrefreshToken,并保存到Redis中。
  5. 请求继续执行,返回结果。

4. 其他

Redis的使用

  • Redis用于存储和管理accessTokenrefreshToken,确保在accessToken过期后可以通过refreshToken重新生成新的accessToken

AOP切面的使用

  • RefreshTokenAspect通过AOP切面,在控制器方法执行前后,检查并刷新token,确保用户在使用过程中不会因为token过期而中断。

通过上述机制,该项目实现了双token机制,确保在accessToken过期后可以通过refreshToken重新生成新的accessToken,从而提高了系统的安全性和用户体验。

@Column和@TableField

@Column用在jpa框架

@TableField用在MyBatis-Plus

@Slf4j

https://www.jb51.net/program/322645laz.htm

我一般使用的是@SLf4j作为日志输出。

使用logback.xml,需要引入下面两个依赖。

如果你使用的是springboot,那么springboot的starter会自带这两个依赖,不用再重复导入。

IService

Service 中的方法通常是业务逻辑层的方法,用于处理具体的业务需求。以下是一些常见的 Service 方法:

  1. 保存数据

    boolean save(T entity);
  2. 批量保存数据

    boolean saveBatch(Collection<T> entityList);
  3. 保存或更新数据

    boolean saveOrUpdate(T entity);
  4. 批量保存或更新数据

    boolean saveOrUpdateBatch(Collection<T> entityList);
  5. 根据ID删除数据

    boolean removeById(Serializable id);
  6. 根据条件删除数据

    boolean remove(Wrapper<T> queryWrapper);
  7. 批量删除数据

    boolean removeByIds(Collection<? extends Serializable> idList);
  8. 根据ID更新数据

    boolean updateById(T entity);
  9. 根据条件更新数据

    boolean update(T entity, Wrapper<T> updateWrapper);
  10. 批量更新数据

    boolean updateBatchById(Collection<T> entityList);
  11. 根据ID查询数据

    T getById(Serializable id);
  12. 根据条件查询单条数据

    T getOne(Wrapper<T> queryWrapper);
  13. 查询所有数据

    List<T> list();
  14. 根据条件查询数据列表

    List<T> list(Wrapper<T> queryWrapper);
  15. 分页查询数据

    Page<T> page(Page<T> page);
    // 假设要进行无条件的分页查询,每页显示10条记录,查询第1页
    IPage<User> page = new Page<>(1, 10);
    IPage<User> userPage = userService.page(page); // 调用 page 方法
    List<User> userList = userPage.getRecords();
    long total = userPage.getTotal();
    System.out.println("Total users: " + total);
    for (User user : userList) {
    System.out.println("User: " + user);
    }
  16. 根据条件分页查询数据

    Page<T> page(Page<T> page, Wrapper<T> queryWrapper);

这些方法提供了对数据库进行增删改查的基本操作。

sql映射

在 MyBatis 中,可以通过在映射文件中定义 SQL 语句,并将其与 Java 接口方法关联起来。以下是一个简单的例子,展示了如何在 BUserMapper.xml 文件中定义 SQL 语句,并将其与 BUserMapper 接口中的方法进行映射。

首先,定义 BUserMapper 接口:

package com.example.mysecurity.dao;

import com.example.mysecurity.model.BUser;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface BUserMapper {
@Select("SELECT * FROM b_user WHERE username = #{username}")
BUser findByUsername(String username);

List<BUser> findAll();
}

然后,在 BUserMapper.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.example.mysecurity.dao.BUserMapper">

<!-- 定义 findAll 方法的 SQL 语句 -->
<select id="findAll" resultType="com.example.mysecurity.model.BUser">
SELECT * FROM b_user
</select>

</mapper>

在这个例子中:

  1. BUserMapper 接口中定义了两个方法:findByUsernamefindAll
  2. findByUsername 方法使用了注解 @Select 来直接在接口中定义 SQL 语句。
  3. findAll 方法在 BUserMapper.xml 文件中定义了 SQL 语句,并通过 id 属性与接口中的方法名进行关联。

通过这种方式,MyBatis 可以根据接口方法调用相应的 SQL 语句。

logaspect

package com.example.dorm.aspect;

import com.example.dorm.entity.AccessLog;
import com.example.dorm.mapper.LogMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;

@Aspect
@Component
public class LogAspect {

@Autowired
private LogMapper LogMapper;

@Around("execution(* com.example.dorm.controller.*.*(..))") // 拦截 controller 包下的所有方法
public Object logAccess(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取用户信息,这里假设用户ID和IP可以通过 Security Context 获取
String username = getUserIdFromContext();
String userIp = getUserIp();

// 获取访问的页面
String accessedPage = joinPoint.getSignature().toString();

// 获取操作类型(假设可以通过请求方法获取)
String actionType = getRequestActionType(); // 比如 GET, POST 等

// 创建并保存访问日志
AccessLog accessLog = new AccessLog();
accessLog.setUsername(username);
accessLog.setUserIp(userIp);
accessLog.setAccessedPage(accessedPage);
accessLog.setDate(LocalDateTime.now());
accessLog.setType(actionType);

// 保存日志
LogMapper.insert(accessLog);

return joinPoint.proceed();
}

private String getUserIdFromContext() {
// 从 Spring Security 获取当前用户的 ID
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getName(); // 假设用户名是用户 ID
}

private String getUserIp() {
// 获取用户的 IP 地址,通常通过 HTTP 请求头获取
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getRemoteAddr();
}

private String getRequestActionType() {
// 通过 HTTP 请求方法获取操作类型,例如 GET、POST 等
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getMethod();
}
}



Author: CuberSugar
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source CuberSugar !
  TOC