SpringBoot项目 用户登录JWT技术,登录拦截
1.JWT技术
登录使用JWT技术。
jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。
请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。
jwt 有三部分组成:A.B.C
A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定
B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。
jwt 验证,主要就是验证C部分 是否合法。
pom.xml导入依赖包:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency> <!--md5加密的依赖包-->
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
jwt工具类,一般放在utils目录包下
import com.fwind.blog.service.impl.LoginServiceImpl;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
//jwt token令牌分为 A.B.C 三部分
public class JWTUtils {
private static final String jwtToken = "123456abc@##$"; //token密钥,可自定义
//生成token
public static String createToken(Long userId){ //参数为用户Id
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));//过期时间 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
//检查token是否合法,合法则取出数据
public static Map<String, Object> checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
//以下是测试部分,可以删去
public static void main(String[] args) {
String token = JWTUtils.createToken(100l);
System.out.println(token);
Map<String, Object> map = JWTUtils.checkToken(token);
System.out.println(map.get("userId"));
}
}
2.登录注册-代码实现
前端请求参数
路径:/login
请求方式:POST
参数名 |
参数类型 |
说明 |
account |
String |
账号 |
password |
String |
密码 |
后端登录参数类
import lombok.Data;
@Data
public class LoginParam {
private String account;
private String password;
private String nickname; //注册需要的参数
}
Controller层
@RestController
@RequestMapping("/")
public class LoginController {
@Autowired
private LoginService loginService;
//用户登录
@PostMapping("login")
public Result login(@RequestBody LoginParam loginParam){
return loginService.login(loginParam);
}
//退出登录
@GetMapping("logout") //Authorization为前端请求参数,保存在头部
public Result logout(@RequestHeader("Authorization") String token){
return loginService.logout(token);
}
//注册
@PostMapping("register")
public Result register(@RequestBody LoginParam loginParam){
return loginService.register(loginParam);
}
}
Service层
@Service
@Transactional //事务:若添加用户出错(如服务器崩了),数据库不能有数据
public class LoginServiceImpl implements LoginService {
private static final String slat = "rngnb!@###"; //加密盐
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public Result login(LoginParam loginParam) {
/*
1.检查参数是否合法
2.根据用户名密码查询user是否存在
3.不存在 登陆失败
4.存在使用jwt 生成token返回给前端
5.将token放入redis中,redis token:user信息,设置过期时间
(登录认证先认证字符串是否合法,再去redis认证是否存在)
*/
String account = loginParam.getAccount();
String password = loginParam.getPassword();
if(StringUtils.isBlank(account)||StringUtils.isBlank(password)){
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
password = DigestUtils.md5Hex(password +slat); //密码加密(增加加密盐),因为数据库保存的密码经过了md5加密
SysUser user = sysUserService.findUser(account, password); //找到对应的user对象
if(user == null){ //若用户不存在,返回错误信息(自定义错误码)
return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
}
//用户存在,则创建token
String token = JWTUtils.createToken(user.getId());
//将user对象转化为Json字符串放入redis
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS); //1天过期时间
return Result.success(token); //token返回给前端
}
//检验token,主要给拦截处理器使用
@Override
public SysUser checkToken(String token) {
/*
1.token合法性校验(是否为空,解析是否成功,redis是否存在)
2.校验失败,返回错误;校验成功,返回结果LoginVo
*/
if(StringUtils.isBlank(token)){
return null;
}
//判断token是否能解析成功
Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
if(stringObjectMap == null){
return null;
}
//从rides拿到对应user json字符串
String userJSON = redisTemplate.opsForValue().get("TOKEN_" + token);
if(StringUtils.isBlank(userJSON)){
return null;
}
//把user json字符串解析为user对象
SysUser sysUser = JSON.parseObject(userJSON, SysUser.class);
return sysUser;
}
//退出登录
@Override
public Result logout(String token) {
//后端只需要清除redis的token
redisTemplate.delete("TOKEN_"+token);
return Result.success(null);
}
//注册用户
@Override
public Result register(LoginParam loginParam) {
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 user = sysUserService.findUserByAccount(account);
if(user != null){
return Result.fail(ErrorCode.ACCOUNT_HAS_EXIST.getCode(), ErrorCode.ACCOUNT_HAS_EXIST.getMsg());
}
SysUser sysUser = new SysUser();
sysUser.setNickname(nickname);
sysUser.setAccount(account);
sysUser.setPassword(DigestUtils.md5Hex(password+slat)); //密码加密
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
sysUser.setSalt("");
sysUser.setStatus("");
sysUser.setEmail("");
this.sysUserService.save(sysUser); //mybatis-plus有自己的save方法 加了this用的是自己的save方法,id会自动增加
//保存用户后,自动登录(同上)
String token = JWTUtils.createToken(sysUser.getId());
//将user对象转化为Json字符串放入redis
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
return Result.success(token); //token返回给前端
}
//以下为测试,打印md5加密后的字符串,如密码123
public static void main(String[] args) {
System.out.println(DigestUtils.md5Hex("123" + LoginServiceImpl.slat));
}
}
3.登录拦截
使用拦截器,进行登录拦截,遇到需要登录才能访问的接口时,若未登录,拦截器直接返回,并跳转登录页面。
WebMvcConfig.class
配置类,增加拦截器 ,一般在/config
包下
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) {
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域 //允许该网址(8080端口)访问本域名
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
@Override //增加拦截器
public void addInterceptors(InterceptorRegistry registry) {
/* 拦截所有路径,除了登录和注册接口 */
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login")
.excludePathPatterns("/register");
}
}
登录拦截处理类,一般放/handler
包下。
LoginInterceptor.class
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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方法(handler)之前进行执行
/**
* 1.需要判断请求的接口路径是否为HandlerMethod(controller方法)
* 2.判断token是否为空,如果为空 未登录
* 3.token不为空,登录验证 loginService checkToken
* 4.如果认证成功,放行
*/
if(!(handler instanceof HandlerMethod)){
//有可能是资源handler 如RequestResourceHandler,访问静态资源,直接放行
return true;
}
String token = request.getHeader("Authorization"); //获取浏览器头部token
//打印日志
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(StringUtils.isBlank(token)){ //检查token是否为空
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){ //检查token是否有用户信息
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result)); //未登录,传给前端信息
return false;
}
//登录成功,放行,并将用户信息保存在 本地用户类(后面有介绍)
UserThreadLocal.put(sysUser); //放入本地local
return true;
}
@Override //所以的方法都执行完之后,收尾工作
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除 ThreadLocal中用完的信息,会有内存泄露的风险
UserThreadLocal.remove();
}
}
4.本地用户信息
拦截处理后,把用户信息保存在本地,可能存在多个用户同时登录,这里采用多线程,这样互不干扰。
//保存登录后的用户信息(多线程)
public class UserThreadLocal {
private UserThreadLocal(){} //私有构造方法
//线程变量隔离(每个线程都有各自的LOCAL,互不干扰) ThreadLocal屏蔽了线程间的通讯,避免了多线程问题
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>(); //唯一初始化变量LOCAL
public static void put(SysUser sysUser){
LOCAL.set(sysUser); //将user放入
}
public static SysUser get(){
return LOCAL.get();
}
public static void remove(){
LOCAL.remove();
}
}
其他service层有逻辑需要可以直接获取本地用户,但要保证该controller层的路径增加了拦截,否则不会把用户信息保存在UserThreadLocal
,如果前面拦截了所有路径则不用考虑了。
SysUser sysUser = UserThreadLocal.get();//要增加路径拦截,否则拿不到用户