注册和登录
1.接口
1.1 注册(Regisater)
1.1.1 前端请求
当用户提交注册信息时,前端将用户名(邮箱)和密码发送给后端,后端负责处理注册逻辑并发送邮箱验证链接。
前端请求参数:
- email: 用户的邮箱地址。
- password: 用户的密码。
前端请求示例:
POST /api/v1/register
Content-Type: application/json
Origin: http://localhost:8100
{
"username":"user",
"email": "user@example.com",
"password": "userpassword"
"user_type": 1
"verification_type":0
"user_id":"id" //可选
}
Origin 如果进行邮箱验证,是必填信息,通常浏览器会自动携带 user_type 根据业务需求自定义,示例中 0 匿名 1 普通用户 2 高级用户 verification_type 验证类型 0 不验证邮箱 1 验证码验证 2 链接验证 user_id是用户的id,如果从匿名接口获取了id则填写
1.1.2 后端处理逻辑
后端接收到注册请求后,创建用户,并发送邮箱验证链接。如果注册成功,返回注册成功状态,并告知前端验证邮件已发送。
后端返回示例:
{ "data": null, "ok": true, "msg": null, "code": 1 }
如果注册失败会返回
{
"code": 0,
"msg": null,
"ok": false,
"error": null,
"data": [
{
"field": "password",
"messages": [
"Failed to valiate password:00000000"
]
}
]
}
此时需要对password标红,并并显示messages中的信息
1.1.3 前端处理逻辑
前端接收到成功响应后,提示用户验证邮件已发送,并引导用户查看邮箱进行验证。
1.1.4 用户验证
用户打开邮箱,邮箱内容示例如下.链接进行验证,验证通过后 会自动跳转到下面的前端地址 正式环境中 localhost:8100 是前端的域名
Dear User,
Thank you for signing up for College Bot AI!
Please verify your email address by clicking the link below:
http://localhost:8100/verification/email?email=litongjava001@gmail.com&code=358412
This will ensure your account is secure and fully activated.
If you did not request this verification, please disregard this email.
Best regards,
The College Bot AI Team
用户打开邮箱后会点击上面的地址,前端应该使用一个路由拦截到上面的地址并获取 email 和 code 参数,并请后端发送请求确认验证码有效, 验证失败前端自行处理失败,例如显示失败信息 验证成功的前端跳转到登录地址
/api/v1/login?role={instructor|student} 统一为小写
1.2. 登录(Login)
1.2.1 前端请求
当用户提交登录信息时,前端将邮箱和密码发送到后端,后端完成身份验证并返回用户的 idToken
前端请求参数:
- email: 用户的邮箱地址。
- password: 用户的密码。
前端请求示例:
POST /api/v1/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "userpassword"
}
1.2.2 后端处理逻辑
后端进行登录验证。登录成功后,生成并返回 idToken 给前端。
后端返回参数:
- idToken: idToken。
- expires_in: 令牌过期时间,单位秒
后端返回示例(登录成功):
响应数据示例
{
"data": {
"user_id":"user_id",
"display_name": "Tong Li",
"email": "litongjava001@gmail.com",
"refresh_token":"token",
"token": "token",
"expires_in": xxx
},
"code": 1,
"msg": null,
"ok": true
}
{
"data": {
"user_id": "488388937360424960",
"display_name": "Tong Li",
"email": "litongjava001@gmail.com",
"phone": null,
"photo_url": "https://lh3.googleusercontent.com/a/ACg8ocKy5qtXCrfvGrNoyhGY_Jv8b3aF9GcBXxoAO85RVNLNg88dpCw=s96-c",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi0xLCJ1c2VySWQiOiI0ODgzODg5MzczNjA0MjQ5NjAifQ==.VaziGyn3ellkiFChfWuU0m70mybJKN-yRtPKC0Twp78",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDI0OTY2NTAsInVzZXJJZCI6IjQ4ODM4ODkzNzM2MDQyNDk2MCJ9.bKHkuneji6zlS10bWLcZT-71Hhm7aWlv11FRSahftv8",
"expires_in": 1742496650
},
"ok": true,
"code": 1,
"msg": null,
"error": null
}
1.2.3 前端处理逻辑
前端接收到 idToken 后,后续请求如果需在请求头中携带该 token:格式为
authorization: Bearer {token}
1.3 验证接口
http://localhost:8100/api/v1/verify?email=litongjava001@gmail.com&code=358412
1.4 刷新 token
携带 token
POST /api/v1/user/refresh
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi0xLCJ1c2VySWQiOiI0ODgyMzYyNzIzMzg4NzAyNzIifQ==.a2Nipd4YQc0DQ4S5lUlFVrh6Xn_GuI4qBbFS7yRddIQ"
}
{
"data": {
"user_id": "488236272338870272",
"expires_in": 604800,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjYwNDgwMCwidXNlcklkIjoiNDg4MjM2MjcyMzM4ODcwMjcyIn0=.fCbcrxdxgpbdC81wS3nPRMRyuKA0_G7iTyBfH5qOF2w"
},
"error": null,
"ok": true,
"code": 1,
"msg": null
}
1.5 登出用户
退出当前系统的登录 注意:系统采用 单 Token 设计,后端不会存储后端 token,退出登录后上次生成的 token 依旧有效,所以用户登出实际上不用调用任何接口,前端将 token 删除即可.如果考虑后续的扩展,前端可以使用该接口
GET /api/v1/logout
Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDIwODgzNjgsInVzZXJJZCI6IjQ4ODIzNjI3MjMzODg3MDI3MiJ9.liKfYdWzULDPCeKfktyKyEanZIO3Q7uhZ9mAKMfjWyM
1.6 删除用户
从系统中删除该用户
GET /api/v1/user/remove
Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDIwODgzNjgsInVzZXJJZCI6IjQ4ODIzNjI3MjMzODg3MDI3MiJ9.liKfYdWzULDPCeKfktyKyEanZIO3Q7uhZ9mAKMfjWyM
下面给出一套基于 Java(Tio-boot 框架风格)的示例实现,包含注册、登录、发送邮件以及邮箱验证码验证等接口。示例代码中保留了 SQL 建表语句和详细注释,便于理解各个步骤的逻辑。注意:以下代码仅为示例,实际项目中需要根据项目情况进行完善(如密码加盐算法、Token 生成逻辑、异常处理等)。
2. 后端代码实现
下面实现注册、登录、发送验证邮件及邮箱验证逻辑。整体采用类似 Tio-boot 的 Handler 方式实现。
2.1 注册接口
当用户提交注册请求时,系统完成用户信息存储(密码进行加盐和哈希处理),同时调用发送邮件接口生成验证码记录并发送验证链接。
2.1.1 请求对象
package com.litongjava.tio.boot.admin.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppUserRegisterRequest {
private String email;
private String password;
private int userType; // 0:匿名,1:普通, 2:高级
private int verification_type; //0 不验证邮箱 1 验证码验证 2 链接验证
private Long userId;
}
2.1.2 注册 Handler
package com.litongjava.tio.boot.admin.handler;
import java.util.ArrayList;
import java.util.List;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.model.validate.ValidateResult;
import com.litongjava.tio.boot.admin.services.AppEmailService;
import com.litongjava.tio.boot.admin.services.AppUserService;
import com.litongjava.tio.boot.admin.vo.AppUserRegisterRequest;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.tio.utils.json.Json;
import com.litongjava.tio.utils.validator.EmailValidator;
import com.litongjava.tio.utils.validator.PasswordValidator;
public class AppUserRegisterHandler {
AppUserService appUserService = Aop.get(AppUserService.class);
public HttpResponse register(HttpRequest request) {
String origin = request.getOrigin();
HttpResponse response = TioRequestContext.getResponse();
String body = request.getBodyString();
List<ValidateResult> validateResults = new ArrayList<>();
boolean ok = true;
if (StrUtil.isEmpty(origin)) {
ValidateResult validateResult = ValidateResult.by("origin", "Failed to valiate origin:" + origin);
validateResults.add(validateResult);
ok = false;
}
// 解析注册请求参数
AppUserRegisterRequest req = Json.getJson().parse(body, AppUserRegisterRequest.class);
String email = req.getEmail();
boolean validate = EmailValidator.validate(email);
if (!validate) {
ValidateResult validateResult = ValidateResult.by("eamil", "Failed to valiate email:" + email);
validateResults.add(validateResult);
ok = false;
}
String password = req.getPassword();
validate = PasswordValidator.validate(password);
if (!validate) {
ValidateResult validateResult = ValidateResult.by("password", "Failed to valiate password:" + password);
validateResults.add(validateResult);
ok = false;
}
if (!ok) {
return response.setJson(RespBodyVo.failData(validateResults));
}
boolean exists = appUserService.existsEmail(email);
if (exists) {
ValidateResult validateResult = ValidateResult.by("eamil", "eamil already taken" + email);
validateResults.add(validateResult);
}
if (!ok) {
return response.setJson(RespBodyVo.failData(validateResults));
}
boolean success = false;
Long userId = req.getUserId();
if (userId != null && appUserService.exists(userId.toString())) {
success = appUserService.registerUserByUserId(req, origin);
} else {
// 注册用户(内部会处理密码加盐和哈希等逻辑)
success = appUserService.registerUser(req.getEmail(), req.getPassword(), req.getUserType(), origin);
}
if (success) {
if (req.getVerification_type() == 1) {
// 注册成功后发送验证邮件(验证码及链接)
AppEmailService emailService = Aop.get(AppEmailService.class);
boolean sent = emailService.sendVerificationCodeEmail(req.getEmail(), origin);
if (sent) {
return response.setJson(RespBodyVo.ok());
} else {
return response.setJson(RespBodyVo.fail("Failed to send email"));
}
} else if (req.getVerification_type() == 2) {
// 注册成功后发送验证邮件(验证码及链接)
AppEmailService emailService = Aop.get(AppEmailService.class);
boolean sent = emailService.sendVerificationEmail(req.getEmail(), origin);
if (sent) {
return response.setJson(RespBodyVo.ok());
} else {
return response.setJson(RespBodyVo.fail("Failed to send email"));
}
} else {
return response.setJson(RespBodyVo.ok());
}
} else {
return response.setJson(RespBodyVo.fail());
}
}
}
2.2 登录接口
登录时前端提交邮箱和密码。后端进行密码校验后生成 Token(及 refreshToken),不再强制要求邮箱验证(即使 email_verified 为 false,也允许登录)。
2.2.1 请求对象
package com.litongjava.tio.boot.admin.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class AppUserLoginRequest {
private String email;
private String password;
}
2.2.2 返回对象
package com.litongjava.tio.boot.admin.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppUserLoginVo {
private String user_id;
private String display_name;
private String email;
private String phone;
private String photo_url;
private String refresh_token;
private String token;
private Integer expires_in;
public AppUserLoginVo(String userId, String displayName, String refreshToken, String token, int expires_in) {
this.user_id = userId;
this.display_name = displayName;
this.refresh_token = refreshToken;
this.token = token;
this.expires_in = expires_in;
}
public AppUserLoginVo(String userId, String displayName, String email, String photo_url, String refreshToken, String token, int expires_in) {
this.user_id = userId;
this.display_name = displayName;
this.email = email;
this.photo_url = photo_url;
this.refresh_token = refreshToken;
this.token = token;
this.expires_in = expires_in;
}
}
2.2.3 登录 Handler
package com.litongjava.tio.boot.admin.handler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.tio.boot.admin.services.AppUserService;
import com.litongjava.tio.boot.admin.vo.AppUser;
import com.litongjava.tio.boot.admin.vo.AppUserLoginRequest;
import com.litongjava.tio.boot.admin.vo.AppUserLoginVo;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.json.JsonUtils;
public class AppUserLoginHandler {
public HttpResponse login(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
String body = request.getBodyString();
AppUserLoginRequest req = JsonUtils.parse(body, AppUserLoginRequest.class);
AppUserService appUserService = Aop.get(AppUserService.class);
AppUser user = appUserService.getUserByEmail(req.getEmail());
// 此处允许未验证邮箱的用户登录
if (user != null && appUserService.verifyPassword(user, req.getPassword())) {
// 生成 token,有效期 7 天(604800秒)
Long timeout = EnvUtils.getLong("app.token.timeout", 604800L);
Long tokenTimeout = System.currentTimeMillis() / 1000 + timeout;
String userId = user.getId();
String token = appUserService.createToken(userId, tokenTimeout);
String refreshToken = appUserService.createRefreshToken(userId);
AppUserLoginVo appUserLoginVo = new AppUserLoginVo(userId, user.getDisplayName(), req.getEmail(), refreshToken, token, tokenTimeout.intValue());
return response.setJson(RespBodyVo.ok(appUserLoginVo));
}
return response.setJson(RespBodyVo.fail("username or password is not correct"));
}
public HttpResponse logout(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
String userId = TioRequestContext.getUserIdString();
AppUserService appUserService = Aop.get(AppUserService.class);
boolean logout = appUserService.logout(userId);
if (logout) {
response.setJson(RespBodyVo.ok());
} else {
response.setJson(RespBodyVo.fail());
}
return response;
}
}
2.3 用户接口
package com.litongjava.tio.boot.admin.handler;
import com.jfinal.kit.Kv;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.tio.boot.admin.costants.AppConstant;
import com.litongjava.tio.boot.admin.services.AppUserService;
import com.litongjava.tio.boot.admin.vo.AppUser;
import com.litongjava.tio.boot.admin.vo.UserResetPasswordRequest;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.json.FastJson2Utils;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.jwt.JwtUtils;
public class AppUserHandler {
public HttpResponse refresh(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
String bodyString = request.getBodyString();
String refresh_token = FastJson2Utils.parseObject(bodyString).getString("refresh_token");
String key = EnvUtils.getStr(AppConstant.ADMIN_SECRET_KEY);
boolean verify = JwtUtils.verify(key, refresh_token);
if (verify) {
String userId = JwtUtils.parseUserIdString(refresh_token);
AppUserService appUserService = Aop.get(AppUserService.class);
// 生成 token,有效期 7 天(604800秒)
Long timeout = EnvUtils.getLong("app.token.timeout", 604800L);
Long tokenTimeout = System.currentTimeMillis() / 1000 + timeout;
String token = appUserService.createToken(userId, tokenTimeout);
Kv kv = Kv.by("user_id", userId).set("token", token).set("expires_in", tokenTimeout.intValue());
response.setJson(RespBodyVo.ok(kv));
} else {
response.setJson(RespBodyVo.fail("Failed to validate refresh_token"));
}
return response;
}
public HttpResponse remove(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
String userIdString = TioRequestContext.getUserIdString();
AppUserService appUserService = Aop.get(AppUserService.class);
boolean ok = appUserService.remove(userIdString);
if (ok) {
response.setJson(RespBodyVo.ok());
} else {
response.setJson(RespBodyVo.fail());
}
return response;
}
public HttpResponse resetPassword(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
String bodyString = request.getBodyString();
UserResetPasswordRequest userResetPassword = JsonUtils.parse(bodyString, UserResetPasswordRequest.class);
AppUserService appUserService = Aop.get(AppUserService.class);
RespBodyVo vo = appUserService.resetPassword(userResetPassword);
return response.setJson(vo);
}
public HttpResponse profile(HttpRequest request) {
String userIdString = TioRequestContext.getUserIdString();
AppUserService appUserService = Aop.get(AppUserService.class);
AppUser user = appUserService.getUserById(userIdString);
HttpResponse response = TioRequestContext.getResponse();
return response.setJson(RespBodyVo.ok(user));
}
}
3. 服务层实现
3.1 admin_app_user.sql
--# app_users.findById
SELECT id,
email,
phone,
email_verified,
updated_profile,
display_name,
bio,
photo_url,
background_url,
phone_number,
disabled,
birthday,
coin,
invited_by_user_id,
"of",
platform,
third_platform_url,
school_id,
user_type,
provider_data,
mfa_info,
metadata,
user_info,
google_id,
google_info,
facebook_id,
facebook_info,
twitter_id,
twitter_info,
github_id,
github_info,
wechat_id,
wechat_info,
qq_id,
qq_info,
weibo_id,
weibo_info,
remark,
creator,
create_time,
updater,
update_time,
deleted,
tenant_id
FROM
app_users
WHERE
ID = ?
AND deleted = 0;
3.2 用户服务(AppUserService)
负责用户注册、密码验证、用户查询等操作。注意:此处示例使用简单的密码加密(SHA-256),实际项目中可采用更安全的加盐算法。
package com.litongjava.tio.boot.admin.services;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.codec.digest.DigestUtils;
import com.litongjava.db.activerecord.Db;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.model.validate.ValidateResult;
import com.litongjava.template.SqlTemplates;
import com.litongjava.tio.boot.admin.costants.AppConstant;
import com.litongjava.tio.boot.admin.costants.TioBootAdminTableNames;
import com.litongjava.tio.boot.admin.vo.AppUser;
import com.litongjava.tio.boot.admin.vo.AppUserRegisterRequest;
import com.litongjava.tio.boot.admin.vo.UserResetPasswordRequest;
import com.litongjava.tio.boot.admin.vo.UserToken;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.jwt.JwtUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import com.litongjava.tio.utils.validator.EmailValidator;
import com.litongjava.tio.utils.validator.PasswordValidator;
public class AppUserService {
// 查询用户基本信息(不含密码相关字段),自动映射下划线到驼峰
private static final String SQL_SELECT_USER = SqlTemplates.get("app_users.findById");
// 仅查询密码盐与哈希
private static final String SQL_SELECT_PASSWORD = "SELECT id, password_salt, password_hash FROM app_users WHERE id = ? AND deleted = 0";
public boolean registerUserByUserId(AppUserRegisterRequest req, String origin) {
String password = req.getPassword();
String email = req.getEmail();
int lastIndex = email.lastIndexOf("@");
String displayName = null;
if (lastIndex > 0) {
displayName = email.substring(0, lastIndex);
} else {
displayName = email;
}
int userType = req.getUserType();
boolean exists = Db.exists(TioBootAdminTableNames.app_users, "email", email);
if (exists) {
return true;
}
// 生成加盐字符串(示例中直接使用随机数,实际可使用更复杂逻辑)
String salt = String.valueOf(System.currentTimeMillis());
// 生成密码哈希(密码+盐)
String passwordHash = DigestUtils.sha256Hex(password + salt);
// 插入用户记录(id 这里简单采用 email 作为唯一标识)
long id = SnowflakeIdUtils.id();
String insertSql = "update app_users set display_name=?, email=?, password_salt=?, password_hash=?, user_type=?,of=? where id=?";
int rows = Db.updateBySql(insertSql, displayName, email, salt, passwordHash, userType, origin, id + "");
return rows > 0;
}
// 注册用户:先检查邮箱是否已存在,然后插入用户记录
public boolean registerUser(String email, String password, int userType, String orgin) {
boolean exists = Db.exists(TioBootAdminTableNames.app_users, "email", email);
if (exists) {
return true;
}
int lastIndex = email.lastIndexOf("@");
String displayName = null;
if (lastIndex > 0) {
displayName = email.substring(0, lastIndex);
} else {
displayName = email;
}
// 生成加盐字符串(示例中直接使用随机数,实际可使用更复杂逻辑)
String salt = String.valueOf(System.currentTimeMillis());
// 生成密码哈希(密码+盐)
String passwordHash = DigestUtils.sha256Hex(password + salt);
// 插入用户记录(id 这里简单采用 email 作为唯一标识)
long id = SnowflakeIdUtils.id();
String insertSql = "INSERT INTO app_users (id, display_name,email, password_salt, password_hash, user_type,of) VALUES (?,?,?,?,?,?,?)";
int rows = Db.updateBySql(insertSql, id + "", displayName, email, salt, passwordHash, userType, orgin);
return rows > 0;
}
// 根据邮箱获取用户信息
public AppUser getUserByEmail(String email) {
String sql = "SELECT * FROM app_users WHERE email=? AND deleted=0";
return Db.findFirst(AppUser.class, sql, email);
}
public AppUser getUserById(Long userId) {
return Db.findFirst(AppUser.class, SQL_SELECT_USER, userId);
}
public AppUser getUserById(String userIdString) {
return Db.findFirst(AppUser.class, SQL_SELECT_USER, userIdString);
}
public AppUser getUserPasswordById(Long userId) {
return Db.findFirst(AppUser.class, SQL_SELECT_PASSWORD, userId);
}
public AppUser getUserPasswordById(String userId) {
return Db.findFirst(AppUser.class, SQL_SELECT_PASSWORD, userId);
}
// 校验用户密码
public boolean verifyPassword(AppUser user, String password) {
String salt = user.getPasswordSalt();
String hash = DigestUtils.sha256Hex(password + salt);
return hash.equals(user.getPasswordHash());
}
public boolean verifyPassword(String email, String password) {
AppUser appUser = getUserByEmail(email);
return verifyPassword(appUser, password);
}
public boolean verifyPassword(Long userId, String password) {
AppUser appUser = getUserPasswordById(userId);
return verifyPassword(appUser, password);
}
public String createToken(String id, Long timeout) {
String key = EnvUtils.getStr(AppConstant.ADMIN_SECRET_KEY);
return JwtUtils.createTokenByUserId(key, id, timeout);
}
public String createRefreshToken(String id) {
String key = EnvUtils.getStr(AppConstant.ADMIN_SECRET_KEY);
return JwtUtils.createTokenByUserId(key, id, -1);
}
public boolean logout(String userId) {
return true;
}
public boolean remove(String userId) {
//String sql = "update app_users set deleted=1 WHERE id=?";
//Db.updateBySql(sql, userId);
String sql = "delete from app_users WHERE id=?";
Db.delete(sql, userId);
return true;
}
public RespBodyVo resetPassword(UserResetPasswordRequest req) {
List<ValidateResult> validateResults = new ArrayList<>();
boolean ok = true;
String email = req.getEmail();
String code = req.getCode();
boolean validate = EmailValidator.validate(email);
if (!validate) {
ValidateResult validateResult = ValidateResult.by("eamil", "Failed to valiate email:" + email);
validateResults.add(validateResult);
ok = false;
}
String password = req.getPassword();
validate = PasswordValidator.validate(password);
if (!validate) {
ValidateResult validateResult = ValidateResult.by("password", "Failed to valiate password:" + password);
validateResults.add(validateResult);
ok = false;
}
if (!ok) {
return RespBodyVo.failData(validateResults);
}
boolean exists = Db.exists(TioBootAdminTableNames.app_users, "email", email);
if (exists) {
ValidateResult validateResult = ValidateResult.by("eamil", "eamil already taken" + email);
validateResults.add(validateResult);
}
if (!ok) {
return RespBodyVo.failData(validateResults);
}
boolean verify = Aop.get(AppEmailService.class).verifyEmailCode(email, code);
if (!verify) {
return RespBodyVo.fail("Failed to verify code");
}
// 生成加盐字符串(示例中直接使用随机数,实际可使用更复杂逻辑)
String salt = String.valueOf(System.currentTimeMillis());
// 生成密码哈希(密码+盐)
String passwordHash = DigestUtils.sha256Hex(password + salt);
String updateSql = "update app_users set password_salt=?, password_hash=? where email=?";
Db.updateBySql(updateSql, salt, passwordHash, email);
return RespBodyVo.ok();
}
public RespBodyVo createAnonymousUser(String origin) {
long longId = SnowflakeIdUtils.id();
String userId = longId + "";
String insertSql = "INSERT INTO app_users (id,of,user_type) VALUES (?,?,0)";
Db.updateBySql(insertSql, userId, origin);
// 生成 token,有效期 7 天(604800秒)
Long timeout = EnvUtils.getLong("app.token.timeout", 604800L);
Long tokenTimeout = System.currentTimeMillis() / 1000 + timeout;
String token = createToken(userId, tokenTimeout);
String refreshToken = createRefreshToken(userId);
UserToken userToken = new UserToken(userId, token, tokenTimeout.intValue(), refreshToken, 0);
return RespBodyVo.ok(userToken);
}
public boolean exists(String userId) {
return Db.exists("app_users", "id", userId);
}
public boolean existsEmail(String email) {
return Db.exists(TioBootAdminTableNames.app_users, "email", email);
}
}
4. 路由配置
最后在路由配置中绑定各个接口。下面示例代码与 Tio-boot 的路由配置类似:
package com.example.app.config;
import com.example.app.handler.RegisterHandler;
import com.example.app.handler.LoginHandler;
import com.example.app.handler.EmailVerificationHandler;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.HttpRequestRouter;
public class AppHandlerConfiguration {
public void config() {
HttpRequestRouter router = TioBootServer.me().getRequestRouter();
if (router == null) {
return;
}
AppUserRegisterHandler appUserRegisterHandler = Aop.get(AppUserRegisterHandler.class);
AppUserLoginHandler loginHandler = Aop.get(AppUserLoginHandler.class);
EmailVerificationHandler emailVerificationHandler = Aop.get(EmailVerificationHandler.class);
AppUserHandler appUserHandler = Aop.get(AppUserHandler.class);
// 注册接口
r.add("/api/v1/register", appUserRegisterHandler::register);
// 登录接口
r.add("/api/v1/login", loginHandler::login);
//刷新
r.add("/api/v1/user/refresh", appUserHandler::refresh);
// 登出
r.add("/api/v1/logout", loginHandler::logout);
//删除
r.add("/api/v1/user/remove", appUserHandler::remove);
r.add("/api/v1/user/profile", appUserHandler::profile);
// 发送验证码邮件接口
r.add("/api/v1/sendVerification", emailVerificationHandler::sendVerification);
// 邮箱验证接口
r.add("/api/v1/verify", emailVerificationHandler::verifyEmail);
r.add("/verification/email", emailVerificationHandler::verifyEmail);
}
}
5.拦截器配置
package com.litongjava.tio.boot.admin.services;
import java.util.function.Predicate;
import com.litongjava.tio.boot.admin.costants.AppConstant;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.jwt.JwtUtils;
public class TokenPredicate implements Predicate<String> {
@Override
public boolean test(String token) {
String adminToken = EnvUtils.get(AppConstant.ADMIN_TOKEN);
//system token
boolean equals = adminToken.equals(token);
if (equals) {
TioRequestContext.setUserId(0L);
return true;
}
// user and admin token
String key = EnvUtils.getStr(AppConstant.ADMIN_SECRET_KEY);
boolean verify = JwtUtils.verify(key, token);
if (verify) {
String userId = JwtUtils.parseUserIdString(token);
TioRequestContext.setUserId(userId);
return true;
}
return false;
}
public Long parseUserIdLong(String token) {
String key = EnvUtils.getStr(AppConstant.ADMIN_SECRET_KEY);
boolean verify = JwtUtils.verify(key, token);
if (verify) {
return JwtUtils.parseUserIdLong(token);
}
return null;
}
}
设置 allow urls
model.addAllowUrls("/api/v1/login", "/api/v1/register", "/api/v1/user/refresh", "/api/v1/sendVerification", "/api/v1/verify", "/verification/email");
package com.litongjava.tio.boot.admin.config;
import java.util.function.Predicate;
import com.litongjava.tio.boot.admin.services.TokenPredicate;
import com.litongjava.tio.boot.http.interceptor.HttpInteceptorConfigure;
import com.litongjava.tio.boot.http.interceptor.HttpInterceptorModel;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.boot.token.AuthTokenInterceptor;
public class TioAdminInterceptorConfiguration {
private String[] permitUrls;
private boolean alloweStaticFile;
private Predicate<String> validateTokenLogic;
public TioAdminInterceptorConfiguration() {
}
public TioAdminInterceptorConfiguration(String[] permitUrls) {
this.permitUrls = permitUrls;
}
public TioAdminInterceptorConfiguration(String[] permitUrls, Predicate<String> validateTokenLogic) {
this.permitUrls = permitUrls;
this.validateTokenLogic = validateTokenLogic;
}
public TioAdminInterceptorConfiguration(String[] permitUrls, boolean b) {
this.permitUrls = permitUrls;
this.alloweStaticFile = b;
}
public void config() {
// 创建 SaToken 拦截器实例
if (validateTokenLogic == null) {
validateTokenLogic = new TokenPredicate();
}
AuthTokenInterceptor authTokenInterceptor = new AuthTokenInterceptor(validateTokenLogic);
HttpInterceptorModel model = new HttpInterceptorModel();
model.setInterceptor(authTokenInterceptor);
model.addBlockUrl("/**"); // 拦截所有路由
// index
model.addAllowUrls("", "/");
//user
model.addAllowUrls("/register/*", "/api/login/account", "/api/login/outLogin"); // 设置例外路由
model.addAllowUrls("/api/v1/login", "/api/v1/register", "/api/v1/user/refresh", "/api/v1/sendVerification", "/api/v1/verify", "/verification/email");
model.addAllowUrls("/api/event/add");
String[] previewUrls = { "/table/json/tio_boot_admin_system_article/get/*",
//
"/table/json/tio_boot_admin_system_docx/get/*", "/table/json/tio_boot_admin_system_pdf/get/*" };
model.addAllowUrls(previewUrls);
if (permitUrls != null) {
model.addAllowUrls(permitUrls);
}
model.setAlloweStaticFile(alloweStaticFile);
HttpInteceptorConfigure inteceptorConfigure = new HttpInteceptorConfigure();
inteceptorConfigure.add(model);
// 将拦截器配置添加到 Tio 服务器
TioBootServer.me().setHttpInteceptorConfigure(inteceptorConfigure);
}
}
8.邮件发送
lark.mail.host=smtp.larksuite.com
lark.mail.protocol=smtp
lark.mail.smpt.port=465
lark.mail.user=noreply@myget.ai
lark.mail.password=1111
lark.mail.from=no-reply
emails\register_mail.txt
Dear User,
Thank you for signing up for College Bot AI!
Please verify your email address by clicking the link below:
#(link)
This will ensure your account is secure and fully activated.
If you did not request this verification, please disregard this email.
Best regards,
The College Bot AI Team
com.litongjava.tio.boot.admin.config.TioAdminLarkSuitMailConfig.TioAdminLarkSuitMailConfig()
new TioAdminLarkSuitMailConfig().config();
package com.litongjava.myget.config;
import com.litongjava.annotation.AConfiguration;
import com.litongjava.annotation.Initialization;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.myget.email.MyGetEmailSender;
import com.litongjava.tio.boot.admin.config.*;
import com.litongjava.tio.boot.admin.handler.SystemFileTencentCosHandler;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.HttpRequestRouter;
@AConfiguration
public class AdminAppConfig {
@Initialization
public void config() {
// 配置数据库相关
new TioAdminDbConfiguration().config();
new TioAdminRedisDbConfiguration().config();
new TioAdminMongoDbConfiguration().config();
new TioAdminInterceptorConfiguration().config();
new TioAdminHandlerConfiguration().config();
new TioAdminLarkSuitMailConfig().config();
// 获取 HTTP 请求路由器
TioBootServer server = TioBootServer.me();
HttpRequestRouter r = server.getRequestRouter();
if (r != null) {
// 获取文件处理器,并添加文件上传和获取 URL 的接口
SystemFileTencentCosHandler systemUploadHandler = Aop.get(SystemFileTencentCosHandler.class);
r.add("/api/system/file/upload", systemUploadHandler::upload);
r.add("/api/system/file/url", systemUploadHandler::getUrl);
}
server.setEmailSender(new MyGetEmailSender());
// 配置控制器
new TioAdminControllerConfiguration().config();
}
}
package com.litongjava.myget.email;
import com.jfinal.kit.Kv;
import com.jfinal.template.Template;
import com.litongjava.myget.utils.MailJetUtils;
import com.litongjava.template.EmailEngine;
import com.litongjava.tio.boot.admin.mail.LarkSuitEmailUtils;
import com.litongjava.tio.boot.email.EmailSender;
import com.mailjet.client.MailjetResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyGetEmailSender implements EmailSender {
@Override
public boolean send(String to, String subject, String content) {
return false;
}
@Override
public boolean sendVerificationEmail(String email, String origin, String code) {
Template template = EmailEngine.getTemplate("register_mail.txt");
String link = origin + "/verification/email?email=%s&code=%s";
link = String.format(link, email, code);
String content = template.renderToString(Kv.by("link", link));
// 从邮箱地址中提取用户名作为收件人姓名
String name = email.split("@")[0];
String subject = "College Bot AI Email Verification";
// 发送 HTML 格式的邀请邮件
return sendWithLark(name, email, subject, content);
//return sendWithMailjet(name, email, subject,content);
}
private boolean sendWithLark(String name, String email, String subject, String content) {
try {
LarkSuitEmailUtils.send(email, subject, content);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
private boolean sendWithMailjet(String name, String email, String subject, String content) {
try {
MailjetResponse response = MailJetUtils.sendText(name, email, subject, content);
int status = response.getStatus();
if (status == 200) {
return true;
} else {
log.error(response.getRawResponseContent());
return false;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
9.基本配置
admin.secret.key=123456
admin.token=123456
app.token.timeout=604800
10. 总结
- 注册:前端提交邮箱、密码和用户类型,后端完成用户记录创建(包含密码加盐与哈希),并调用邮件服务发送验证码。
- 登录:前端提交邮箱和密码,后端验证后生成 idToken(以及 refreshToken),即使邮箱未验证也允许登录。
- 邮箱验证码发送与验证:提供独立接口用于发送验证码邮件和验证验证码;验证成功后更新用户表中的邮箱验证状态。
以上示例实现了用户名密码注册和登录,并包含邮箱验证码发送和验证逻辑。所有代码均保留了原始逻辑和必要注释,方便直接参考和二次开发。