结合Spring Security 框架+ JWT,Java实现短信验证码、账号密码Token认证登录
本文介绍了基于Spring Security实现双因素认证(账号密码+短信验证码)的技术方案。系统采用JWT令牌进行无状态认证,关键实现包括:1)自定义安全配置类整合多种认证方式;2)SM3国密算法加密存储密码;3)SM2非对称加密保障传输安全;4)JWT过滤器实现Token校验与续期;5)异常处理机制精细化权限管控。方案通过组合AuthenticationProvider、自定义UserDeta
一、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 非常简单:
- 添加依赖:引入
spring-boot-starter-security依赖。 - 基础配置:Spring Boot 会自动配置一个默认的安全策略,包括:
- 所有端点都需要认证。
- 提供一个默认的登录页(
/login)和注销页(/logout)。 - 为每个用户生成一个随机密码(在控制台输出)。
- 自定义安全配置:通常通过配置
SecurityFilterChainBean 来自定义安全规则,如指定放行路径、自定义登录页、设置权限等
二、业务场景
系统同时要求具备短信验证码及账号密码登录,同时需要注意密码必须需要加密,不能使用明码传输。
三、具体实现
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、自定义安全配置
配置核心作用分析,该配置类主要通过自定义和组合多项安全组件,实现了以下核心功能:
| 功能模块 | 实现方式 | 核心作用 |
|---|---|---|
| 认证方式 | 注入 JwtAuthenticationFilter, SmsAuthenticationProvider |
支持 JWT令牌 和 短信 两种认证方式 |
| 授权控制 | @EnableGlobalMethodSecurity(prePostEnabled = true) |
开启方法级权限注解(如 @PreAuthorize) |
| 会话管理 | SessionCreationPolicy.STATELESS |
设置为无状态,适用于 RESTful API |
| 密码编码 | 返回自定义 SM3PasswordEncode |
使用国密 SM3 算法进行密码加密与验证 |
| 异常处理 | 注入 AccessDeniedHandlerImpl, AuthenticationEntryPointImpl |
自定义认证失败和权限不足的异常响应 |
| 请求规则 | 配置 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加密形态去校验密码
更多推荐

所有评论(0)