CREATE TABLE `tb_user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号码',
`password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '密码,加密存储',
`nick_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '昵称,默认是用户id',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '人物头像',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uniqe_key_phone` (`phone`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1011 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
com.baomidou
mybatis-plus-boot-starter
3.4.3
mysql
mysql-connector-java
runtime
8.0.33
org.projectlombok
lombok
true
cn.hutool
hutool-all
5.7.17
@MapperScan("com.liang.mapper")
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: #配置自己的数据库url
username: #配置自己的数据库用户名
password: #配置自己的密码
/**
* 登录信息
*/
@Data
public class LoginFormDTO {
private String phone;
private String code;
}
/**
* 统一结果返回
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;
public static Result ok(){
return new Result(true, null, null, null);
}
public static Result ok(Object data){
return new Result(true, null, data, null);
}
public static Result ok(List> data, Long total){
return new Result(true, null, data, total);
}
public static Result fail(String errorMsg){
return new Result(false, errorMsg, null, null);
}
}
/**
* User实体类 对应数据库表tb_user
*/
@Data
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 手机号码
*/
private String phone;
/**
* 密码,加密存储
*/
private String password;
/**
* 昵称,默认是随机字符
*/
private String nickName;
/**
* 用户头像
*/
private String icon = "";
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
/**
* 存储用户非敏感信息
*/
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
/**
* User对象前端控制器
*/
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/**
* 发送手机验证码
* @param phone 手机号
* @param session
* @return
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone, session)?Result.ok():Result.fail("手机号码不合规");
}
/**
* 登录功能
* @param loginForm
* @param session
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
return userService.login(loginForm, session) ? Result.ok() : Result.fail("手机号或验证码错误");
}
public interface IUserService extends IService {
boolean sendCode(String phone, HttpSession session);
boolean login(LoginFormDTO loginForm, HttpSession session);
}
@Service
public class UserServiceImpl extends ServiceImpl implements IUserService {
@Override
public boolean sendCode(String phone, HttpSession session) {
return true;
}
@Override
public boolean login(LoginFormDTO loginForm, HttpSession session) {
return true;
}
}
@Override
public boolean sendCode(String phone, HttpSession session) {
//获取手机号,验证手机号是否合规
boolean mobile = PhoneUtil.isMobile(phone);
//不合规,则提示
if (!mobile){
return false;
}
//生成验证码
String code = RandomUtil.randomNumbers(6);
//将验证码保存到session中
session.setAttribute("code",code);
//发送验证码
System.out.println("验证码:" + code);
return true;
}
(将用户信息存储在session中,主要是方便后序获取当前登录信息)
@Override
public boolean login(LoginFormDTO loginForm, HttpSession session) {
//获取手机号
String phone = loginForm.getPhone();
//验证手机号是否合理
boolean mobile = PhoneUtil.isMobile(phone);
//如果不合理 提示
if (!mobile){
//提示用户手机号不合理
return false;
}
//手机号合理 进行验证码验证
String code = loginForm.getCode();
String sessionCode = session.getAttribute("code").toString();
//如果验证码输入的是错误的 提示
if (!code.equals(sessionCode)){
return false;
}
//如果验证码也正确 那么通过手机号进行查询
User user = this.getOne(new LambdaQueryWrapper().eq(User::getPhone, phone));
// 数据库中没查询到用户信息
if (ObjectUtil.isNull(user)){
user = new User();
user.setPhone(phone);
user.setNickName("user_"+ RandomUtil.randomString(10));
this.save(user);
}
// 将该用户信息存入session中
// 简化user,只存储必要信息以及不重要的信息
UserDTO userDTO = BeanUtil.toBean(user, UserDTO.class);
session.setAttribute("user", userDTO);
return true;
}
自定义拦截器,实现HandlerInterceptor接口
public class LoginInterceptor implements HandlerInterceptor {
/**
* preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
UserDTO user = (UserDTO) session.getAttribute("user");
//判断是否在session中获取到了用户
if (ObjectUtil.isNull(user)){
return false;
}
UserHolder.saveUser(user);
return true;
}
/**
* postHandle方法在控制层方法执行后,视图解析前执行(可以在这里修改控制层返回的视图和模型)
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* fterCompletion方法在视图解析完成后执行,多用于释放资源
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
实现WebMvcConfigurer接口,通过重写addInterceptors方法添加自定义拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加拦截器
registry.addInterceptor(new LoginInterceptor())
//放行资源
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
)
// 设置拦截器优先级
.order(1);
}
}
注意隐藏用户敏感信息:
我们应当在返回用户信息之前,将用户敏感信息进行隐藏,采用的核心思路就是创建UserDTO类,该类没有用户敏感信息,在返回用户信息之前,将有用户敏感新的的User对象转换为没有敏感信息的UserDTO对象,就可以有效的避免用户信息被泄露的问题。
客户端发送请求,通过nginx负载均衡到下游的tomcat服务器(一台4核8G的tomcat服务器,在优化和处理简单业务的加持下,处理的并发量很有限),经过nginx负载均衡分流后,利用集群支撑整个项目,同时nginx在部署了前端项目后,做到了动静分离,进一步降低tomcat的压力,如果让tomcat直接访问mysql,一般16、32核CPU、32/64G内存,并发量在4k~7K左右,在高并发场景下也是容易崩溃,所有一般会使用mysql集群,同时为了进一步降低mysql压力,增加访问性能,一般会加入redis集群,以提供更好地服务。
org.springframework.boot
spring-boot-starter-data-redis
spring:
redis:
host: 192.168.175.128
port: 6379
password: liang
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
/**
* 保存验证码的redis中的key
*/
public static final String LOGIN_CODE_KEY = "login:code:";
/**
* 验证码的过期时间
*/
public static final Long LOGIN_CODE_TTL = 2L;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public boolean sendCode(String phone, HttpSession session) {
//获取手机号,验证手机号是否合规
boolean mobile = PhoneUtil.isMobile(phone);
//不合规,则提示
if (!mobile){
return false;
}
//生成验证码
String code = RandomUtil.randomNumbers(6);
//保存验证码到redis,并设置过期时间
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
//发送验证码,这里就通过打印验证码模拟了下发送验证码
System.out.println("验证码:" + code);
return true;
}
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
String uuid = userService.sendCode(phone, session);
return uuid.equals("") ? Result.fail("手机号码不合规"): Result.ok(uuid);
}
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 30L;
@Override
public String login(LoginFormDTO loginForm, HttpSession session) {
//获取手机号
String phone = loginForm.getPhone();
//验证手机号是否合理
boolean mobile = PhoneUtil.isMobile(phone);
//如果不合理 提示
if (!mobile){
//提示用户手机号不合理
return "";
}
//手机号合理 进行验证码验证
String code = loginForm.getCode();
//从redis中获取验证码
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
//如果验证码输入的是错误的 提示
if (!code.equals(redisCode)){
return "";
}
//如果验证码也正确 那么通过手机号进行查询
User user = this.getOne(new LambdaQueryWrapper().eq(User::getPhone, phone));
// 数据库中没查询到用户信息
if (ObjectUtil.isNull(user)){
user = new User();
user.setPhone(phone);
user.setNickName("user_"+ RandomUtil.randomString(10));
this.save(user);
}
// 将用户信息保存到Redis中,注意避免保存用户敏感信息
UserDTO userDTO = BeanUtil.toBean(user, UserDTO.class);
// 设置UUID保存用户信息
String uuid = IdUtil.fastSimpleUUID();
// 将user对象转化为Map,同时将Map中的值存储为String类型的
Map userDTOMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().ignoreNullValue()
.setFieldValueEditor((key, value) -> value.toString()));
stringRedisTemplate.opsForHash().putAll( LOGIN_USER_KEY + uuid, userDTOMap);
//设置过期时间
stringRedisTemplate.expire(LOGIN_USER_KEY + uuid, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 通过UUID生成简单的token
String token = uuid + userDTO.getId();
return token;
}
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
String token = userService.login(loginForm, session);
return StrUtil.isNotBlank(token) ? Result.ok(token) : Result.fail("手机号或验证码错误");
}
private StringRedisTemplate stringRedisTemplate;
/**
* 构造函数
* @param stringRedisTemplate
*/
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从请求头中获取token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
return false;
}
String uuid = token.substring(0,token.lastIndexOf("-"));
System.out.println(uuid);
//从redis中获取值
Map
因为设置了redis中存储的用户的有效期,所以在用户访问界面的时,需要更新token令牌的存活时间,例如修改LoginInterceptor拦截器,在此拦截器中刷新过期时间
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从请求头中获取token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
return false;
}
String uuid = token.substring(0,token.lastIndexOf("-"));
System.out.println(uuid);
//从redis中获取值
Map
但是需要注意的是,自定义的登录拦截器只是针对需要登录访问的请求进行了拦截,如果用户访问没被拦截的请求,该拦截器不会生效,则token令牌不能进行更新,当用户长时间访问不需要登录的页面,token令牌失效,再去访问被拦截的请求,则需要重新登录,这是不合理的。所有我们还需要在定义一个拦截器,进行token令牌刷新。
/**
* 刷新令牌的拦截器
* @author liang
*/
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate redisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从请求头中获取token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
return false;
}
String uuid = token.substring(0, token.lastIndexOf("-"));
//从Redis中获取值
Map
public class LoginInterceptor implements HandlerInterceptor {
/**
* preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserDTO user = UserHolder.getUser();
return ObjectUtil.isNotNull(user);
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
StringRedisTemplate stringRedisTemplate;
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate));
registry.addInterceptor(new LoginInterceptor())
//放行资源
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
)
// 设置拦截器优先级
.order(1);
}
}