1. 注册
1.1 接口说明
接口url:/register
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
account | string | 账号 |
password | string | 密码 |
nickname | string | 昵称 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
1.2 Controller
package com.cherriesovo.blog.controller;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.vo.Result;
import com.cherriesovo.blog.vo.params.LoginParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("register")
public class RegisterController {
@Autowired
private LoginService loginService;
@PostMapping
public Result register(@RequestBody LoginParam loginParam){
//sso单点登录,后期如果把登陆注册功能提出去(单独的服务 可以独立提供接口服务)
return loginService.register(loginParam);
}
}
参数LoginParam类中 添加新的参数nickname。
package com.cherriesovo.blog.vo.params;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
@Data
public class LoginParam {
private String account;
private String password;
private String nickname;
}
1.3 Service
package com.cherriesovo.blog.service;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.vo.Result;
public interface SysUserService {
//根据账户查询用户
SysUser findUserByAccount(String account);
//保存用户
void save(SysUser sysUser);
}
package com.cherriesovo.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.cherriesovo.blog.dao.mapper.SysUserMapper;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.service.SysUserService;
import com.cherriesovo.blog.utils.JWTUtils;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.LoginUserVo;
import com.cherriesovo.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findUserByAccount(String account) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
//SELECT * FROM sys_user WHERE account = '指定的account' LIMIT 1
queryWrapper.eq(SysUser::getAccount,account);
queryWrapper.last("limit 1");
return sysUserMapper.selectOne(queryWrapper);
}
@Override
public void save(SysUser sysUser) {
//注意 保存用户的id会自动生成 默认生成的id 是分布式id 采用了雪花算法
//采用框架提供的insert()插入数据
this.sysUserMapper.insert(sysUser);
}
}
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser), 1, TimeUnit.DAYS);
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
key
: Redis 中的键值,这里是以 “TOKEN_” 开头加上用户的 token。value
: 要存储的值,这里是用户信息对象sysUser
转换成的 JSON 字符串,使用了JSON.toJSONString()
方法。timeout
: 过期时间,这里设置为 1。timeUnit
: 过期时间单位,这里设置为TimeUnit.DAYS
,表示一天。这段代码的作用是将用户信息存入 Redis 中,并设置了一天的过期时间。通常这样做是为了实现用户登录状态的持久化,以及实现一定的缓存机制。
package com.cherriesovo.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.service.SysUserService;
import com.cherriesovo.blog.utils.JWTUtils;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.Result;
import com.cherriesovo.blog.vo.params.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
@Transactional
public class LoginServiceImpl implements LoginService {
@Override
public Result register(LoginParam loginParam) {
/*
* 1、判断参数是否合法
* 2、判断账户是否存在,存在则返回账户已被注册
* 3、如果账户不存在,注册用户
* 4、生成token
* 5、存入redis并返回
* 6、注意 加上事务,一旦中间出现任何问题,需要回滚
* */
//判断参数
String account = loginParam.getAccount();
String password = loginParam.getPassword();
String nickname = loginParam.getNickname();
if (StringUtils.isBlank(account)
|| StringUtils.isBlank(password)
|| StringUtils.isBlank(nickname)
){
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
//判断账户是否存在,存在则返回账户已被注册
SysUser sysUser = sysUserService.findUserByAccount(account);
if (sysUser != null){
return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),ErrorCode.ACCOUNT_EXIST.getMsg());
}
//如果账户不存在,注册用户
sysUser = new SysUser();
sysUser.setNickname(nickname);
sysUser.setAccount(account);
sysUser.setPassword(DigestUtils.md5Hex(password+slat)); //将密码与盐值拼接后进行 MD5 加密,并设置为用户的密码。
sysUser.setCreateDate(System.currentTimeMillis());
sysUser.setLastLogin(System.currentTimeMillis());
sysUser.setAvatar("/static/img/logo.b3a48c0.png");
sysUser.setAdmin(1); //1 为true
sysUser.setDeleted(0); // 0 为false,设置用户是否被删除,这里将其设置为 0,代表未删除。
sysUser.setSalt("");//设置用户的盐值为空字符串,这里是一个占位符,实际应用中根据需要生成盐值
sysUser.setStatus("");
sysUser.setEmail("");
this.sysUserService.save(sysUser);
//通过用户id生成token
String token = JWTUtils.createToken(sysUser.getId());
//使用 RedisTemplate 将用户信息以 JSON 格式存入 Redis 中,并设置了过期时间为一天
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
return Result.success(token);
}
}
//ErrorCode类中添加
ACCOUNT_EXIST(10004,"账号已存在"),
1.4 加事务
@Service
@Transactional
public class LoginServiceImpl implements LoginService {}
当然 一般建议加在 接口上,通用一些。
测试的时候 可以将redis 停掉,那么redis连接异常后,新添加的用户 应该执行回滚操作。
1.5 测试
2. 登录拦截器
每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
2.1 拦截器实现
public class LoginInterceptor implements HandlerInterceptor
:
LoginInterceptor
类实现了HandlerInterceptor
接口,拦截请求并在进入 Controller 方法之前进行处理。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
:这是拦截器中的
preHandle
方法,用于在请求到达 Controller 方法之前进行处理。让我解释这个方法的参数和作用:
Object handler
: 表示被拦截的处理器对象,通常是一个 Controller 方法。在这个方法中,我们可以对请求进行拦截、处理和转发。boolean
: 方法的返回类型为布尔值,用于指示是否允许请求继续执行。如果返回true
,则请求将继续执行后续的拦截器或进入 Controller 方法;如果返回false
,则请求将被拦截,不会继续执行后续的拦截器或进入 Controller 方法。if (!(handler instanceof HandlerMethod)){ //handler可能是RequestResourceHandler springboot程序访问静态资源默认去classpath下的static目录去查询 return true; }
这段代码用于检查
handler
是否为HandlerMethod
的实例,如果不是,则认为是请求静态资源,直接放行。
HandlerMethod
是 Spring MVC 中用于处理请求的处理器方法的封装类。通常情况下,Controller 中的方法会被包装成HandlerMethod
实例。RequestResourceHandler
是 Spring Boot 默认用于处理静态资源的处理器。当请求的路径匹配到静态资源时,会由RequestResourceHandler
处理。因此,这段代码的逻辑是:如果
handler
不是HandlerMethod
的实例,即不是 Controller 中的处理器方法,则认为是请求静态资源,直接放行,不进行拦截处理。
package com.cherriesovo.blog.handler;
import com.alibaba.fastjson.JSON;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j //日志
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//该方法在执行controller方法之前执行
/*
* 1、需要判断请求的接口路径是否为HandlerMethod(controller方法)
* 2、判断token是否为空,如果为空 未登录
* 3、如果token不为空,登录验证loginService checkToken
* 4、如果认证成功,放行即可
* */
if (!(handler instanceof HandlerMethod)){
//handler可能是RequestResourceHandler springboot程序访问静态资源默认去classpath下的static目录去查询
return true;
}
String token = request.getHeader("Authorization");
//打印日志
log.info("=================request start===========================");
String requestURI = request.getRequestURI(); //获取uri,即客户端请求的资源路径
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}", token);
log.info("=================request end===========================");
if (token == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8"); //设置 HTTP 响应的内容类型为 JSON 格式
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//至此是登录状态,放行
return true;
}
}
2.2 使拦截器生效
package com.cherriesovo.blog.config;
import com.cherriesovo.blog.handler.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
//允许http://localhost:8080访问所有端口
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口。后续遇到需要拦截的接口时再进行配置
registry.addInterceptor(loginInterceptor).addPathPatterns("/test");
}
}
2.3 测试
package com.mszlu.blog.controller;
import com.mszlu.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping
public Result test(){
return Result.success(null);
}
}
3. ThreadLocal保存用户信息
ThreadLocal 是 Java 提供的一个线程局部变量类,它允许我们在每个线程中存储和获取各自的值,而不会被其他线程共享。每个线程都拥有自己独立的 ThreadLocal 实例,可以在其中存储数据,这些数据对其他线程是不可见的。
ThreadLocal 的主要特点包括:
- 线程隔离:每个线程都拥有自己独立的 ThreadLocal 对象实例,通过该实例可以存储和获取线程私有的数据。
- 数据共享:在同一个线程内部,ThreadLocal 可以在多个方法之间共享数据,而不需要通过参数传递或全局变量。
- 线程安全:由于每个线程拥有自己的 ThreadLocal 实例,因此对 ThreadLocal 的操作是线程安全的,不会受到其他线程的干扰。
- 高效性:ThreadLocal 使用线程的 ThreadLocalMap 存储数据,底层是一个数组结构,查找速度快,不会引起线程间的竞争。
ThreadLocal 的常见用途包括:
- 保存用户上下文信息:可以在 Web 请求处理过程中将用户信息存储在 ThreadLocal 中,便于后续的业务处理方法获取用户信息,而不必每次都去查询数据库。
- 避免参数传递:可以在同一个线程的不同方法之间共享数据,避免参数传递的复杂性。
- 线程安全的日期格式化:可以使用 ThreadLocal 存储日期格式化对象,确保在多线程环境下的安全使用。
需要注意的是,由于 ThreadLocal 是与线程绑定的,因此在使用完 ThreadLocal 后需要及时清理,以避免内存泄漏。通常在线程结束时,或在合适的时机调用
remove()
方法进行清理。
定义了一个
UserThreadLocal
类,用于在当前线程中存储和获取用户信息。
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
:声明一个
ThreadLocal
对象,用于保存用户信息。ThreadLocal
是一个线程局部变量,可以在每个线程中存储各自的值,而不会被其他线程共享。
public static void put(SysUser sysUser)
: 将传入的sysUser
对象存储在当前线程的ThreadLocal
中。
public static SysUser get()
: 从当前线程的ThreadLocal
中获取存储的SysUser
对象。
public static void remove()
: 从当前线程的ThreadLocal
中移除存储的SysUser
对象。
package com.cherriesovo.blog.utils;
import com.cherriesovo.blog.dao.pojo.SysUser;
public class UserThreadLocal {
private UserThreadLocal(){}
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
public static void put(SysUser sysUser){
LOCAL.set(sysUser);
}
public static SysUser get(){
return LOCAL.get();
}
public static void remove(){
LOCAL.remove();
}
}
package com.cherriesovo.blog.handler;
import com.alibaba.fastjson.JSON;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.service.LoginService;
import com.cherriesovo.blog.utils.UserThreadLocal;
import com.cherriesovo.blog.vo.ErrorCode;
import com.cherriesovo.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j //日志
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//该方法在执行controller方法之前执行
/*
* 1、需要判断请求的接口路径是否为HandlerMethod(controller方法)
* 2、判断token是否为空,如果为空 未登录
* 3、如果token不为空,登录验证loginService checkToken
* 4、如果认证成功,放行即可
* */
if (!(handler instanceof HandlerMethod)){
//handler可能是RequestResourceHandler springboot程序访问静态资源默认去classpath下的static目录去查询
return true;
}
String token = request.getHeader("Authorization");
log.info("=================request start===========================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}", token);
log.info("=================request end===========================");
if (token == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//至此是登录状态,放行
//我希望在controller中直接获取用户信息,怎么获取
UserThreadLocal.put(sysUser);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除,Threadlocal中用完的信息会有内存泄漏的风险
UserThreadLocal.remove();
}
}
package com.cherriesovo.blog.controller;
import com.cherriesovo.blog.dao.pojo.SysUser;
import com.cherriesovo.blog.utils.UserThreadLocal;
import com.cherriesovo.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping
public Result test(){
// SysUser
SysUser sysUser = UserThreadLocal.get();
System.out.println(sysUser);
return Result.success(null);
}
}