Spring Boot 项目整合Spring Security 进行身份验证

前言

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。

Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以很容易地进行扩展以满足自定义要求。

引入库

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.cdkjframework</groupId>
            <artifactId>cdkj-entity</artifactId>
        </dependency>
        <dependency>
            <groupId>com.cdkjframework</groupId>
            <artifactId>cdkj-util</artifactId>
        </dependency>
        <dependency>
            <groupId>com.cdkjframework</groupId>
            <artifactId>cdkj-redis</artifactId>
        </dependency>

项目结构

受权

身份验证筛选器 AuthenticationFilter

java 复制代码
package com.cdkjframework.security.authorization;

import com.cdkjframework.constant.BusinessConsts;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.authorization
 * @ClassName: AuthenticationFilter
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    /**
     * 请求类型
     */
    private final List<String> CONTENT_TYPE_LIST = Arrays.asList(MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE);

    /**
     * 尝试身份验证
     *
     * @param request  请求
     * @param response 响应
     * @return 返回结果
     * @throws AuthenticationException 权限异常
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //attempt Authentication when Content-Type is json
        if (CONTENT_TYPE_LIST.contains(request.getContentType())) {
            Object userName = request.getAttribute(BusinessConsts.USER_NAME);
            Object password = request.getAttribute(BusinessConsts.PASSWORD);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        } else {
            return super.attemptAuthentication(request, response);
        }
    }
}

用户身份验证提供程序 UserAuthenticationProvider

java 复制代码
package com.cdkjframework.security.authorization;

import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.security.encrypt.Md5PasswordEncoder;
import com.cdkjframework.security.service.UserRoleService;
import com.cdkjframework.util.tool.number.ConvertUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.authorization
 * @ClassName: UserAuthenticationProvider
 * @Description: 自定义登录验证
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {

  /**
   * 用户信息查询服务
   */
  private final UserDetailsService userDetailsService;

  /**
   * 用户角色服务
   */
  private final UserRoleService userRoleService;

  /**
   * 构造函数
   *
   * @param userDetailsService 用户服务
   * @param userRoleService    用户角色服务
   */
  public UserAuthenticationProvider(UserDetailsService userDetailsService, UserRoleService userRoleService) {
    this.userDetailsService = userDetailsService;
    this.userRoleService = userRoleService;
  }

  /**
   * 身份权限验证
   *
   * @param authentication 身份验证
   * @return 返回权限
   * @throws AuthenticationException 权限异常
   */
  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 获取表单输入中返回的用户名
    String userName = ConvertUtils.convertString(authentication.getPrincipal());
    // 获取表单中输入的密码
    String password = ConvertUtils.convertString(authentication.getCredentials());
    // 查询用户是否存在
    Object userDetails = userDetailsService.loadUserByUsername(userName);
    if (userDetails == null) {
      throw new UsernameNotFoundException("用户名不存在");
    }
    SecurityUserEntity userInfo = (SecurityUserEntity) userDetails;
    // 我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的
    if (!new Md5PasswordEncoder().matches(userInfo.getPassword(), password)) {
      throw new BadCredentialsException("用户名或密码不正确");
    }
    // 还可以加一些其他信息的判断,比如用户账号已停用等判断
    if (userInfo.getStatus().equals(IntegerConsts.ZERO) ||
            userInfo.getDeleted().equals(IntegerConsts.ONE)) {
      throw new LockedException("该用户已被冻结");
    }
    // 角色集合
    Set<GrantedAuthority> authorities = new HashSet<>();
    // 查询用户角色
    List<RoleEntity> sysRoleEntityList = userRoleService.listRoleByUserId(userInfo.getUserId());
    for (RoleEntity sysRoleEntity : sysRoleEntityList) {
      authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));
    }
    userInfo.setRoleList(sysRoleEntityList);
    userInfo.setAuthorities(authorities);
    // 进行登录
    return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
  }

  /**
   * 是否支持权限验证
   */
  @Override
  public boolean supports(Class<?> authentication) {
    return true;
  }
}

用户权限评估器 UserPermissionEvaluator

typescript 复制代码
package com.cdkjframework.security.authorization;

import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.authorization
 * @ClassName: UserPermissionEvaluator
 * @Description: 自定义权限注解验证
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {

    /**
     * hasPermission鉴权方法
     * 这里仅仅判断PreAuthorize注解中的权限表达式
     * 实际中可以根据业务需求设计数据库通过targetUrl和permission做更复杂鉴权
     * 当然targetUrl不一定是URL可以是数据Id还可以是管理员标识等,这里根据需求自行设计
     *
     * @Param authentication  用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)
     * @Param targetUrl  请求路径
     * @Param permission 请求路径权限
     * @Return boolean 是否通过
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
//        // 获取用户信息
//        SecurityUserEntity securityUserEntity =(SecurityUserEntity) authentication.getPrincipal();
//        // 查询用户权限(这里可以将权限放入缓存中提升效率)
//        Set<String> permissions = new HashSet<>();
//        List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(securityUserEntity.getUserId());
//        for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {
//            permissions.add(sysMenuEntity.getPermission());
//        }
//        // 权限对比
//        if (permissions.contains(permission.toString())){
//            return true;
//        }
        return true;
    }

    /**
     * 用于评估权限的替代方法,其中只有目标对象的标识符可用,而不是目标实例本身。
     *
     * @param authentication 用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)
     * @param targetId       对象实例的标识符(通常为Long)
     * @param targetType     表示目标类型的字符串(通常是Java类名)。不为空。
     * @param permission     表达式系统提供的权限对象的表示形式。不为空。
     * @return 如果权限被授予,则为true,否则为false
     */
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

验证代码筛选器 ValidateCodeFilter

java 复制代码
package com.cdkjframework.security.authorization;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.security.AuthenticationEntity;
import com.cdkjframework.util.log.LogUtils;
import com.cdkjframework.util.network.ResponseUtils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.authorization
 * @ClassName: ValidateCodefilter
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

  /**
   * 过虑权限验证
   *
   * @param request     请求
   * @param response    响应
   * @param filterChain 过滤链
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String uri = request.getRequestURI();
    // 如果为get请求并且请求uri为/login(也就是我们登录表单的form的action地址)
    if (uri.contains(BusinessConsts.LOGIN) && !validateCode(request, response)) {
      return;
    }

    filterChain.doFilter(request, response);
  }

  /**
   * 验证用户输入的验证码和session中存的是否一致
   *
   * @param request  请求
   * @param response 响应
   */
  private boolean validateCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
    HttpSession httpSession = request.getSession();
    String validateValue = ConvertUtils.convertString(httpSession.getAttribute(BusinessConsts.IMAGE_CODE));
    //这里需要验证前端传过来的验证码是否和session里面存的一致,并且要判断是否过期
    if (StringUtils.isNullAndSpaceOrEmpty(validateValue)) {
      return true;
    }
    // 时间效验
    long time = ConvertUtils.convertLong(httpSession.getAttribute(BusinessConsts.TIME));
    final long EXPIRATION_TIME = IntegerConsts.FIVE * IntegerConsts.SIXTY * IntegerConsts.ONE_THOUSAND;
    if (EXPIRATION_TIME < (System.currentTimeMillis() - time)) {
      ResponseUtils.out(response, ResponseBuilder.failBuilder("验证码过期!"));
      return false;
    }
    validateValue = validateValue.toLowerCase();

    // 获取数据
    String[] values = request.getParameterValues(BusinessConsts.IMAGE_CODE);
    if (values == null || values.length == IntegerConsts.ZERO) {
      ResponseUtils.out(response, ResponseBuilder.failBuilder("验证码错误!"));
    }
    String verifyCode = ConvertUtils.convertString(values[IntegerConsts.ZERO]).toLowerCase();

    if (StringUtils.isNullAndSpaceOrEmpty(verifyCode) ||
        !validateValue.equals(verifyCode)) {
      ResponseUtils.out(response, ResponseBuilder.failBuilder("验证码错误!"));
      return false;
    }
    return true;
  }
}

配置

安全配置 SecurityConfigure

scss 复制代码
package com.cdkjframework.security.configure;

import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.security.authorization.AuthenticationFilter;
import com.cdkjframework.security.authorization.UserAuthenticationProvider;
import com.cdkjframework.security.authorization.UserPermissionEvaluator;
import com.cdkjframework.security.authorization.ValidateCodeFilter;
import com.cdkjframework.security.encrypt.JwtAuthenticationFilter;
import com.cdkjframework.security.encrypt.Md5PasswordEncoder;
import com.cdkjframework.security.handler.*;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.config
 * @ClassName: SecurityConfig
 * @Description: 权限配置 开启权限注解,默认是关闭的
 * @Author: xiaLin
 * @Version: 1.0
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfigure {

  /**
   * 放行路径
   */
  private final String[] PATTERNS = new String[]{"/**"};

  /**
   * 自定义登录成功处理器
   */
  private final UserLoginSuccessHandler userLoginSuccessHandler;
  /**
   * 自定义登录失败处理器
   */
  private final UserLoginFailureHandler userLoginFailureHandler;
  /**
   * 自定义注销成功处理器
   */
  private final UserLogoutSuccessHandler userLogoutSuccessHandler;
  /**
   * 自定义暂无权限处理器
   */
  private final UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
  /**
   * 自定义未登录的处理器
   */
  private final UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
  /**
   * 自定义登录逻辑验证器
   */
  private final UserAuthenticationProvider userAuthenticationProvider;

  /**
   * 读取配置文件
   */
  private final CustomConfig customConfig;

  /**
   * 身份验证管理器
   */
  @Resource(name = "authentication")
  private AuthenticationManager authentication;

  /**
   * 鉴权管理类
   */
  @Bean(name = "authentication")
  public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
  }

  /**
   * 加密方式
   */
  @Bean
  public Md5PasswordEncoder md5PasswordEncoder() {
    return new Md5PasswordEncoder();
  }

  /**
   * 注入自定义PermissionEvaluator
   */
  @Bean
  public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler() {
    DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
    handler.setPermissionEvaluator(new UserPermissionEvaluator());
    return handler;
  }

  /**
   * Spring Security 过滤链
   */
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    int size = customConfig.getPatternsUrls().size();
    String[] patternsUrls = customConfig.getPatternsUrls().toArray(new String[size]);
    return http
            // 配置未登录自定义处理类
            .httpBasic(basic ->
                    basic.authenticationEntryPoint(userAuthenticationEntryPointHandler)
            )
            // 禁用缓存
            .headers(header -> header.cacheControl(cache -> cache.disable()))
            // 关闭csrf
            .csrf(AbstractHttpConfigurer::disable)
            // 配置登录地址
            .formLogin(form -> form.loginPage(customConfig.getLoginPage())
                    .loginProcessingUrl(customConfig.getLoginUrl())
                    .defaultSuccessUrl(customConfig.getLoginSuccess())
                    // 配置登录成功自定义处理类
                    .successHandler(userLoginSuccessHandler)
                    // 配置登录失败自定义处理类
                    .failureHandler(userLoginFailureHandler)
                    .permitAll()
            )
            // 禁用默认登出页
            .logout(logout -> logout.logoutUrl(customConfig.getLogoutUrl())
                    .logoutSuccessHandler(userLogoutSuccessHandler)
                    .permitAll()
            )
            // 配置没有权限自定义处理类
            .exceptionHandling(exception -> exception.accessDeniedHandler(userAuthAccessDeniedHandler))
            // 禁用session
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // 配置拦截信息
            .authorizeHttpRequests(authorization -> authorization
                    // 允许所有的OPTIONS请求
                    .requestMatchers(HttpMethod.OPTIONS, PATTERNS).permitAll()
                    // 放行白名单
                    .requestMatchers(patternsUrls).permitAll()
                    // 根据接口所需权限进行动态鉴权
                    .anyRequest()
                    .authenticated()
            )
            // 配置登录验证逻辑
            .authenticationProvider(userAuthenticationProvider)
            // 注册自定义拦截器
            .addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class)
            // 权限验证
            .addFilterAt(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            // 添加JWT过滤器
            .addFilter(new JwtAuthenticationFilter(authentication, customConfig))
            .build();
  }

  /**
   * 身份验证筛选器
   *
   * @return 返回 身份验证筛选器
   * @throws Exception 异常信息
   */
  private AuthenticationFilter authenticationFilter() throws Exception {
    AuthenticationFilter filter = new AuthenticationFilter();
    filter.setAuthenticationManager(authentication);
    filter.setFilterProcessesUrl(customConfig.getLoginUrl());
    // 处理登录成功
    filter.setAuthenticationSuccessHandler(userLoginSuccessHandler);
    // 处理登录失败
    filter.setAuthenticationFailureHandler(userLoginFailureHandler);
    return filter;
  }
}

接口

安全认证控制器 SecurityCertificateController

该接口类提供了APP扫码登录、票据认证、刷新票据、刷新TOKEN等接口

ini 复制代码
package com.cdkjframework.security.controller;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.exceptions.GlobalException;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.UserAuthenticationService;
import com.cdkjframework.util.encrypts.AesUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.files.images.code.QrCodeUtils;
import com.cdkjframework.util.make.VerifyCodeUtils;
import com.cdkjframework.util.network.http.HttpRequestUtils;
import com.cdkjframework.util.tool.StringUtils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import static com.cdkjframework.constant.BusinessConsts.TICKET_SUFFIX;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.controller
 * @ClassName: SecurityCertificateController
 * @Description: 安全认证接口
 * @Author: xiaLin
 * @Version: 1.0
 */
@RestController
@Tag(name = "安全认证接口")
@RequiredArgsConstructor
@RequestMapping(value = "/security")
public class SecurityCertificateController {

  /**
   * 二维生成
   */
  private final QrCodeUtils qrCodeUtils;

  /**
   * 配置文件
   */
  private final CustomConfig customConfig;

  /**
   * 用户权限服务
   */
  private final UserAuthenticationService userAuthenticationServiceImpl;

  /**
   * 获取验证码
   *
   * @param request  请求
   * @param response 响应
   * @throws IOException IO异常信息
   */
  @GetMapping(value = "/verify/code")
  @Operation(summary = "获取验证码")
  public void verificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
    OutputStream outputStream = response.getOutputStream();
    response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE);

    // 创建 session
    HttpSession session = request.getSession();
    // 生成验证码
    String code = VerifyCodeUtils.outputVerifyImage(IntegerConsts.ONE_HUNDRED, IntegerConsts.THIRTY_FIVE,
        outputStream, IntegerConsts.FOUR);
    // 将图形验证码存入到session中
    session.setAttribute(BusinessConsts.IMAGE_CODE, code);
    session.setAttribute(BusinessConsts.TIME, System.currentTimeMillis());
  }

  /**
   * 获取扫码二维码
   *
   * @param request  请求
   * @param response 响应
   * @throws IOException IO异常信息
   */
  @Operation(summary = "获取验证码")
  @GetMapping(value = "/scan/qrcode.html")
  public void scanCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
    OutputStream outputStream = response.getOutputStream();
    response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE);

    // 创建 session
    HttpSession session = request.getSession();
    String id = session.getId().toLowerCase();
    long currentTime = System.currentTimeMillis();
    StringBuilder content = new StringBuilder(id);
    content.append(StringUtils.COMMA);
    content.append(currentTime);
    // 生成二维码
    if (StringUtils.isNotNullAndEmpty(customConfig.getQrlogo())) {
      // 添加LOGO
      InputStream stream = HttpRequestUtils.readImages(customConfig.getQrlogo());
      qrCodeUtils.createQrCode(content.toString(), stream, outputStream);
    } else {
      qrCodeUtils.createQrCode(content.toString(), outputStream);
    }
    String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
    String timeKey = CacheConsts.USER_PREFIX + BusinessConsts.TIME;
    // 将图形验证码存入到session中
    RedisUtils.hSet(timeKey, id, String.valueOf(currentTime));
    RedisUtils.hSet(statusKey, id, StringUtils.ZERO);
  }

  /**
   * 验证二维码是否已被扫码
   *
   * @param request  请求
   * @param response 响应
   * @throws IOException IO异常信息
   */
  @ResponseBody
  @Operation(summary = "验证二维码是否已被扫码")
  @GetMapping(value = "/validate.html")
  public ResponseBuilder validate(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 创建 session
    HttpSession session = request.getSession();
    String id = session.getId().toLowerCase();
    String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
    String timeKey = CacheConsts.USER_PREFIX + BusinessConsts.TIME;

    // 读取状态
    int status = ConvertUtils.convertInt(RedisUtils.hGet(statusKey, id));
    long time = ConvertUtils.convertLong(RedisUtils.hGet(timeKey, id));

    // 返回结果
    String message;
    switch (status) {
      case 1 -> {
        long currentTime = System.currentTimeMillis();
        long value = ((currentTime - time) / IntegerConsts.ONE_THOUSAND) / IntegerConsts.SIXTY;
        message = "success";
        if (value > IntegerConsts.FIVE) {
          message = "timeout";
        }
      }
      case 2 -> {
        String tokenKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + id;
        String token = RedisUtils.syncGet(tokenKey);
        if (StringUtils.isNotNullAndEmpty(token)) {
          message = "successful";
          Claims claims = JwtUtils.parseJwt(token, customConfig.getJwtKey());
          if (claims == null) {
            message = "timeout";
          } else {          // 生成 ticket 票据
            token = ConvertUtils.convertString(claims.get(BusinessConsts.HEADER_TOKEN));
            String ticket = URLEncoder.encode(AesUtils.base64Encode(token), StandardCharsets.UTF_8) + TICKET_SUFFIX;
            response.setHeader(BusinessConsts.TICKET, ticket);
            String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN;
            RedisUtils.hSet(ticketKey, token, ticket);
          }
        } else {
          message = "timeout";
        }
      }
      default -> message = StringUtils.Empty;
    }
    // 返回结果
    return ResponseBuilder.successBuilder(message);
  }

  /**
   * 票据认证
   *
   * @throws IOException IO异常信息
   */
  @ResponseBody
  @Operation(summary = "票据认证")
  @GetMapping(value = "/ticket.html")
  public SecurityUserEntity ticket(@RequestParam("ticket") String ticket, HttpServletResponse response) throws Exception {
    return userAuthenticationServiceImpl.ticket(ticket, response);
  }

    /**
     * 票据刷新
     *
     * @param request 响应
     * @return 返回票据信息
     * @throws UnsupportedEncodingException 异常信息
     * @throws GlobalException              异常信息
     */
    @ResponseBody
    @Operation(summary = "票据刷新")
    @GetMapping(value = "/refresh/ticket.html")
    public ResponseBuilder refreshTicket(HttpServletRequest request) throws UnsupportedEncodingException, GlobalException {
        String ticket = userAuthenticationServiceImpl.refreshTicket(request);
        // 返回结果
        return ResponseBuilder.successBuilder(ticket);
    }

    /**
     * token 刷新
     *
     * @param request  请求
     * @param response 响应
     * @return 返回票据信息
     * @throws UnsupportedEncodingException 异常信息
     * @throws GlobalException              异常信息
     */
    @ResponseBody
    @Operation(summary = "token 刷新")
    @GetMapping(value = "/refresh/token.html")
    public ResponseBuilder refreshToken(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, GlobalException {
        String ticket = userAuthenticationServiceImpl.refreshToken(request, response);
        // 返回结果
        return ResponseBuilder.successBuilder(ticket);
    }

    /**
     * 扫码确认接口
     */
    @ResponseBody
    @Operation(summary = "验证二维码是否已被扫码")
    @GetMapping(value = "/confirm.html")
    public void confirm(@RequestParam("id") String id) {
        String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
        RedisUtils.hSet(statusKey, id, String.valueOf(IntegerConsts.ONE));
  }

  /**
   * 扫码完成接口
   *
   * @param username 用户名
   * @param id       ID
   * @throws IOException IO异常信息
   */
  @ResponseBody
  @Operation(summary = "扫码完成接口【即登录】")
  @PostMapping(value = "/completed.html")
  public void completed(@RequestParam("username") String username, @RequestParam("id") String id) throws Exception {

    String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
    Integer status = ConvertUtils.convertInt(RedisUtils.hGet(statusKey, id));
    if (!status.equals(IntegerConsts.ONE)) {
      throw new GlobalException("二维码已过期,请刷新重试!");
    }
    RedisUtils.syncDel(statusKey);

    // 受权信息
    userAuthenticationServiceImpl.authenticate(username, id);
    RedisUtils.hSet(statusKey, id, String.valueOf(IntegerConsts.TWO));
  }

  /**
   * 用户退出登录
   *
   * @param request 响应
   * @throws GlobalException 异常信息
   */
  @ResponseBody
  @Operation(summary = "扫码完成接口【即登录】")
  @PostMapping(value = "/logout.html")
  public void logout(HttpServletRequest request) throws GlobalException {
    userAuthenticationServiceImpl.logout(request);
  }
}

加密

Jwt身份验证筛选器 JwtAuthenticationFilter

java 复制代码
package com.cdkjframework.security.encrypt;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.log.LogUtils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.CollectionUtils;

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.server.jwt
 * @ClassName: AuthenticationFilter
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    /**
     * 日志
     */
    private LogUtils logUtils = LogUtils.getLogger(JwtAuthenticationFilter.class);

    /**
     * 配置读取
     */
    private CustomConfig customConfig;

    /**
     * 替换字符
     */
    private final String REPLACE = "**";

    /**
     * 构造函数
     *
     * @param authenticationManager 身份验证管理器
     */
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager, CustomConfig customConfig) {
        super(authenticationManager);
        this.customConfig = customConfig;
    }

    /**
     * 权限验证过虑
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        try {
            // 是否验证权限
            if (!verifyToken(request)) {
                chain.doFilter(request, response);
                return;
            }

            // 请求体的头中是否包含Authorization
            String token = request.getHeader(BusinessConsts.HEADER_TOKEN);
            // Authorization中是否包含Bearer,有一个不包含时直接返回
            if (StringUtils.isNullAndSpaceOrEmpty(token)) {
                responseJson(response);
                chain.doFilter(request, response);
                return;
            }
            // 获取权限失败,会抛出异常
            UsernamePasswordAuthenticationToken authentication = getAuthentication(token);
            // 获取后,将Authentication写入SecurityContextHolder中供后序使用
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } catch (Exception e) {
            responseJson(response);
        }
    }

    /**
     * 未登錄時的提示
     *
     * @param response 响应
     */
    private void responseJson(HttpServletResponse response) {
        try {
            // 未登錄時,使用json進行提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            ResponseBuilder builder = ResponseBuilder.failBuilder();
            builder.setCode(HttpServletResponse.SC_FORBIDDEN);
            builder.setMessage("请登录!");
            out.write(JsonUtils.objectToJsonString(builder));
            out.flush();
            out.close();
        } catch (Exception e) {
            logUtils.error(e);
        }
    }

    /**
     * 通过 token,获取用户信息
     *
     * @param token token 值
     * @return 返回用户权限
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String token) {
        if (StringUtils.isNotNullAndEmpty(token)) {
            // 通过 token 解析出用户信息
            Claims claims = JwtUtils.parseJwt(token, customConfig.getJwtKey());
            Object username = claims.get(BusinessConsts.USER_NAME);
            if (StringUtils.isNullAndSpaceOrEmpty(username)) {
                username = claims.get(BusinessConsts.LOGIN_NAME);
            }
            // 不为 null,返回
            if (StringUtils.isNotNullAndEmpty(username)) {
                return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
            }
            return null;
        }
        return null;
    }

    /**
     * 是否需要验证 token
     *
     * @param request 请求
     * @return 结果
     */
    private boolean verifyToken(HttpServletRequest request) {
        // 读取配置
        List<String> patternsUrls = customConfig.getPatternsUrls();
        // 请求地址
        String contextPath = request.getServletPath();
        // 比对结构
        List<String> filterList = patternsUrls.stream()
                .filter(url -> contextPath.startsWith(url.replace(REPLACE, StringUtils.Empty)))
                .collect(Collectors.toList());
        // 返回结果
        return CollectionUtils.isEmpty(filterList);
    }
}

Md5密码编码器 Md5PasswordEncoder

typescript 复制代码
package com.cdkjframework.security.encrypt;

import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.tool.StringUtils;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.encrypt
 * @ClassName: Md5PasswordEncoder
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */
public class Md5PasswordEncoder implements PasswordEncoder {
    /**
     * 对原始密码进行编码
     *
     * @param rawPassword 密码
     */
    @Override
    public String encode(CharSequence rawPassword) {
        return Md5Utils.getMd5(rawPassword.toString());
    }

    /**
     * 比较
     *
     * @param rawPassword     要编码和匹配的原始密码
     * @param encodedPassword 要与之比较的存储器中的编码密码
     * @return 如果原始密码在编码后与存储中的编码密码匹配,则为true
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // 密码是否为空
        if (StringUtils.isNullAndSpaceOrEmpty(encodedPassword)) {
            return false;
        }

        // 对密码加密
        encodedPassword = encode(encodedPassword);

        // 返回比较结果
        return rawPassword.toString().equals(encodedPassword);
    }
}

结束处理

用户身份验证访问被拒绝处理程序 UserAuthAccessDeniedHandler

java 复制代码
package com.cdkjframework.security.handler;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.util.network.ResponseUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @ProjectName: cdkjframework-cloud
 * @Package: com.cdkjframework.cloud.handler
 * @ClassName: UserAuthAccessDeniedHandler
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */

@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {

    /**
     * 无权限返回结果
     *
     * @param request  请求
     * @param response 响应
     * @param e        权限异常
     * @throws IOException      异常信息
     * @throws ServletException 异常信息
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        ResponseBuilder builder = ResponseBuilder.failBuilder("未授权");
        ResponseUtils.out(response, builder);
    }
}

用户身份验证入口点处理程序 UserAuthenticationEntryPointHandler

java 复制代码
package com.cdkjframework.security.handler;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.util.network.ResponseUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @ProjectName: cdkjframework-cloud
 * @Package: com.cdkjframework.cloud.handler
 * @ClassName: UserAuthenticationEntryPointHandler
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {

    /**
     * 用户未登录返回结果
     *
     * @param request  请求
     * @param response 响应
     * @param e        权限异常
     * @throws IOException      异常信息
     * @throws ServletException 异常信息
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseBuilder builder = ResponseBuilder.failBuilder("登录错误,请稍后在试!");
        ResponseUtils.out(response, builder);
    }
}

用户登录失败处理程序 UserLoginFailureHandler

java 复制代码
package com.cdkjframework.security.handler;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.util.log.LogUtils;
import com.cdkjframework.util.network.ResponseUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @ProjectName: cdkjframework-cloud
 * @Package: com.cdkjframework.cloud.handler
 * @ClassName: UserLoginFailureHandler
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {

    /**
     * 日志
     */
    private LogUtils logUtils = LogUtils.getLogger(UserLoginFailureHandler.class);

    /**
     * 登录失败返回结果
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        ResponseBuilder builder;
        // 这些对于操作的处理类可以根据不同异常进行不同处理
        if (exception instanceof UsernameNotFoundException) {
            logUtils.error("【登录失败】" + exception.getMessage());
            builder = ResponseBuilder.failBuilder(exception.getMessage());
        } else if (exception instanceof LockedException) {
            logUtils.error("【登录失败】" + exception.getMessage());
            builder = ResponseBuilder.failBuilder(exception.getMessage());
        } else if (exception instanceof BadCredentialsException) {
            logUtils.error("【登录失败】" + exception.getMessage());
            builder = ResponseBuilder.failBuilder(exception.getMessage());
        } else {
            builder = ResponseBuilder.failBuilder("用户名或密码不正确");
        }
        ResponseUtils.out(response, builder);
    }
}

用户登录成功处理程序 UserLoginSuccessHandler

ini 复制代码
package com.cdkjframework.security.handler;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.BmsConfigureEntity;
import com.cdkjframework.entity.user.ResourceEntity;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.WorkflowEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.ConfigureService;
import com.cdkjframework.security.service.ResourceService;
import com.cdkjframework.security.service.WorkflowService;
import com.cdkjframework.util.encrypts.AesUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.network.ResponseUtils;
import com.cdkjframework.util.tool.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.cdkjframework.constant.BusinessConsts.TICKET_SUFFIX;

/**
 * @ProjectName: cdkjframework-cloud
 * @Package: com.cdkjframework.cloud.handler
 * @ClassName: UserLoginSuccessHandler
 * @Description: 用户登录成功
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
@RequiredArgsConstructor
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {

  /**
   * 有效时间
   */
  private final long EFFECTIVE = IntegerConsts.TWENTY_FOUR * IntegerConsts.SIXTY * IntegerConsts.SIXTY;

  /**
   * 自定义配置
   */
  private final CustomConfig customConfig;

  /**
   * 配置服务
   */
  private final ConfigureService configureServiceImpl;

  /**
   * 资源服务
   */
  private final ResourceService resourceServiceImpl;

  /**
   * 工作流服务
   */
  private final WorkflowService workflowServiceImpl;

  /**
   * 权限认证成功
   *
   * @param request        请求
   * @param response       响应
   * @param authentication 权限
   * @throws IOException      异常信息
   * @throws ServletException 异常信息
   */
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    SecurityUserEntity user = (SecurityUserEntity) authentication.getPrincipal();
    ResponseBuilder builder = ResponseBuilder.successBuilder();
    builder.setData(user);
    // 构建 token
    buildJwtToken(request, response, user);

    // 获取配置信息
    BmsConfigureEntity configure = new BmsConfigureEntity();
    configure.setOrganizationId(user.getOrganizationId());
    configure.setTopOrganizationId(user.getTopOrganizationId());
    List<BmsConfigureEntity> configureList = configureServiceImpl.listConfigure(configure);
    user.setConfigureList(configureList);

    // 用户角色
    List<RoleEntity> roleList = user.getRoleList();
    if (!CollectionUtils.isEmpty(roleList)) {
      // 用户资源
      List<ResourceEntity> resourceList = resourceServiceImpl.listResource(roleList, user.getUserId());
      // 用户资源写入缓存
      String key = CacheConsts.USER_RESOURCE + user.getUserId();
      RedisUtils.syncEntitySet(key, resourceList, EFFECTIVE);
      user.setResourceList(resourceList);
    }
    // 查询工作流信息
    WorkflowEntity workflow = new WorkflowEntity();
    workflow.setOrganizationId(user.getOrganizationId());
    workflow.setTopOrganizationId(user.getTopOrganizationId());
    workflowServiceImpl.listWorkflow(workflow);

    // 返回登录结果
    ResponseUtils.out(response, builder);
  }

  /**
   * 生成 jwt token
   *
   * @param user     用户实体
   * @param request  请求
   * @param response 响应
   */
  private void buildJwtToken(HttpServletRequest request, HttpServletResponse response, SecurityUserEntity user) throws UnsupportedEncodingException {
    // 生成 JWT token
    Map<String, Object> map = new HashMap<>(IntegerConsts.FOUR);
    map.put(BusinessConsts.LOGIN_NAME, user.getUsername());
    long time = System.currentTimeMillis() / IntegerConsts.ONE_THOUSAND;
    map.put(BusinessConsts.TIME, time);
    map.put(BusinessConsts.USER_NAME, user.getUsername());
    map.put(BusinessConsts.USER_TYPE, user.getUserType());
    map.put(BusinessConsts.DISPLAY_NAME, user.getDisplayName());
    // 暂不需要该参数
    String userAgent = StringUtils.Empty;
    StringBuilder builder = new StringBuilder();
    /**
     * 加密 token 参数
     */
    String TOKEN_ENCRYPTION = "loginName=%s&effective=%s&time=%s&userAgent=%s";
    builder.append(String.format(TOKEN_ENCRYPTION,
        user.getUsername(), EFFECTIVE, time, userAgent));
    String token = Md5Utils.getMd5(builder.toString());
    map.put(BusinessConsts.HEADER_TOKEN, token);
    String jwtToken = JwtUtils.createJwt(map, customConfig.getJwtKey());
    response.setHeader(BusinessConsts.HEADER_TOKEN, jwtToken);
    // 票据添加到响应中
    String ticket = URLEncoder.encode(AesUtils.base64Encode(token), StandardCharsets.UTF_8.toString()) + TICKET_SUFFIX;
    response.setHeader(BusinessConsts.TICKET, ticket);

    // 票据 token 关系
    String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + token;
    RedisUtils.syncSet(ticketKey, jwtToken, IntegerConsts.SIXTY);


    // 用户信息写入缓存
    String key = CacheConsts.USER_LOGIN + token;
    RedisUtils.syncEntitySet(key, user, EFFECTIVE);
  }
}

用户注销成功处理程序 UserLogoutSuccessHandler

ini 复制代码
package com.cdkjframework.security.handler;

import com.alibaba.fastjson.JSONObject;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.network.ResponseUtils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * @ProjectName: cdkjframework-cloud
 * @Package: com.cdkjframework.cloud.handler
 * @ClassName: LogoutSuccessHandler
 * @Description: 退出登录成功
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {

  /**
   * 受权
   */
  private static final String TOKEN = "token";

  /**
   * 用户登出返回结果
   * 这里应该让前端清除掉Token
   */
  @Override
  public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    String token = request.getHeader(TOKEN);
    Claims claims = JwtUtils.parseJwt(token, new CustomConfig().getJwtKey());
    if (claims != null) {
      String tokenKey = ConvertUtils.convertString(claims.get(TOKEN));
      final String userKey = CacheConsts.USER_LOGIN + tokenKey;
      // 读取用户
      String userInfo = RedisUtils.syncGet(userKey);
      if (StringUtils.isNotNullAndEmpty(userInfo)) {
        JSONObject object = JsonUtils.parseObject(userInfo);
        /**
         * 用户ID
         */
        String ID = "id";
        String userId = object.getString(ID);
        final String resourceKey = CacheConsts.USER_RESOURCE + userId;
        RedisUtils.syncDel(resourceKey);
      }
      RedisUtils.syncDel(userKey);
    }
    ResponseBuilder builder = ResponseBuilder.successBuilder();
    SecurityContextHolder.clearContext();
    ResponseUtils.out(response, builder);
  }
}

服务或接口

抽象用户详细信息服务 AbstractUserDetailsService

该抽象类需要应用服务端实现

java 复制代码
package com.cdkjframework.security.service.impl;

import com.cdkjframework.entity.user.security.SecurityUserEntity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.service
 * @ClassName: AbstractUserDetailsService
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
public abstract class AbstractUserDetailsService implements UserDetailsService {

    /**
     * 查询用户信息
     *
     * @param username 用户名
     * @return 返回用户信息
     * @throws UsernameNotFoundException 用户未找到异常信息
     */
    @Override
    public abstract SecurityUserEntity loadUserByUsername(String username) throws UsernameNotFoundException;
}

用户身份验证服务

接口 UserAuthenticationService

java 复制代码
package com.cdkjframework.security.service;

import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.exceptions.GlobalException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.service
 * @ClassName: UserAuthenticationService
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Date: 2023/5/16 22:41
 * @Version: 1.0
 */
public interface UserAuthenticationService {

    /**
     * 获取登录参数
     */
    String GRANT_TYPE = "grantType";

    /**
     * 有效时间
     */
    long EFFECTIVE = IntegerConsts.TWENTY_FOUR * IntegerConsts.SIXTY * IntegerConsts.SIXTY;

    /**
     * 授权常量
     */
    String AUTHORIZATION = "token";

    /**
     * 身份权限验证
     *
     * @param userName  用户名
     * @param sessionId 会话id
     * @return 返回权限
     * @throws AuthenticationException 权限异常
     * @throws ServletException        权限异常
     * @throws IOException             权限异常
     */
    Authentication authenticate(String userName, String sessionId) throws AuthenticationException, IOException, ServletException;

    /**
     * 票据认证
   *
   * @param ticket   票据
   * @param response 响应
   * @return 返回用户信息
   * @throws Exception IO异常信息
   */
  SecurityUserEntity ticket(String ticket, HttpServletResponse response) throws Exception;

    /**
     * 刷新 token
     *
     * @param request 响应
     * @return 返回票据
     * @throws GlobalException              异常信息
     * @throws UnsupportedEncodingException 异常信息
     */
    String refreshTicket(HttpServletRequest request) throws GlobalException, UnsupportedEncodingException;

    /**
     * token 刷新
     *
     * @param request  请求
     * @param response 响应
     * @return 返回最新 token
     * @throws GlobalException              异常信息
     * @throws UnsupportedEncodingException 异常信息
     */
    String refreshToken(HttpServletRequest request, HttpServletResponse response) throws GlobalException, UnsupportedEncodingException;

    /**
     * 用户退出登录
     *
     * @param request 响应
     * @throws GlobalException 异常信息
     */
    void logout(HttpServletRequest request) throws GlobalException;
}

实现 UserAuthenticationServiceImpl

ini 复制代码
package com.cdkjframework.security.service.impl;

import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.BmsConfigureEntity;
import com.cdkjframework.entity.user.ResourceEntity;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.UserEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.exceptions.GlobalException;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.*;
import com.cdkjframework.util.encrypts.AesUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static com.cdkjframework.constant.BusinessConsts.TICKET_SUFFIX;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.authorization
 * @ClassName: UserAuthenticationProvider
 * @Description: 自定义登录验证
 * @Author: xiaLin
 * @Version: 1.0
 */
@Component
@RequiredArgsConstructor
public class UserAuthenticationServiceImpl implements UserAuthenticationService {

  /**
   * 用户登录成功服务
   */
  private final UserLoginSuccessService userLoginSuccessServiceImpl;

  /**
   * 用户信息查询服务
   */
  private final UserDetailsService userDetailsService;

  /**
   * 用户角色服务
   */
  private final UserRoleService userRoleService;

  /**
   * 配置信息
   */
  private final CustomConfig customConfig;

  /**
   * 资源服务
   */
  private final ResourceService resourceServiceImpl;

  /**
   * 配置服务
   */
  private final ConfigureService configureServiceImpl;

  /**
   * 身份权限验证
   *
   * @param userName  用户名
   * @param sessionId 会话id
   * @return 返回权限
   * @throws AuthenticationException 权限异常
   */
  @Override
  public Authentication authenticate(String userName, String sessionId) throws AuthenticationException, IOException, ServletException {
    // 查询用户是否存在
    Object userDetails = userDetailsService.loadUserByUsername(userName);
    if (userDetails == null) {
      throw new UsernameNotFoundException("用户名不存在");
    }
    SecurityUserEntity userInfo = (SecurityUserEntity) userDetails;
    // 还可以加一些其他信息的判断,比如用户账号已停用等判断
    if (userInfo.getStatus().equals(IntegerConsts.ZERO) ||
            userInfo.getDeleted().equals(IntegerConsts.ONE)) {
      throw new LockedException("该用户已被冻结");
    }
    // 角色集合
    Set<GrantedAuthority> authorities = new HashSet<>();
    // 查询用户角色
    List<RoleEntity> sysRoleEntityList = userRoleService.listRoleByUserId(userInfo.getUserId());
    for (RoleEntity sysRoleEntity : sysRoleEntityList) {
      authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));
    }
    userInfo.setAuthorities(authorities);
    // 进行登录
    Authentication authentication = new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(), authorities);

    // 登录成功后操作
    userLoginSuccessServiceImpl.onAuthenticationSuccess(sessionId, authentication);

    // 返回结果
    return authentication;
  }

  /**
   * 票据认证
   *
   * @param ticket   票据
   * @param response 响应
   * @throws IOException IO异常信息
   */
  @Override
  public SecurityUserEntity ticket(String ticket, HttpServletResponse response) throws Exception {
    if (StringUtils.isNullAndSpaceOrEmpty(ticket)) {
      throw new GlobalException("票据错误!");
    }
    ticket = URLDecoder.decode(ticket, StandardCharsets.UTF_8.toString());
    String token = AesUtils.base64Decrypt(ticket
            .replace(BusinessConsts.TICKET_SUFFIX, StringUtils.Empty));
    // 读取用户信息
    String key = CacheConsts.USER_LOGIN + token;
    SecurityUserEntity user = RedisUtils.syncGetEntity(key, SecurityUserEntity.class);
    if (user == null) {
      throw new GlobalException("用户信息错误!");
    }

    // 票据 token 关系
    String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + token,
            jwtToken = RedisUtils.syncGet(ticketKey),
            // 资源 key
            resourceKey;
    RedisUtils.syncDel(ticketKey);
    user.setToken(jwtToken);
    response.setHeader(BusinessConsts.HEADER_TOKEN, jwtToken);

    // 读取当前用户所登录平台资源数据
    List<ResourceEntity> resourceList = resourceServiceImpl.listResource(new ArrayList<>(), user.getUserId());
    user.setResourceList(resourceList);

    // 读取配置信息
    BmsConfigureEntity configure = new BmsConfigureEntity();
    configure.setOrganizationId(user.getCurrentOrganizationId());
    configure.setTopOrganizationId(user.getTopOrganizationId());
    configure.setDeleted(IntegerConsts.ZERO);
    configure.setStatus(IntegerConsts.ONE);
    List<BmsConfigureEntity> configureList = configureServiceImpl.listConfigure(configure);
    user.setConfigureList(configureList);

    // 删除数据
    RedisUtils.syncDel(ticketKey);
    user.setPassword(StringUtils.Empty);
    // 返回结果
    return user;
  }

  /**
   * 刷新票据
   *
   * @param request 响应
   * @return 返回票据
   */
  @Override
  public String refreshTicket(HttpServletRequest request) throws GlobalException, UnsupportedEncodingException {
        String jwtToken = request.getHeader(AUTHORIZATION);
        // 验证TOKEN有效性
        String tokenValue = JwtUtils.checkToken(jwtToken, customConfig.getJwtKey(), StringUtils.Empty);

        // 票据 token 关系
        String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + tokenValue;
        // 存储票据信息
        RedisUtils.syncSet(ticketKey, jwtToken, IntegerConsts.SIXTY);

        // 返回票据
        return URLEncoder.encode(AesUtils.base64Encode(tokenValue), StandardCharsets.UTF_8.toString()) + TICKET_SUFFIX;
    }

    /**
     * token 刷新
     *
     * @param request  请求
     * @param response 响应
     * @return 返回最新 token
     * @throws GlobalException              异常信息
     * @throws UnsupportedEncodingException 异常信息
     */
    @Override
    public String refreshToken(HttpServletRequest request, HttpServletResponse response) throws GlobalException, UnsupportedEncodingException {
        String jwtToken = request.getHeader(AUTHORIZATION);
        // 验证 TOKEN 有效性
        String tokenValue = JwtUtils.checkToken(jwtToken, customConfig.getJwtKey(), StringUtils.Empty);
        if (StringUtils.isNotNullAndEmpty(tokenValue)) {
            // 用户信息
            String key = CacheConsts.USER_LOGIN + tokenValue;
            SecurityUserEntity user = RedisUtils.syncGetEntity(key, SecurityUserEntity.class);
            buildJwtToken(user, response);
        }
        return null;
    }

    /**
     * 用户退出登录
     *
     * @param request 响应
     * @throws GlobalException 异常信息
     */
    @Override
    public void logout(HttpServletRequest request) throws GlobalException {
        String jwtToken = request.getHeader(AUTHORIZATION);
        // 验证TOKEN有效性
        String tokenValue = JwtUtils.checkToken(jwtToken, customConfig.getJwtKey(), StringUtils.Empty);
        // 先读取用户信息
        String key = CacheConsts.USER_LOGIN + tokenValue;
        String jsonCache = RedisUtils.syncGet(key);
        if (StringUtils.isNullAndSpaceOrEmpty(jsonCache)) {
            return;
        }
        UserEntity user = JsonUtils.jsonStringToBean(jsonCache, UserEntity.class);
        // 删除 用户信息
        RedisUtils.syncDel(key);
        // 删除 资源
        key = CacheConsts.USER_RESOURCE + tokenValue;
        RedisUtils.syncDel(key);
        // 删除 用户全部资源
        key = CacheConsts.USER_RESOURCE_ALL + user.getId();
        RedisUtils.syncDel(key);
        // 删除 用户工作流引擎
        key = CacheConsts.WORK_FLOW + user.getId();
        RedisUtils.syncDel(key);
    }

    /**
     * 生成 jwt token
     *
     * @param user     用户实体
     * @param response 响应
     */
    private void buildJwtToken(SecurityUserEntity user, HttpServletResponse response) throws UnsupportedEncodingException {
        // 生成 JWT token
        Map<String, Object> map = new HashMap<>(IntegerConsts.FOUR);
        map.put(BusinessConsts.LOGIN_NAME, user.getUsername());
        long time = System.currentTimeMillis() / IntegerConsts.ONE_THOUSAND;
        map.put(BusinessConsts.TIME, time);
        map.put(BusinessConsts.USER_NAME, user.getUsername());
        map.put(BusinessConsts.USER_TYPE, user.getUserType());
        map.put(BusinessConsts.DISPLAY_NAME, user.getDisplayName());
        // 暂不需要该参数
        String userAgent = StringUtils.Empty;
        StringBuilder builder = new StringBuilder();
        // 加密 token 参数
        String TOKEN_ENCRYPTION = "loginName=%s&effective=%s&time=%s&userAgent=%s";
        builder.append(String.format(TOKEN_ENCRYPTION,
                user.getUsername(), EFFECTIVE, time, userAgent));
        String token = Md5Utils.getMd5(builder.toString());
        map.put(BusinessConsts.HEADER_TOKEN, token);
        String jwtToken = JwtUtils.createJwt(map, customConfig.getJwtKey());
        response.setHeader(BusinessConsts.HEADER_TOKEN, jwtToken);

        // 用户信息写入缓存
        String key = CacheConsts.USER_LOGIN + token;
        RedisUtils.syncEntitySet(key, user, EFFECTIVE);
    }
}

用户登录成功服务

接口 UserLoginSuccessService

java 复制代码
package com.cdkjframework.security.service;

import org.springframework.security.core.Authentication;

import jakarta.servlet.ServletException;
import java.io.IOException;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.service
 * @ClassName: UserLoginSuccessService
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Date: 2023/5/16 22:52
 * @Version: 1.0
 */
public interface UserLoginSuccessService {

  /**
   * 权限认证成功
   *
   * @param sessionId      会话id
   * @param authentication 权限
   * @throws IOException      异常信息
   * @throws ServletException 异常信息
   */
  void onAuthenticationSuccess(String sessionId, Authentication authentication) throws IOException, ServletException;
}

实现 UserLoginSuccessServiceImpl

ini 复制代码
package com.cdkjframework.security.service.impl;

import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.BmsConfigureEntity;
import com.cdkjframework.entity.user.ResourceEntity;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.WorkflowEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.*;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.tool.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import jakarta.servlet.ServletException;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @ProjectName: cdkj-framework
 * @Package: com.cdkjframework.security.service.impl
 * @ClassName: UserLoginSuccessServiceImpl
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Date: 2023/5/16 22:52
 * @Version: 1.0
 */
@Component
@RequiredArgsConstructor
public class UserLoginSuccessServiceImpl implements UserLoginSuccessService {

  /**
   * 自定义配置
   */
  private final CustomConfig customConfig;

  /**
   * 配置服务
   */
  private final ConfigureService configureServiceImpl;

  /**
   * 资源服务
   */
  private final ResourceService resourceServiceImpl;

  /**
   * 用户角色
   */
  private final UserRoleService userRoleServiceImpl;

  /**
   * 工作流服务
   */
  private final WorkflowService workflowServiceImpl;

  /**
   * 有效时间
   */
  private final long EFFECTIVE = IntegerConsts.TWENTY_FOUR * IntegerConsts.SIXTY * IntegerConsts.SIXTY;

  /**
   * 权限认证成功
   *
   * @param sessionId      会话id
   * @param authentication 权限
   * @throws IOException      异常信息
   * @throws ServletException 异常信息
   */
  @Override
  public void onAuthenticationSuccess(String sessionId, Authentication authentication) throws IOException, ServletException {
    SecurityUserEntity user = (SecurityUserEntity) authentication.getPrincipal();
    // 构建 token
    buildJwtToken(user, sessionId);

    // 获取配置信息
    BmsConfigureEntity configure = new BmsConfigureEntity();
    configure.setOrganizationId(user.getOrganizationId());
    configure.setTopOrganizationId(user.getTopOrganizationId());
    List<BmsConfigureEntity> configureList = configureServiceImpl.listConfigure(configure);
    user.setConfigureList(configureList);

    // 用户角色
    List<RoleEntity> roleList = userRoleServiceImpl.listRoleByUserId(user.getUserId());
    if (!CollectionUtils.isEmpty(roleList)) {
      // 用户资源
      List<ResourceEntity> resourceList = resourceServiceImpl.listResource(roleList, user.getUserId());
      // 用户资源写入缓存
      String key = CacheConsts.USER_RESOURCE + user.getUserId();
      RedisUtils.syncEntitySet(key, resourceList, EFFECTIVE);
      user.setResourceList(resourceList);
    }
    // 查询工作流信息
    WorkflowEntity workflow = new WorkflowEntity();
    workflow.setOrganizationId(user.getOrganizationId());
    workflow.setTopOrganizationId(user.getTopOrganizationId());
    workflowServiceImpl.listWorkflow(workflow);
  }

  /**
   * 生成 jwt token
   *
   * @param user      用户实体
   * @param sessionId 会话id
   */
  private void buildJwtToken(SecurityUserEntity user, String sessionId) {
    // 生成 JWT token
    Map<String, Object> map = new HashMap<>(IntegerConsts.FOUR);
    map.put(BusinessConsts.LOGIN_NAME, user.getUsername());
    long time = System.currentTimeMillis() / IntegerConsts.ONE_THOUSAND;
    map.put(BusinessConsts.TIME, time);
    map.put(BusinessConsts.USER_NAME, user.getUsername());
    map.put(BusinessConsts.USER_TYPE, user.getUserType());
    map.put(BusinessConsts.DISPLAY_NAME, user.getDisplayName());
    map.put(BusinessConsts.TIME, time);
    // 暂不需要该参数
    String userAgent = StringUtils.Empty;
    StringBuilder builder = new StringBuilder();
    builder.append(String.format("loginName=%s&effective=%s&time=%s&userAgent=%s",
        user.getUsername(), EFFECTIVE, time, userAgent));
    String token = Md5Utils.getMd5(builder.toString());
    map.put(BusinessConsts.HEADER_TOKEN, token);
    String jwtToken = JwtUtils.createJwt(map, customConfig.getJwtKey());
    user.setToken(jwtToken);
    String tokenKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + sessionId;
    RedisUtils.syncSet(tokenKey, jwtToken, IntegerConsts.SIXTY);

    // 票据 token 关系
    String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + token;
    RedisUtils.syncSet(ticketKey, jwtToken, IntegerConsts.SIXTY);

    // 用户信息写入缓存
    String key = CacheConsts.USER_LOGIN + token;
    RedisUtils.syncEntitySet(key, user, EFFECTIVE);
  }
}

其中 ConfigureService、ResourceService、UserRoleService、WorkflowService服务接口需根据自己的业务进行相应的调整也可以直接使用项目,在实际服务中直接继承即可。

总结

以上只是博主自己在实际项目中总结出来,然后在将其实封成工具分享给大家。

相关源码在:维基框架

Gitee: gitee.com/cdkjframewo...

Github:github.com/cdkjframewo...

如果喜欢博主的分享记得给博主点点小星星

相关推荐
皮皮林5514 小时前
SpringBoot 加载外部 Jar,实现功能按需扩展!
java·spring boot
rocksun4 小时前
认识Embabel:一个使用Java构建AI Agent的框架
java·人工智能
高松燈4 小时前
若伊项目学习 后端分页源码分析
后端·架构
Java中文社群5 小时前
AI实战:一键生成数字人视频!
java·人工智能·后端
王中阳Go6 小时前
从超市收银到航空调度:贪心算法如何破解生活中的最优决策谜题?
java·后端·算法
shepherd1116 小时前
谈谈TransmittableThreadLocal实现原理和在日志收集记录系统上下文实战应用
java·后端·开源
水泥工boss7 小时前
🚀微前端与模块联邦的深度结合(基于vue+vite)
前端·架构
日月星辰Ace7 小时前
Java JVM 垃圾回收器(四):现代垃圾回收器 之 Shenandoah GC
java·jvm
天天摸鱼的java工程师7 小时前
商品详情页 QPS 达 10 万,如何设计缓存架构降低数据库压力?
java·后端·面试