文章目录
-
- 一、注册
-
- [1.1 敏感字段加密](#1.1 敏感字段加密)
- [1.2 用户注册](#1.2 用户注册)
- [1.3 TypeHandler](#1.3 TypeHandler)
- 二、控制层通用异常处理
- 三、登录
-
- [3.1 发送验证码](#3.1 发送验证码)
- [3.2 Redis的配置与使用](#3.2 Redis的配置与使用)
-
- [> 核心工具类`RedisUtil`](#> 核心工具类
RedisUtil)
- [> 核心工具类`RedisUtil`](#> 核心工具类
- [3.3 JWT](#3.3 JWT)
-
- [> JWT 令牌介绍](#> JWT 令牌介绍)
- [> 核心工具类`JWTUtil`](#> 核心工具类
JWTUtil)
- [3.4 管理员登录](#3.4 管理员登录)
- 四、强制登录
-
- [4.1 前端处理](#4.1 前端处理)
- [4.2 后端处理](#4.2 后端处理)
- 五、用户管理
-
- [5.1 后台管理页面](#5.1 后台管理页面)
- [5.2 注册用户(管理员新增)](#5.2 注册用户(管理员新增))
- [5.3 人员列表展示](#5.3 人员列表展示)

一、注册
1.1 敏感字段加密
用户注册时,密码和手机号等敏感信息需加密存储,避免明文泄露风险。
- 密码加密:采用加盐哈希(SHA-256)方案,用户注册时生成随机盐值,将密码与盐拼接后进行哈希运算,最终存储哈希结果,确保不可逆。
- 手机号加密:因业务可能需使用明文手机号(如发送短信),采用AES对称加密,存储加密后的结果,使用时解密。
- 加密工具:引入国产Java工具类库Hutool,封装加密解密方法,简化开发。
HuTool 官网地址:https://hutool.cn/
Maven仓库地址:https://mvnrepository.com/artifact/cn.hutool
可引入以下依赖使用:
xml
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
1.2 用户注册
时序图

前后端交互接口
- 请求 :
/register POST
json
{
"name":"张三",
"mail":"451@qq.com",
"phoneNumber":"13188888888",
"password":"123456789",
"identity":"ADMIN"
}
- 响应:
json
{
"code": 200,
"data": {
"userId": 22
},
"msg": ""
}
Controller层接口设计
- 接收注册参数,通过
@Validated注解进行参数校验,调用Service层完成注册逻辑,返回注册结果。 - 核心类:
UserRegisterParam:注册入参封装(姓名、邮箱、手机号、密码、身份),含@NotBlank等校验注解。UserRegisterResult:注册出参封装(用户ID)。UserIdentityEnum:用户身份枚举(普通用户NORMAL、管理员ADMIN)。
控制层代码示例:
java
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
@Autowired
private VerificationCodeService verificationCodeService;
/**
* 注册
* @param param
* @return
*/
@PostMapping("/register")
public CommonResult<UserRegisterResult> userRegister(
@Validated @RequestBody UserRegisterParam param){
// 日志打印
logger.info("userRegister UserRegisterParam 用户注册: {}", JacksonUtil.writeValueAsString(param));
// 调用Service 层服务进行访问
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需要引入依赖:
xml
<!-- spring-boot 2.3及以上的版本只需要引⼊下⾯的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Service层接口设计
- 接口与实现分离,定义
UserService接口,UserServiceImpl实现具体逻辑:- 校验注册信息(邮箱格式、手机号格式、身份合法性、密码格式、邮箱/手机号唯一性)。
- 对密码和手机号进行加密处理。
- 调用Dao层将用户信息存入数据库。
- 返回用户ID封装结果。
- 核心工具类:
RegexUtil,封装邮箱、手机号、密码的格式校验正则表达式。
实现类代码示例:
java
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private VerificationCodeService verificationCodeService;
/**
* 注册
*
* @param param
* @return
*/
@Override
public UserRegisterDTO register(UserRegisterParam param) {
// 校验用户信息
checkRegisterInfo(param);
//加密数据
UserDO userDO = new UserDO();
userDO.setUserName(param.getName());
userDO.setEmail(param.getMail());
userDO.setPhoneNumber(new Encrypt(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);
}
// 校验邮箱格式 xxx@xxx.xxx
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);
}
// 校验密码格式 至少6位
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);
}
}
/**
* 检验手机号是否被使用
* @param phoneNumber
* @return
*/
private boolean checkPhoneNumberUsed(String phoneNumber) {
int count = userMapper.countByPhone(new Encrypt(phoneNumber));
return count > 0;
}
/**
* 校验邮箱是否被使用
* @param mail
* @return
*/
private boolean checkMailUsed(String mail) {
int count = userMapper.countByMail(mail);
return count > 0;
}
}
接口分离设计的好处: 有助于创建更加灵活、可维护和可扩展的软件系统。
- 抽象与具体实现分离:接口定义了一组操作的契约,而实现则提供了这些操作的具体行为。这种分离允许改变具体实现而不影响使用接口的客户端代码。
- 支持多态性:接口允许通过共同的接口来引用不同的实现,这是多态性的基础,使得代码更加灵活和通用。
- 提高代码的可读性和可理解性:接口提供了清晰的 API 视图,使得其他开发者能够更容易地理解和使用这些 API。
- 安全性:接口可以隐藏实现细节,只暴露必要的操作,这有助于保护系统的内部状态和实现不被外部直接访问。
- 遵循开闭原则:软件实体应当对扩展开放,对修改封闭。接口与实现的分离使得在不修改客户端代码的情况下扩展系统的功能。
- 促进面向对象的设计:接口与实现的分离鼓励开发者进行面向对象的设计,考虑如何将系统分解为可重用和可组合的组件。
Dao层接口设计
- 使用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生成随机数字验证码。
CaptchaUtil示例:
java
/**
* 随机验证码生成工具
*/
public class CaptchaUtil {
/**
* 生成随机验证码
*
* @param length 验证码位数
* @return
*/
public static String getCaptcha(int length){
// 自定义纯数字的验证码(随机4位数字,可重复)
RandomGenerator randomGenerator = new RandomGenerator("0123456789", length);
LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);
lineCaptcha.setGenerator(randomGenerator);
// 重新生成code
lineCaptcha.createCode();
return lineCaptcha.getCode();
}
}
前后端交互接口
- 请求 :
/verification-code/send?phoneNumber=13199999999 GET - 响应:
json
{
"code": 200,
"data": true,
"msg": ""
}
Controller层接口设计
- 接收手机号参数,调用
VerificationCodeService发送验证码,返回发送结果。
Service层接口设计
VerificationCodeService接口定义发送验证码和获取验证码的方法。VerificationCodeServiceImpl实现逻辑:- 校验手机号格式。
- 生成验证码。
- 调用
SMSUtil发送短信。 - 将验证码存入Redis,设置60秒过期时间。
VerificationCodeServiceImpl阿里云短信验证码服务类示例:
java
/**
* 阿里云短信验证码服务类
*/
@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Autowired
private SMSUtil smsUtil;
@Autowired
private RedisUtil redisUtil;
/**
* 发送并缓存验证码
*
* @param phoneNumber
*/
@Override
public void sendVerificationCode(String phoneNumber) {
// 校验手机号
if(!RegexUtil.checkMobile(phoneNumber)){
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
// 生成随机验证码 ,长度 4
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:设置字符串类型键的值(支持过期时间)。
RedisUtil示例:
java
/**
* Redis 工具缓存
*/
@Configuration
public class RedisUtil {
private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
// --------- String ---------
/**
* 设置值
*
* @param key
* @param value
* @return
*/
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;
}
}
/**
* 设置值(可设置过期时间)
*
* @param key
* @param value
* @param time 单位:秒
* @return
*/
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;
}
}
/**
* 获取值
*
* @param key
* @return
*/
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 令牌介绍
JWT 全称: JSON Web Token,官网: https://jwt.io/
JSON Web Token (JWT) 是一个开放的行业标准 (RFC 7519),用于客户端和服务器之间传递安全可靠的信息。
其本质是一个 token,是一种紧凑的 URL 安全方法。
JWT 组成: JWT 由三部分组成,每部分中间使用点 (.) 分隔,例如:aaaaa.bbbbb.cccc
-
Header (头部)
头部包括令牌的类型(即 JWT)及使用的哈希算法(如 HMAC SHA256 或 RSA)。
-
Payload (负载)
负载部分是存放有效信息的地方,里面是一些自定义内容。比如:
json{"userId":"123","userName":"zhangsan"}也可以存在 JWT 提供的现成字段,比如
exp(过期时间戳) 等。此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
-
Signature (签名)
此部分用于防止 JWT 内容被篡改,确保安全性。
防止被篡改,而不是防止被解析。
JWT 之所以安全,就是因为最后的签名。JWT 当中任何一个字符被篡改,整个令牌都会校验失败。
就好比身份证,之所以能标识一个人的身份,是因为它不能被篡改,而不是因为内容加密。(任何人都可以看到身份证的信息,JWT 也是)
> 核心工具类JWTUtil
genJwt:生成JWT令牌,包含自定义载荷(用户ID、身份)、签发时间、过期时间(1小时),使用HMAC SHA算法签名。parseJWT:解析JWT令牌,验证签名合法性,返回载荷信息。getUserIdFromToken:从令牌中提取用户ID。
JWTUtil示例:
java
public class JWTUtil {
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
/**
* 密钥:Base64编码的密钥
*/
private static final String SECRET = "********";
/**
* 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。
*/
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
Decoders.BASE64.decode(SECRET));
/**
* 过期时间(单位: 毫秒)
*/
private static final long EXPIRATION = 24*60*60*1000;
/**
* 生成密钥
*
* @param claim {"id": 12, "name":"张山"}
* @return
*/
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 {
//解析token
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
json
{
"loginName":"13199999999",
"password":"123456",
"mandatoryIdentity":"ADMIN"
}
- 密码登录响应:
json
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9...",
"identity": "ADMIN"
},
"msg": ""
}
- 验证码登录请求 :
/message/login POST
json
{
"loginMobile":"13199999999",
"verificationCode":"0475",
"mandatoryIdentity":"ADMIN"
}
- 验证码登录响应:
json
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9...",
"identity": "ADMIN"
},
"msg": ""
}
Controller层接口设计
- 提供两个登录接口,分别处理密码登录和验证码登录。
- 接收登录参数,通过
@Validated校验,调用UserService完成登录逻辑,返回UserLoginResult(含token和身份)。 - 核心类:
UserPasswordLoginParam:密码登录入参(登录名、密码、强制身份)。ShortMessageLoginParam:验证码登录入参(手机号、验证码、强制身份)。UserLoginResult:登录出参(token、身份)。
Service层接口设计
UserService接口新增login方法,支持不同登录参数类型。UserServiceImpl实现逻辑:- 区分登录类型(密码登录/验证码登录)。
- 密码登录:校验登录名格式(邮箱/手机号)、查询用户信息、校验身份、校验密码,生成JWT令牌。
- 验证码登录:校验手机号格式、查询用户信息、校验身份、校验验证码(从Redis获取),生成JWT令牌。
Dao层接口设计
UserMapper新增接口:selectByEmail:通过邮箱查询用户信息。selectByPhoneNumber:通过手机号查询用户信息。
登录页面前端实现
- 支持tab切换两种登录方式(密码登录/验证码登录)。
- 使用jQuery Validate校验表单字段(手机号、密码/验证码必填)。
- 验证码登录支持60秒倒计时重新获取。
- 登录成功后,将token和身份存入localStorage,跳转管理员首页。
四、强制登录
通过拦截器实现非登录页面的强制登录校验。例如,当用户当前尚未登陆,访问抽奖页时希望自动跳转到登录页面。
4.1 前端处理
- 所有AJAX请求通过
ajaxSend方法在请求头中添加user_token(从localStorage获取),让后端取校验。 - 若请求返回401(未登录),跳转至登录页(含iframe框架场景处理)。
4.2 后端处理
登录拦截器LoginInterceptor
实现HandlerInterceptor,重写preHandle方法:
- 从请求头获取
user_token。 - 调用
JWTUtil解析令牌,校验合法性。 - 令牌无效或不存在时,返回401状态码,拦截请求。
拦截器类示例:
java
/**
* 登录拦截器
*
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**
* 预处理,业务请求前调用
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@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;
}
}
配置类AppConfig
实现WebMvcConfigurer,配置拦截器:
- 拦截所有请求(
/**)。 - 排除登录、注册、验证码发送等接口,以及静态资源(HTML、CSS、JS等)。
拦截器排除路径示例:
java
/**
* 拦截器排除路径
*
*/
@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筛选身份) - 响应:
json
{
"code": 200,
"data": [
{
"userId": 15,
"userName": "郭靖",
"identity": "NORMAL"
},
{
"userId": 14,
"userName": "王五",
"identity": "ADMIN"
}
],
"msg": ""
}
Controller层接口设计
- 接收身份筛选参数,调用
UserService查询用户列表,转换为UserBaseInfoResult返回。
Service层接口设计
UserService新增findUserList方法,支持按身份筛选(null查询所有)。UserServiceImpl实现逻辑:查询用户列表,转换为UserDTO,包含用户ID、姓名、邮箱、手机号、身份。
Dao层接口设计
UserMapper新增selectUserList方法,支持按身份查询用户列表,按ID降序排序。
人员列表页面前端实现
- 页面加载时通过AJAX请求
/base-user/find-list接口,获取用户列表。 - 渲染表格展示用户ID、姓名、身份。
- 未登录时跳转至登录页。