一、Spring Security介绍

1、工作原理与核心组件

Spring Security 的核心是基于过滤器链(Filter Chain)​

来拦截和处理 HTTP 请求,每个过滤器负责特定的安全任务,例如身份验证、授权、异常处理等。

其核心组件主要包括:

  • SecurityContextHolder​:存储当前用户的安全上下文(Security Context),包含认证和权限信息。
  • Authentication​:接口,表示用户的身份认证信息(如 Principal、Credentials、Authorities)。
  • UserDetails 与 UserDetailsService​:
    • UserDetails 接口定义了核心用户信息(用户名、密码、权限等)。
    • UserDetailsService 接口用于根据用户名加载用户信息,是认证过程的关键。
  • AuthenticationManager & AuthenticationProvider​:负责认证过程的调度(Manager)和具体的认证逻辑实现(Provider)。
  • PasswordEncoder​:负责密码的加密和匹配,确保密码安全存储。
  • AccessDecisionManager​:负责在授权过程中做最终的访问决策。

2、集成与配置

在 Spring Boot 项目中集成 Spring Security 非常简单:

  1. 添加依赖​:引入 spring-boot-starter-security 依赖。
  2. 基础配置​:Spring Boot 会自动配置一个默认的安全策略,包括:
    • 所有端点都需要认证。
    • 提供一个默认的登录页(/login)和注销页(/logout)。
    • 为每个用户生成一个随机密码(在控制台输出)。
  3. 自定义安全配置​:通常通过配置 SecurityFilterChain Bean 来自定义安全规则,如指定放行路径、自定义登录页、设置权限等

二、业务场景

        系统同时要求具备短信验证码及账号密码登录,同时需要注意密码必须需要加密,不能使用明码传输。

三、具体实现

1、依赖引入

<!--   springSecurity 依赖   -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--引入jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.11.0</version>
        </dependency>

2、自定义安全配置

        配置核心作用分析,该配置类主要通过自定义和组合多项安全组件,实现了以下核心功能:

功能模块 实现方式 核心作用
认证方式 注入 JwtAuthenticationFilterSmsAuthenticationProvider 支持 ​JWT令牌​ 和 ​短信​ 两种认证方式
授权控制 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启方法级权限注解(如 @PreAuthorize
会话管理 SessionCreationPolicy.STATELESS 设置为无状态,适用于 RESTful API
密码编码 返回自定义 SM3PasswordEncode 使用国密 SM3 算法进行密码加密与验证
异常处理 注入 AccessDeniedHandlerImplAuthenticationEntryPointImpl 自定义认证失败和权限不足的异常响应
请求规则 配置 HttpSecurity,放行指定 URL 保护绝大多数接口,仅允许匿名访问登录等特定端点
import com.b2bwings.nry.app.common.constant.SysConstant;
import com.b2bwings.nry.app.common.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.Arrays;


/**
 * Spring Security 核心安全配置类
 * 实现基于JWT的无状态认证,并支持短信登录等多种认证方式
 *
 * @author
 */
@Configuration
//@EnableWebSecurity //因为我引入了spring-boot-starter-security,所以不用@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 自定义JWT认证过滤器,用于解析和验证Token
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    // 自定义授权失败处理类(如权限不足403)
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    // 自定义认证失败处理类(如未登录401)
    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;

    // 自定义用户详情服务,用于从数据库加载用户信息
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

     // 自定义短信认证提供器
    @Autowired
    private SmsAuthenticationProvider smsAuthenticationProvider;


    /​**​
     * 配置密码编码器Bean
     * 使用国密SM3算法进行密码哈希加密与验证,替代默认的BCrypt等算法
     * 此编码器将在DaoAuthenticationProvider中被使用
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new SM3PasswordEncode();
    }


    /​**​
     * 配置认证管理器构造器
     * 用于注册自定义的AuthenticationProvider
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 初始化默认配置
        super.configure(auth);

        //显式注册自定义的短信认证提供器,使其生效
        auth.authenticationProvider(smsAuthenticationProvider);
    }

    /​**​
     * 配置DAO认证提供器Bean
     * 用于处理传统的用户名密码认证方式
     * 设置了自定义的UserDetailsService和PasswordEncoder
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }


    /​**​
     * 暴露AuthenticationManager Bean
     * 手动组合多个认证提供器(短信+账号密码),并保留父管理器的其他自动配置提供器
     * 这样可以在其他地方(如Controller)注入并使用AuthenticationManager进行认证
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // 组合手动和自动配置
        return new ProviderManager(
                Arrays.asList(
                        smsAuthenticationProvider,
                        daoAuthenticationProvider()
                ),
                // 保留父管理器
                super.authenticationManagerBean()
        );
    }


    /​**​
     * 核心配置:配置HTTP安全规则
     * 定义URL访问权限、过滤器、异常处理等
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable() // 关闭csrf验证(防止跨站请求伪造攻击)由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问
                // 不通过session 获取SecurityContext(基于Token不需要session)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //开启权限拦截
                .authorizeRequests()
                //允许登录接口匿名访问
                .antMatchers(SysConstant.UN_AUTHORIZED_URL).permitAll()
                .antMatchers("/**/open/**").permitAll()
                // 其他请求都需要认证
                .anyRequest().authenticated();

        //将jwtAuthenticationTokenFilter过滤器注入到UsernamePasswordAuthenticationFilter过滤器之前
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        // 认证授权异常自定义处理
        http.exceptionHandling()
                //自定义认证失败异常处理类
                .authenticationEntryPoint(authenticationEntryPoint)
                //自定义授权失败异常处理类
                .accessDeniedHandler(accessDeniedHandler);


        // 禁用缓存
        http.headers().cacheControl();

        // 跨域请求配置
        http.cors();
    }

}

3、相关类实现

3.1、自定义授权失败异常处理类

import com.alibaba.fastjson.JSON;
import com.b2bwings.nry.app.common.util.WebUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

//自定义授权失败异常处理类
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {


        WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(accessDeniedException)));

    }
}

3.2、自定义认证失败异常处理类

import com.alibaba.fastjson.JSON;
import com.b2bwings.nry.app.common.util.WebUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

//自定义认证失败异常处理类
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException authenticationException) throws IOException, ServletException {

        WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(authenticationException)));
    }
}

3.3、认证异常工具类

import com.b2bwings.nry.common.api.ApiCode;
import com.b2bwings.nry.common.api.ApiResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.AuthorizationServiceException;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.csrf.CsrfException;

//认证异常工具类
public class AuthExceptionUtil {

    public static ApiResult getErrMsgByExceptionType(AuthenticationException e) {

        if (e instanceof InsufficientAuthenticationException) {

            return ApiResult.fail(ApiCode.SECURITY_40000);//需要登录

        }else if (e instanceof LockedException) {

            return ApiResult.fail(ApiCode.SECURITY_40001);//账户被锁定,请联系管理员!

        } else if (e instanceof CredentialsExpiredException) {

            return ApiResult.fail(ApiCode.SECURITY_40002);//用户名或者密码输入错误!

        }else if (e instanceof AccountExpiredException) {

            return ApiResult.fail(ApiCode.SECURITY_40003);//账户过期,请联系管理员!

        } else if (e instanceof DisabledException) {

            return ApiResult.fail(ApiCode.SECURITY_40004);//"账户被禁用,请联系管理员!

        } else if (e instanceof BadCredentialsException) {

            return ApiResult.fail(ApiCode.SECURITY_40005);//用户名或者密码输入错误!

        }else if (e instanceof AuthenticationServiceException) {

            return ApiResult.fail(ApiCode.SECURITY_40006);//认证失败,请重试!
        }

        return ApiResult.fail(ApiCode.SECURITY_40099);//权限认证其他异常!
    }

    public static ApiResult getErrMsgByExceptionType(AccessDeniedException e) {

        if (e instanceof CsrfException) {

            return ApiResult.fail(ApiCode.SECURITY_40007);//非法访问跨域请求异常!

        }  else if (e instanceof AuthorizationServiceException) {

            return ApiResult.fail(ApiCode.SECURITY_40008);//认证服务异常请重试!

        }else if (e instanceof AccessDeniedException) {

            return ApiResult.fail(ApiCode.SECURITY_40009);//权限不足不允许访问!
        }

        return ApiResult.fail(ApiCode.SECURITY_40099);//权限认证其他异常!
    }

}

3.4、JWT认证过滤器

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.b2bwings.nry.app.business.entity.LoanUser;
import com.b2bwings.nry.app.business.mapper.core.LoanUserMapper;
import com.b2bwings.nry.app.business.web.vo.authentication.LoginLoanUser;
import com.b2bwings.nry.app.business.web.vo.authentication.LoginVo;
import com.b2bwings.nry.app.business.web.vo.authentication.TokenVo;
import com.b2bwings.nry.app.common.constant.SysConstant;
import com.b2bwings.nry.app.common.util.JwtUtils;
import com.b2bwings.nry.app.common.util.RedisUtil;
import com.b2bwings.nry.app.common.util.WebUtils;
import com.b2bwings.nry.common.api.ApiCode;
import com.b2bwings.nry.common.api.ApiResult;
import com.b2bwings.nry.common.constant.CommonRedisKey;
import com.b2bwings.nry.common.exception.BusinessException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private LoanUserMapper loanUserMapper;

    //每次请求都会执行这个方法
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws BusinessException, ServletException, IOException {

        String requestURI = request.getRequestURI();
        // 获取Headers上的token,我命名为token
        String token = request.getHeader("token");
        //免认证接口:/v1/login/authentication,放行接口
        List<String> unAuthorizedUrl = Arrays.asList(SysConstant.UN_AUTHORIZED_URL);
        if (StringUtils.isEmpty(token) || unAuthorizedUrl.contains(requestURI) || requestURI.contains("/open/")) {
            // token 放行 并且直接return 返回
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userId;
        try {
            DecodedJWT tokenInfo = JwtUtils.verifyToken(token);
            userId = tokenInfo.getClaim("userId").asString();
            String account = tokenInfo.getClaim("account").asString();
            //token过期时间
            Date expiresAt = tokenInfo.getExpiresAt();
            Date now = new Date();
            long diffInMilliseconds = (expiresAt.getTime() - now.getTime()) / 1000;
            //token快过期的处理(剩余5分钟),然后返回新的token给前端
            if (diffInMilliseconds < SysConstant.EXPIRATION) {
                TokenVo tokenVo = new TokenVo();
                Map<String, String> payloadMap = new HashMap<>();
                payloadMap.put("userId", userId);
                payloadMap.put("account", account);
                String newToken = JwtUtils.generateToken(payloadMap);
                //redis用户信息续期,默认一周
                redisUtil.expire(CommonRedisKey.LOGIN_KEY + userId, SysConstant.AMOUNT);
                tokenVo.setToken(newToken);
                WebUtils.rednerString(response, JSON.toJSONString(ApiResult.fail(ApiCode.SECURITY_40100, tokenVo)));
                return;
            }
            if (!validUser(Long.valueOf(userId))) {
                redisUtil.del(CommonRedisKey.LOGIN_KEY + userId);
                WebUtils.rednerString(response, JSON.toJSONString(ApiResult.fail(ApiCode.SECURITY_40000)));
                return;
            }
        } catch (Exception e) {
            log.info(e.getMessage());
            WebUtils.rednerString(response, JSON.toJSONString(ApiResult.fail(ApiCode.SECURITY_40000)));
            return;
        }

        // 获取userid 从redis中获取用户信息
        String redisKey = CommonRedisKey.APPLET_SESSION_TIMEOUT + userId;
        LoginLoanUser loginUser = (LoginLoanUser) redisUtil.get(redisKey);
        if (Objects.isNull(loginUser)) {
            WebUtils.rednerString(response, JSON.toJSONString(ApiResult.fail(ApiCode.SECURITY_40000)));
            return;
        }

        //将用户信息存入到SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }

    /**
     * 判断用户是否有效
     *
     * @param userId
     */
    private Boolean validUser(Long userId) {
        LoanUser user = loanUserMapper.selectOne(new LambdaQueryWrapper<LoanUser>()
                .eq(LoanUser::getLoanUserId, userId)
                .eq(LoanUser::getUserStatus, 0)
                .eq(LoanUser::getDeleted, 0)
        );
        if (user == null) {
            return false;
        }
        return true;
    }

}

3.5、认证成功返回结果实体

自定义实体,一定要实现UserDetails类,重写方法,后面账号密码校验依照重写方法返回的参数

import com.b2bwings.nry.app.business.entity.LoanUser;
import com.b2bwings.nry.app.business.entity.LoanUserInfo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

/**
 * @author maijunhao
 */
@Data
@Accessors(chain = true)
public class LoginLoanUser implements UserDetails {

    @ApiModelProperty(value = "用户信息")
    private LoanUser user;

    @ApiModelProperty(value = "用户信息")
    private LoanUserInfo userInfo;

    @ApiModelProperty(value = "账户可信级别")
    private String creditableLevelOfAccount;

    @ApiModelProperty(value = "是否实名认证 是=true, 否=false")
    private Boolean auth;

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

    @Override
    public String getPassword() {
        //返回带盐值的密码用于SM3PasswordEncode匹配
        return user.getSalt() + "|" + user.getUserPass();
    }

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

3.6、密码加密算法sm3

需要实现PasswordEncoder类,主要用于账号密码登录时的密码校验

import com.b2bwings.nry.common.util.CommonUtil;
import com.b2bwings.nry.common.util.SM3Util;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Objects;

/**
 * 密码加密算法sm3
 * @author admim
 * @date 2025/8/21 15:05
 */
public class SM3PasswordEncode implements PasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        return encode(rawPassword, SM3Util.getSale());
    }

    public String encode(CharSequence rawPassword, String salt) {
        return CommonUtil.hmacsm3Encrypt(salt + rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        String salt = encodedPassword.substring(0, encodedPassword.indexOf("|"));
        encodedPassword = encodedPassword.substring(encodedPassword.indexOf("|") + 1);
        return Objects.equals(CommonUtil.hmacsm3Encrypt(salt + rawPassword), encodedPassword);
    }

    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        return PasswordEncoder.super.upgradeEncoding(encodedPassword);
    }
}

3.7、JWT工具类

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;

/**
 * 自定义认证令牌-->短信验证码登录认证
 * @author 
 * @date 2025/08/22
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    // 手机号(认证主体)
    private final Object principal;
    // 短信验证码(凭证)
    private String code;

    // 未认证构造器(用于认证前)
    public SmsAuthenticationToken(Object principal, String code) {
        super(null);
        this.principal = principal;
        this.code = code;
        // 标记为未认证
        setAuthenticated(false);
    }

    // 已认证构造器(用于认证后)
    public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.code = null;
        // 标记为已认证
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        // 返回验证码
        return this.code;
    }

    @Override
    public Object getPrincipal() {
        // 返回手机号
        return this.principal;
    }

    // 禁止直接调用setAuthenticated,避免误用
    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("请使用已认证构造器");
        }
        super.setAuthenticated(false);
    }
}

4、认证实现类(短信、密码)

4.1、短信登录认证类


import com.b2bwings.nry.app.business.entity.LoanUser;
import com.b2bwings.nry.app.business.service.LoanUserService;
import com.b2bwings.nry.app.business.service.SmsService;
import com.b2bwings.nry.app.business.web.vo.authentication.LoginLoanUser;
import com.b2bwings.nry.app.business.web.vo.authentication.LoginVo;
import com.b2bwings.nry.app.business.enums.CaptchaType;
import com.b2bwings.nry.app.business.service.LoginService;
import com.b2bwings.nry.common.api.ApiCode;
import com.b2bwings.nry.common.api.ApiResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

/**
 * 短信验证码登录
 * @author maijunhao
 */
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private LoanUserService loanUserService;

    @Autowired
    private SmsService smsService;

    @Override
    public Authentication authenticate(Authentication auth) {
        SmsAuthenticationToken authentication = (SmsAuthenticationToken) auth;
        String phone = (String) authentication.getPrincipal();
        String code = (String) authentication.getCredentials();

        // 1. 校验验证码

        // 2. 处理用户信息
        LoginLoanUser user = xxxxx;

        // 3. 返回已认证的Token
        return new SmsAuthenticationToken(user, user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

4.2、账号密码登录认证类


import com.b2bwings.nry.app.business.entity.LoanUser;
import com.b2bwings.nry.app.business.service.LoanUserService;
import com.b2bwings.nry.app.business.service.LoginService;
import com.b2bwings.nry.app.business.web.vo.authentication.LoginLoanUser;
import com.b2bwings.nry.app.business.web.vo.authentication.LoginVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * 账号密码登录,用户认证数据库查询服务
 * @author 
 */

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private LoanUserService loanUserService;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {

        // 1. 通过账号查询用户
        LoanUser user = loanUserService.getByAccount(account);

        // 2. 处理用户信息
        LoginLoanUser logUser = loanUserService.handleLoginInfo(user);
        logUser.setUser(user);

        //3.返回登录用户信息
        return logUser;
    }
}

5、自定义认证令牌

这里只需要自定义短信认证令牌即可,账号密码登录已有默认认证类(UsernamePasswordAuthenticationToken)

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;

/**
 * 自定义认证令牌-->短信验证码登录认证
 * @author 
 * @date 2025/08/22
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    // 手机号(认证主体)
    private final Object principal;
    // 短信验证码(凭证)
    private String code;

    // 未认证构造器(用于认证前)
    public SmsAuthenticationToken(Object principal, String code) {
        super(null);
        this.principal = principal;
        this.code = code;
        // 标记为未认证
        setAuthenticated(false);
    }

    // 已认证构造器(用于认证后)
    public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.code = null;
        // 标记为已认证
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        // 返回验证码
        return this.code;
    }

    @Override
    public Object getPrincipal() {
        // 返回手机号
        return this.principal;
    }

    // 禁止直接调用setAuthenticated,避免误用
    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("请使用已认证构造器");
        }
        super.setAuthenticated(false);
    }
}

6、登录示例

6.1、账号密码登录

/**
     * 账号密码登录
     * @param hostAddress 主机地址
     * @param param 请求参数
     * @return ApiResult
     * @throws Exception 登录异常
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ApiResult loginByPassword(String hostAddress, LoginByPasswordParam param) throws Exception {
        LoanUser user = loanUserService.getByAccount(param.getAccount());
        if (user == null) {
            throw new BizException("账号或密码错误!");
        }
        // 加锁控制登录频率
        String lockKey = CommonRedisKey.LOGIN_KEY_WECHAT + user.getLoanUserId();
        String lockId = redisUtil.tryLock(lockKey, 60);
        if (lockId == null) {
            throw new BizException("请勿重复操作~");
        }
        try {
            //登录次数校验
            //loginValidated(user.getLoanUserId());

            //账号密码处理
            handleAccountAndPassword(param);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getAccount(),param.getPassword());
            Authentication authenticate;
            try {
                authenticate = authenticationManager.authenticate(authenticationToken);
            } catch (Exception e) {
                //int ret =timesAddOne(user.getLoanUserId());
                //throw new BizException("用户名或密码错误,当前错误次数" + ret + "次。");
                throw new BizException("用户名或密码错误!");
            }

            //认证通过 使用userid 生成jwt token令牌
            LoginLoanUser loginUser = (LoginLoanUser) authenticate.getPrincipal();
            LoanUser sysUser = loginUser.getUser();
            String userId = sysUser.getLoanUserId().toString();

            Map<String, String> payloadMap = new HashMap<>();
            payloadMap.put("userId", userId);
            payloadMap.put("account", sysUser.getAccount());
            String token = JwtUtils.generateToken(payloadMap);

            if (!redisUtil.set(CommonRedisKey.APPLET_SESSION_TIMEOUT + userId, loginUser, SysConstant.AMOUNT)) {
                log.error("redis连接不上,登录失败");
                throw new BizException("登录失败");
            }

            //登录成功参数处理
            
            //返回结果
            return ApiResult.result(ApiCode.SUCCESS, "登录成功");
        }finally {
            redisUtil.unLock(lockKey, lockId);
        }
    }

6.2、手机号验证码登录

/**
     * 手机验证码登录
     * @param hostAddress 主机地址
     * @param param 请求参数
     * @return ApiResult
     * @throws Exception 登录异常
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ApiResult loginByCode(String hostAddress, LoginByCodeParam param) throws Exception {
        // 根据手机号查询用户
        LoanUser user = loanUserService.getByPhone(param.getPhone());
        if (user == null){
            throw new BizException("当前用户不存在,请检查手机号是否正确!");
        }

        // 加锁控制登录频率
        String lockKey = CommonRedisKey.LOGIN_KEY_WECHAT + user.getLoanUserId();
        String lockId = redisUtil.tryLock(lockKey, 60);
        try {
            if (lockId == null) {
                throw new BizException("请勿重复操作~");
            }
            //登录次数校验
            //loginValidated(user.getLoanUserId());

            //登录认证,跳UserDetailsServiceImp.loadUserByUsername()方法认证
            SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(user.getPhone(),param.getCode());
            Authentication authenticate;
            try {
                authenticate = authenticationManager.authenticate(authenticationToken);
            } catch (Exception e) {
                //int ret = timesAddOne(user.getLoanUserId());
                //throw new BizException("手机号码或验证码有误,当前错误次数" + ret + "次。");
                throw new BizException("手机号码或验证码有误!");
            }

            //认证通过 使用userid 生成jwt token令牌
            LoginLoanUser loginUser = (LoginLoanUser) authenticate.getPrincipal();
            LoanUser sysUser = loginUser.getUser();
            String userId = sysUser.getLoanUserId().toString();
            //token构建
            Map<String, String> payloadMap = new HashMap<>();
            payloadMap.put("userId", userId);
            payloadMap.put("account", sysUser.getAccount());
            String token = JwtUtils.generateToken(payloadMap);
            //会话控制
            if (!redisUtil.set(CommonRedisKey.APPLET_SESSION_TIMEOUT + userId, loginUser, SysConstant.AMOUNT)) {
                log.error("redis连接不上,登录失败");
                throw new BizException("登录失败");
            }

            //登录成功参数处理
            
            
            return ApiResult.result(ApiCode.SUCCESS, "登录成功");
        } finally {
            redisUtil.unLock(lockKey, lockId);
        }
    }

四、细节处理

1、注册时前后端加解密方式

使用SM2的加解密方式用于注册时的密码传输,需要成对生成公钥、私钥,公钥用于加密(前端)、私钥用于解密(后端)。

生成密钥对

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.70</version> <!-- 建议使用较新版本 -->
</dependency>
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.util.encoders.Hex;

import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/​**​
 * SM2密钥对生成工具类
 * 使用Bouncy Castle密码库实现国密SM2算法密钥对生成
 */
public class SM2KeyGenerator {

    // 添加Bouncy Castle提供者
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /​**​
     * 生成SM2密钥对
     * @return KeyPair SM2密钥对,包含公钥和私钥
     * @throws Exception 如果密钥生成失败
     */
    public static KeyPair generateSM2KeyPair() throws Exception {
        // 获取SM2椭圆曲线参数规范
        ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
        
        // 获取密钥对生成器实例
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", "BC");
        
        // 初始化密钥对生成器
        keyPairGenerator.initialize(sm2Spec, new SecureRandom());
        
        // 生成并返回密钥对
        return keyPairGenerator.generateKeyPair();
    }

    /​**​
     * 将公钥转换为十六进制字符串
     * @param publicKey 公钥对象
     * @return 十六进制格式的公钥字符串
     */
    public static String publicKeyToHex(PublicKey publicKey) {
        return Hex.toHexString(publicKey.getEncoded());
    }

    /​**​
     * 将私钥转换为十六进制字符串
     * @param privateKey 私钥对象
     * @return 十六进制格式的私钥字符串
     */
    public static String privateKeyToHex(PrivateKey privateKey) {
        return Hex.toHexString(privateKey.getEncoded());
    }

    /​**​
     * 从十六进制字符串恢复公钥
     * @param hexString 十六进制公钥字符串
     * @return 公钥对象
     * @throws Exception 如果恢复失败
     */
    public static PublicKey publicKeyFromHex(String hexString) throws Exception {
        byte[] keyBytes = Hex.decode(hexString);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
        return keyFactory.generatePublic(keySpec);
    }

    /​**​
     * 从十六进制字符串恢复私钥
     * @param hexString 十六进制私钥字符串
     * @return 私钥对象
     * @throws Exception 如果恢复失败
     */
    public static PrivateKey privateKeyFromHex(String hexString) throws Exception {
        byte[] keyBytes = Hex.decode(hexString);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
        return keyFactory.generatePrivate(keySpec);
    }

    /​**​
     * 测试示例
     */
    public static void main(String[] args) {
        try {
            // 生成SM2密钥对
            KeyPair keyPair = generateSM2KeyPair();
            PublicKey publicKey = keyPair.getPublic();
            PrivateKey privateKey = keyPair.getPrivate();
            
            // 转换为十六进制字符串
            String publicKeyHex = publicKeyToHex(publicKey);
            String privateKeyHex = privateKeyToHex(privateKey);
            
            System.out.println("SM2公钥 (Hex): " + publicKeyHex);
            System.out.println("SM2私钥 (Hex): " + privateKeyHex);
            System.out.println("公钥长度: " + publicKeyHex.length() + "字符");
            System.out.println("私钥长度: " + privateKeyHex.length() + "字符");
            
            // 测试密钥恢复功能
            PublicKey restoredPublicKey = publicKeyFromHex(publicKeyHex);
            PrivateKey restoredPrivateKey = privateKeyFromHex(privateKeyHex);
            
            System.out.println("公钥恢复验证: " + restoredPublicKey.equals(publicKey));
            System.out.println("私钥恢复验证: " + restoredPrivateKey.equals(privateKey));
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

前端加密

后端解密

/**
     * 解密
     *
     * @param encryptText 16进制编码的密文
     * @param privateKey  Base64编码
     * @return
     * @throws
     */
    public static String decryptByPrivateKey(String encryptText, String privateKey) {
        SM2 sm2 = new SM2(privateKey, null);
        //前端密文需要加04
        try {
            return sm2.decryptStr("04" + encryptText, KeyType.PrivateKey);
        } catch (Exception e) {
            return sm2.decryptStr(encryptText, KeyType.PrivateKey);
        }
    }

解密后加密入库(SM3摘要加密)

String salt = SM3Util.getSale();
String encodePassword = CommonUtil.hmacsm3Encrypt(salt + CommonUtil.hmacsm3Encrypt(password));

2、登录时前后端加解密方式

2.1、登录时前端加密传参(先对明码进行SM3加密),再对整个加密后密码进行SM2加密

2.2、登录时后端解密校验,直接使用SM2解密,保留SM3加密形态去校验密码

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐