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

参数名参数类型说明
accountString账号
passwordString密码

后端登录参数类

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();//要增加路径拦截,否则拿不到用户
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐