跳到主要内容
Spring Boot 项目用户模块设计:注册登录、权限管控与敏感数据加密 | 极客日志
Java java
Spring Boot 项目用户模块设计:注册登录、权限管控与敏感数据加密 综述由AI生成 基于 Spring Boot 的用户模块设计方案,涵盖注册登录流程、权限管控及敏感数据加密技术。核心内容包括密码加盐哈希存储、手机号 AES 加密、MyBatis TypeHandler 自动处理、全局异常处理机制。登录功能采用 JWT 实现无状态认证,结合 Redis 缓存验证码。此外还实现了强制登录拦截器、管理员后台管理及用户列表展示等功能,为构建安全的用户系统提供完整后端架构参考。
萤火微光 发布于 2026/3/29 更新于 2026/5/30 37 浏览一、注册
1.1 敏感字段加密
用户注册时,密码和手机号等敏感信息需加密存储,避免明文泄露风险。
密码加密 :采用加盐哈希(SHA-256)方案,用户注册时生成随机盐值,将密码与盐拼接后进行哈希运算,最终存储哈希结果,确保不可逆。
手机号加密 :因业务可能需使用明文手机号(如发送短信),采用 AES 对称加密,存储加密后的结果,使用时解密。
加密工具 :引入国产 Java 工具类库 Hutool,封装加密解密方法,简化开发。
Hutool 官方文档:https://hutool.cn/
Maven 仓库地址:https://mvnrepository.com/artifact/cn.hutool
可引入以下依赖使用:
<dependency >
<groupId > cn.hutool</groupId >
<artifactId > hutool-all</artifactId >
<version > 5.8.25</version >
</dependency >
1.2 用户注册
前后端交互接口
{
"name" : "张三" ,
"mail" : "[email protected] " ,
"phoneNumber" : "13188888888" ,
"password" : "123456789" ,
"identity" : "ADMIN"
}
{
"code" : 200 ,
"data" : {
"userId" : 22
} ,
"msg" : ""
}
接收注册参数,通过 @Validated 注解进行参数校验,调用 Service 层完成注册逻辑,返回注册结果。
核心类:
UserRegisterParam:注册入参封装(姓名、邮箱、手机号、密码、身份),含 @NotBlank 等校验注解。
UserRegisterResult:注册出参封装(用户 ID)。
UserIdentityEnum:用户身份枚举(普通用户 NORMAL、管理员 ADMIN)。
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
@Autowired
private VerificationCodeService verificationCodeService;
@PostMapping("/register")
public CommonResult<UserRegisterResult> userRegister (@Validated @RequestBody UserRegisterParam param) {
logger.info("userRegister UserRegisterParam 用户注册:{}" , JacksonUtil.writeValueAsString(param));
UserRegisterDTO userRegisterDTO = userService.register(param);
return CommonResult.success(convertToUserRegisterResult(userRegisterDTO));
}
private UserRegisterResult convertToUserRegisterResult (UserRegisterDTO userRegisterDTO) {
UserRegisterResult result = new UserRegisterResult ();
if (null == userRegisterDTO) {
throw new ControllerException (ControllerErrorCodeConstants.REGISTER_ERROR);
}
result.setUserId(userRegisterDTO.getUserId());
return result;
}
}
使用 SpringBoot 中集成的 Validation 需要引入依赖:
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-validation</artifactId >
</dependency >
接口与实现分离,定义 UserService 接口,UserServiceImpl 实现具体逻辑:
校验注册信息(邮箱格式、手机号格式、身份合法性、密码格式、邮箱/手机号唯一性)。
对密码和手机号进行加密处理。
调用 Dao 层将用户信息存入数据库。
返回用户 ID 封装结果。
核心工具类:RegexUtil,封装邮箱、手机号、密码的格式校验正则表达式。
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private VerificationCodeService verificationCodeService;
@Override
public UserRegisterDTO register (UserRegisterParam param) {
checkRegisterInfo(param);
UserDO userDO = new UserDO ();
userDO.setUserName(param.getName());
userDO.setEmail(param.getMail());
userDO.setPhoneNumber(newEncrypt(param.getPhoneNumber()));
userDO.setIdentity(param.getIdentity());
if (StringUtils.hasLength(param.getPassword())) {
userDO.setPassword(DigestUtil.sha256Hex(param.getPassword()));
}
userMapper.insert(userDO);
UserRegisterDTO userRegisterDTO = new UserRegisterDTO ();
userRegisterDTO.setUserId(userDO.getId());
return userRegisterDTO;
}
private void checkRegisterInfo (UserRegisterParam param) {
if (null == param) {
throw new ServiceException (ServiceErrorCodeConstants.REGISTER_INFO_IS_EMPTY);
}
if (!RegexUtil.checkMail(param.getMail())) {
throw new ServiceException (ServiceErrorCodeConstants.MAIL_ERROR);
}
if (!RegexUtil.checkMobile(param.getPhoneNumber())) {
throw new ServiceException (ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
if (null == UserIdentityEnum.forName(param.getIdentity())) {
throw new ServiceException (ServiceErrorCodeConstants.IDENTITY_ERROR);
}
if (param.getIdentity().equalsIgnoreCase(UserIdentityEnum.ADMIN.name()) && !StringUtils.hasLength(param.getPassword())) {
throw new ServiceException (ServiceErrorCodeConstants.PASSWORD_IS_EMPTY);
}
if (StringUtils.hasLength(param.getPassword()) && !RegexUtil.checkPassword(param.getPassword())) {
throw new ServiceException (ServiceErrorCodeConstants.PASSWORD_ERROR);
}
if (checkMailUsed(param.getMail())) {
throw new ServiceException (ServiceErrorCodeConstants.MAIL_USED);
}
if (checkPhoneNumberUsed(param.getPhoneNumber())) {
throw new ServiceException (ServiceErrorCodeConstants.PHONE_NUMBER_USED);
}
}
private boolean checkPhoneNumberUsed (String phoneNumber) {
int count = userMapper.countByPhone(newEncrypt(phoneNumber));
return count > 0 ;
}
private boolean checkMailUsed (String mail) {
int count = userMapper.countByMail(mail);
return count > 0 ;
}
}
接口分离设计的好处: 有助于创建更加灵活、可维护和可扩展的软件系统。抽象与具体实现分离 :接口定义了一组操作的契约,而实现则提供了这些操作的具体行为。这种分离允许改变具体实现而不影响使用接口的客户端代码。支持多态性 :接口允许通过共同的接口来引用不同的实现,这是多态性的基础,使得代码更加灵活和通用。提高代码的可读性和可理解性 :接口提供了清晰的 API 视图,使得其他开发者能够更容易地理解和使用这些 API。安全性 :接口可以隐藏实现细节,只暴露必要的操作,这有助于保护系统的内部状态和实现不被外部直接访问。遵循开闭原则 :软件实体应当对扩展开放,对修改封闭。接口与实现的分离使得在不修改客户端代码的情况下扩展系统的功能。促进面向对象的设计 :接口与实现的分离鼓励开发者进行面向对象的设计,考虑如何将系统分解为可重用和可组合的组件。
使用 MyBatis 实现数据库交互,核心接口 UserMapper:
insert:插入用户信息,支持自动生成主键。
countByPhoneNumber:查询手机号绑定的用户数(校验唯一性)。
countByMail:查询邮箱绑定的用户数(校验唯一性)。
核心类:UserDO,映射用户表,继承 BaseDO(含主键 id、创建时间 gmtCreate、更新时间 gmtModified)。
使用 jQuery Validate 插件校验表单字段(姓名、邮箱、手机号、密码必填,密码长度≥6)。
表单验证通过后,通过 AJAX 发送 POST 请求到 /register 接口,携带用户注册信息。
注册成功跳转登录页,失败提示错误信息。
1.3 TypeHandler 对手机号进行存储时,要先将手机号加密,如果要拿出使用时,还要进行一次解密操作。为简化手机号的自动加解密操作,使用 MyBatis 的 TypeHandler 实现字段处理。
TypeHandler:简单理解就是当处理特殊字段时,实现一些方法,让 mybatis 遇到这些特定字段可以自动运行处理。
Encrypt 类 :标记需加解密的字段类型,封装待加密的字符串值。
EncryptTypeHandler 类 :实现 BaseTypeHandler<Encrypt>,重写以下方法:
setNonNullParameter:设置参数时对手机号进行 AES 加密。
getNullableResult:查询结果时对手机号进行 AES 解密。
配置 :在 application.properties 中指定 TypeHandler 的包路径。
二、控制层通用异常处理 使用 @RestControllerAdvice + @ExceptionHandler 实现全局异常处理,统一响应格式。可以针对所有异常类型先进行通用处理后,再对特定异常类型进行不同的处理操作。
捕获 Exception、ServiceException、ControllerException 三类异常。
日志记录异常信息,返回 CommonResult 格式的错误响应,包含错误码和错误提示。
三、登录
3.1 发送验证码 采用阿里云短信服务发送验证码,支持短信登录方式。因为没用企业认证,只能使用其提供的一些特定模板,示例如下:
在 application.properties 中配置阿里云短信服务的 AccessKeyId、AccessKeySecret、签名名称。
SMSUtil:封装短信发送逻辑,通过阿里云 SDK 调用短信接口,处理发送结果。
CaptchaUtil:基于 Hutool 生成随机数字验证码。
public class CaptchaUtil {
public static String getCaptcha (int length) {
RandomGenerator randomGenerator = new RandomGenerator ("0123456789" , length);
LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200 , 100 );
lineCaptcha.setGenerator(randomGenerator);
lineCaptcha.createCode();
return lineCaptcha.getCode();
}
}
请求 :/verification-code/send?phoneNumber=13199999999 GET
响应 :
{
"code" : 200 ,
"data" : true ,
"msg" : ""
}
接收手机号参数,调用 VerificationCodeService 发送验证码,返回发送结果。
VerificationCodeService 接口定义发送验证码和获取验证码的方法。
VerificationCodeServiceImpl 实现逻辑:
校验手机号格式。
生成验证码。
调用 SMSUtil 发送短信。
将验证码存入 Redis,设置 60 秒过期时间。
VerificationCodeServiceImpl 阿里云短信验证码服务类示例:
@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Autowired
private SMSUtil smsUtil;
@Autowired
private RedisUtil redisUtil;
@Override
public void sendVerificationCode (String phoneNumber) {
if (!RegexUtil.checkMobile(phoneNumber)) {
throw new ServiceException (ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
String code = CaptchaUtil.getCaptcha(Constants.CODE_LENGTH);
smsUtil.sendVerifyCode(phoneNumber, code);
redisUtil.set(Constants.VERIFICATION_CODE_PREFIX + phoneNumber, code, Constants.VERIFICATION_CODE_TIMEOUT);
}
}
3.2 Redis 的配置与使用
> 核心工具类 RedisUtil 基于 StringRedisTemplate 封装 Redis 操作,避免乱码问题,核心方法:
hasKey:判断键是否存在。
setExpire:设置键的过期时间。
getExpire:获取键的过期时间。
del:删除键。
get:获取字符串类型键的值。
set:设置字符串类型键的值(支持过期时间)。
@Configuration
public class RedisUtil {
private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
public boolean set (String key, String value) {
try {
stringRedisTemplate.opsForValue().set(key, value);
return true ;
} catch (Exception e) {
logger.error("RedisUtil error,set({}, {})" , key, value, e);
return false ;
}
}
public boolean set (String key, String value, Long time) {
try {
stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
return true ;
} catch (Exception e) {
logger.error("RedisUtil error,set({}, {}, {})" , key, value, time, e);
return false ;
}
}
public String get (String key) {
try {
return StringUtils.hasText(key) ? stringRedisTemplate.opsForValue().get(key) : null ;
} catch (Exception e) {
logger.error("RedisUtil error,get({})" , key, e);
return null ;
}
}
}
3.3 JWT 采用 JWT(JSON Web Token)实现无状态登录,解决集群环境下 Session 共享问题。
> JWT 令牌介绍 JSON Web Token (JWT) 是一个开放的行业标准 (RFC 7519),用于客户端和服务器之间传递安全可靠的信息。
其本质是一个 token,是一种紧凑的 URL 安全方法。
JWT 组成: JWT 由三部分组成,每部分中间使用点 (.) 分隔,例如:aaaaa.bbbbb.cccc
Header (头部)
头部包括令牌的类型(即 JWT)及使用的哈希算法(如 HMAC SHA256 或 RSA)。
Signature (签名)
此部分用于防止 JWT 内容被篡改,确保安全性。
防止被篡改,而不是防止被解析。
JWT 之所以安全,就是因为最后的签名。JWT 当中任何一个字符被篡改,整个令牌都会校验失败。
就好比身份证,之所以能标识一个人的身份,是因为它不能被篡改,而不是因为内容加密。(任何人都可以看到身份证的信息,JWT 也是)
Payload (负载)
负载部分是存放有效信息的地方,里面是一些自定义内容。比如:
{ "userId" : "123" , "userName" : "zhangsan" }
也可以存在 JWT 提供的现成字段,比如 exp (过期时间戳) 等。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
> 核心工具类 JWTUtil
genJwt:生成 JWT 令牌,包含自定义载荷(用户 ID、身份)、签发时间、过期时间(1 小时),使用 HMAC SHA 算法签名。
parseJWT:解析 JWT 令牌,验证签名合法性,返回载荷信息。
getUserIdFromToken:从令牌中提取用户 ID。
public class JWTUtil {
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
private static final String SECRET = "********" ;
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET));
private static final long EXPIRATION = 24 * 60 * 60 * 1000 ;
public static String genJwt (Map<String, Object> claim) {
String jwt = Jwts.builder()
.setClaims(claim)
.setIssuedAt(new Date ())
.setExpiration(new Date (System.currentTimeMillis() + EXPIRATION))
.signWith(SECRET_KEY)
.compact();
return jwt;
}
public static Claims parseJWT (String jwt) {
if (!StringUtils.hasLength(jwt)) {
return null ;
}
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
Claims claims = null ;
try {
claims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();
} catch (Exception e) {
logger.error("解析令牌错误,jwt:{}" , jwt, e);
}
return claims;
}
}
3.4 管理员登录 支持'电话 + 密码'和'电话 + 验证码'两种登录方式,基于 JWT 返回令牌。
登陆页面把用户名密码提交给服务器。
服务器端验证用户名密码是否正确,如果正确,服务器生成令牌,下发给客户端。
客户端把令牌存储起来(比如 Cookie、local storage 等),后续请求时,把 token 发给服务器。
服务器对令牌进行校验,如果令牌正确,进行下一步操作。
密码登录请求 :/password/login POST
{
"loginName" : "13199999999" ,
"password" : "123456" ,
"mandatoryIdentity" : "ADMIN"
}
{
"code" : 200 ,
"data" : {
"token" : "eyJhbGciOiJIUzI1NiJ9..." ,
"identity" : "ADMIN"
} ,
"msg" : ""
}
验证码登录请求 :/message/login POST
{
"loginMobile" : "13199999999" ,
"verificationCode" : "0475" ,
"mandatoryIdentity" : "ADMIN"
}
{
"code" : 200 ,
"data" : {
"token" : "eyJhbGciOiJIUzI1NiJ9..." ,
"identity" : "ADMIN"
} ,
"msg" : ""
}
提供两个登录接口,分别处理密码登录和验证码登录。
接收登录参数,通过 @Validated 校验,调用 UserService 完成登录逻辑,返回 UserLoginResult(含 token 和身份)。
核心类:
UserPasswordLoginParam:密码登录入参(登录名、密码、强制身份)。
ShortMessageLoginParam:验证码登录入参(手机号、验证码、强制身份)。
UserLoginResult:登录出参(token、身份)。
UserService 接口新增 login 方法,支持不同登录参数类型。
UserServiceImpl 实现逻辑:
区分登录类型(密码登录/验证码登录)。
密码登录:校验登录名格式(邮箱/手机号)、查询用户信息、校验身份、校验密码,生成 JWT 令牌。
验证码登录:校验手机号格式、查询用户信息、校验身份、校验验证码(从 Redis 获取),生成 JWT 令牌。
UserMapper 新增接口:
selectByEmail:通过邮箱查询用户信息。
selectByPhoneNumber:通过手机号查询用户信息。
支持 tab 切换两种登录方式(密码登录/验证码登录)。
使用 jQuery Validate 校验表单字段(手机号、密码/验证码必填)。
验证码登录支持 60 秒倒计时重新获取。
登录成功后,将 token 和身份存入 localStorage,跳转管理员首页。
四、强制登录 通过拦截器实现非登录页面的强制登录校验。例如,当用户当前尚未登陆,访问抽奖页时希望自动跳转到登录页面。
4.1 前端处理
所有 AJAX 请求通过 ajaxSend 方法在请求头中添加 user_token(从 localStorage 获取),让后端取校验。
若请求返回 401(未登录),跳转至登录页(含 iframe 框架场景处理)。
4.2 后端处理 实现 HandlerInterceptor,重写 preHandle 方法:
从请求头获取 user_token。
调用 JWTUtil 解析令牌,校验合法性。
令牌无效或不存在时,返回 401 状态码,拦截请求。
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("user_token" );
log.info("获取 Token: {}" , token);
log.info("获取请求路径:{}" , request.getRequestURI());
Claims claims = JWTUtil.parseJWT(token);
if (null == claims) {
log.error("解析 JWT 令牌失败!" );
response.setStatus(401 );
return false ;
}
log.info("解析 JWT 令牌成功!" );
return true ;
}
}
实现 WebMvcConfigurer,配置拦截器:
拦截所有请求(/**)。
排除登录、注册、验证码发送等接口,以及静态资源(HTML、CSS、JS 等)。
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
private final List<String> excludes = Arrays.asList(
"/**.html" ,
"/css/**" ,
"/js/**" ,
"/pic/**" ,
"/*.jpg" ,
"/*.png" ,
"/favicon.ico" ,
"/**/login" ,
"/register" ,
"/verification-code/send" ,
"/winning-records/show"
);
@Override
public void addInterceptors (InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**" )
.excludePathPatterns(excludes);
}
}
五、用户管理
5.1 后台管理页面
管理员首页(admin.html)通过 iframe 加载子页面,支持导航菜单切换。
监听子页面消息,实现子页面向父页面传递跳转指令。
提供退出登录功能,清除 localStorage 中的 token,跳转登录页。
5.2 注册用户(管理员新增)
前端逻辑与普通注册类似,但新增用户后直接跳转到用户列表页。
通过 URL 参数 jumpList 控制跳转行为,admin 参数控制用户身份。
5.3 人员列表展示
请求 :/base-user/find-list GET(可选参数 identity 筛选身份)
响应 :
{
"code" : 200 ,
"data" : [
{
"userId" : 15 ,
"userName" : "郭靖" ,
"identity" : "NORMAL"
} ,
{
"userId" : 14 ,
"userName" : "王五" ,
"identity" : "ADMIN"
}
] ,
"msg" : ""
}
接收身份筛选参数,调用 UserService 查询用户列表,转换为 UserBaseInfoResult 返回。
UserService 新增 findUserList 方法,支持按身份筛选(null 查询所有)。
UserServiceImpl 实现逻辑:查询用户列表,转换为 UserDTO,包含用户 ID、姓名、邮箱、手机号、身份。
UserMapper 新增 selectUserList 方法,支持按身份查询用户列表,按 ID 降序排序。
页面加载时通过 AJAX 请求 /base-user/find-list 接口,获取用户列表。
渲染表格展示用户 ID、姓名、身份。
未登录时跳转至登录页。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online