JWT令牌登入

一、JWT令牌

1、什么是JWT令牌

JWT是JSON Web Token的缩写,即JSON Web令牌,是一种自包含令牌。
JWT的使用场景:

  • 一种情况是webapi,类似之前的阿里云播放凭证的功能
  • 另一种情况是多web服务器下实现无状态分布式身份验证
    • JWT官网有一张图描述了JWT的认证过程
      在这里插入图片描述

JWT的作用:

  • JWT 最重要的作用就是对 token信息的防伪作用

JWT的原理:

  • 一个JWT由三个部分组成:JWT头、有效载荷、签名哈希
  • 最后由这三者组合进行base64编码得到JWT

2、JWT令牌的组成

典型的,一个JWT看起来如下图:
https://jwt.io/
在这里插入图片描述

该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。

每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名

Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。

作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/“和”=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。

注意:base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。

3、JWT的用法

客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。

此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。

当跨域时,也可以将JWT放置于POST请求的数据主体中。

二、JWT问题和趋势

1、JWT默认不加密,但可以加密。生成原始令牌后,可以使用该令牌再次对其进行加密。

2、当JWT未加密时,一些私密数据无法通过JWT传输。

3、JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。

4、JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。

5、JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行身份验证。

6、为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

三、落地实现

1. 基本依赖

   <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>

2.在application.yaml文件中添加jwt相关的配置

jwt:
  secret: tuling-portal #服务端私钥
  expiration: 86400 #过期时间 60*60*24 = 一天
  tokenHead: Bearer #jwt规范  #告诉客户端jwt令牌开头需要加的一个字符串
  tokenHeader: Authorization #告诉客户端你要在请求头里面传什么参数名字

secure:
  ignored:
    urls: #安全路径白名单
      - /swagger-ui.html
      - /swagger-resources/**
      - /swagger/**
      - /**/v2/api-docs
      - /**/*.js
      - /**/*.css
      - /**/*.png
      - /**/*.ico
      - /webjars/springfox-swagger-ui/**
      - /actuator/**
      - /druid/**
      - /error
      - /login/**
      - /register/**
      - /user/**

3. 添加jwt加密解密 工具类

package cn.ecut.lrj.web.util;

import cn.ecut.lrj.web.uns.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken生成的工具类
 * JWT token的格式:header.payload.signature
 * header的格式(算法、token的类型):
 * {"alg": "HS512","type": "JWT"}
 * payload的格式(用户名、创建时间、生成时间):
 * {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature的生成算法:
 * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 */
@Component
public class JwtTokenUtil2 {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil2.class);
    private static final String CLAIM_KEY_USERNAME = "user_name";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;


    /**
     * 根据负责生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}",token);
        }
        return claims;
    }

    /**
     *  生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 解密: 从token中获取登录用户名(项目中使用)
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =  claims.get(CLAIM_KEY_USERNAME,String.class);
        } catch (Exception e) {
            username = null;
        }
        return username;
    }
    /**
     * 加密:根据用户信息生成token(项目中使用)
     */
    public String generateToken(String userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    /*public boolean validateToken(String token, User userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getLoginAct()) && !isTokenExpired(token);
    }*/

    /**
     * 判断token是否已经失效
     */
    public boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }



    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
}


4. 客户端发起登入请求

axios.post(
          'http://localhost:8989/user/login',
          {
            "loginAct": this.user.loginAct,
            'loginPwd': this.user.loginPwd
          },{headers: {'Content-Type': 'application/json'}})
          .then((res)=>{

		  })

5. 服务端验证成功后加密token,把token返回给客户端

在controller层验证登入成功后进行加密

package cn.ecut.lrj.web.uns.controller;


import cn.ecut.lrj.web.api.CommonResult;
import cn.ecut.lrj.web.uns.model.User;
import cn.ecut.lrj.web.uns.service.UserService;
import cn.ecut.lrj.web.util.JwtTokenUtil2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
@CrossOrigin
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtTokenUtil2 jwtTokenUtil2;

    @Value("${jwt.tokenHead}")
    private String tokenHead; //#jwt规范  #告诉客户端jwt令牌开头需要加的一个字符串

    @Value("${jwt.tokenHeader}") //#告诉客户端你要在请求头里面传什么参数名字
    private String tokenHeader;


    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public CommonResult login(@RequestBody User user) {
       
        User login = userService.login(user.getLoginAct(), user.getLoginPwd());
        if (login == null) {
            return CommonResult.validateFailed("用户名或密码错误");
        }


        //jwt加密
        Map<String, String> tokenMap = new HashMap<>();
        String token = jwtTokenUtil2.generateToken(login.getLoginAct());
        tokenMap.put("token",token);
        tokenMap.put("tokenHead",tokenHead);
        tokenMap.put("tokenHeader",tokenHeader);
        return CommonResult.success(tokenMap);
    }


}

6.客户端接收拼接tokenHead+ jwt 存入cookie

axios.post(
          'http://localhost:8989/user/login',
          {
            "loginAct": this.user.loginAct,
            'loginPwd': this.user.loginPwd
          },{headers: {'Content-Type': 'application/json'}})
          .then((res)=>{

            console.log(res.data.data)
            let result = res.data.data
           /* this.$cookie.set('token',result.tokenHead+' '+result.token,{expires:'1M'});*/

            setCookie("token",result.tokenHead+' '+result.token,{expires:'1M'}) //cookie自己定义(安装一个cookie库,或定义)

            // 拿到payloader 解码

            var tokenStr= decodeURIComponent(escape(window.atob(result.token.split('.')[1])));

            // 转换为json对象
            let username = JSON.parse(tokenStr).user_name;
            setCookie("username",username,120);


            this.$router.push("/success")


          })

setcookie(),getcookie()定义

export function setCookie(name,value)
{
    var Days = 30;
    var exp = new Date();
    exp.setTime(exp.getTime() + Days*24*60*60*1000);
    document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
}
export function getCookie(name)
{
    var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
    return (arr=document.cookie.match(reg))?unescape(arr[2]):null;
}
export function delCookie(name)
{
    var exp = new Date();
    exp.setTime(exp.getTime() - 1);
    var cval=getCookie(name);
    if(cval!=null)
        document.cookie= name + "="+cval+";expires="+exp.toGMTString();
}

7. 后续客户端所有的axios请求都需要携带cookie中token放入请求头Authorization中

// axios的拦截器   jwt+spring security
axios.interceptors.request.use(config => {
  // jwt令牌
  var token= getCookie("token");
  window.console.log(token);
  if (token !=undefined) {
    config.headers['Authorization'] = token; // 让每个请求携带自定义token 请根据实际情况自行修改
  } 
  return config
}, error => {
  // Do something with request error
  Promise.reject(error)
})

8. 服务器拦截到请求头中的Authorization 解密jwt .从而拿到username 查询 是否存在用户

  • 在拦截器中进行验证token是否正确、失效
  • 拦截器配置文件
package cn.ecut.lrj.web.config;

import cn.ecut.lrj.web.Interceptor.AuthInterceptor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AuthInterceptorConfig implements WebMvcConfigurer {

    /**
     * 该拦截器主要是为了权限验证
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor()).addPathPatterns("/**");
    }


    /**
     * 
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = "secure.ignored")//获取配置文件中的白名单urls
    public AuthInterceptor authInterceptor(){
        return new AuthInterceptor();
    }


}

  • 拦截器
package cn.ecut.lrj.web.Interceptor;

import cn.ecut.lrj.web.api.ResultCode;
import cn.ecut.lrj.web.exception.ApiException;
import cn.ecut.lrj.web.uns.model.User;
import cn.ecut.lrj.web.uns.service.UserService;
import cn.ecut.lrj.web.util.JwtTokenUtil2;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Slf4j
public class AuthInterceptor  implements HandlerInterceptor {

    private List<String> urls;

    @Autowired
    private JwtTokenUtil2 jwtTokenUtil2;

    @Autowired
    private UserService userService;


    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;


    public List<String> getUrls() {
        return urls;
    }

    public void setUrls(List<String> urls) {
        this.urls = urls;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、不需要登录就可以访问的路径——白名单
        // 获取当前请求   /admin/login
        log.info("拦截开始");
        String requestURI = request.getRequestURI();
        log.info("requestURI-->"+requestURI);
        // Ant方式路径匹配 /**  ?  _
        PathMatcher matcher = new AntPathMatcher();
        for (String ignoredUrl : urls) {
            if(matcher.match(ignoredUrl,requestURI)){
                return  true;
            }
        }
        //拿到jwt令牌
        String jwt = request.getHeader(tokenHeader);

        System.out.println("jwt令牌: "+jwt);

        //判断是否存在 判断开头是否加了tokenHead
        if (StrUtil.isBlank(jwt) || !jwt.startsWith(tokenHead)){
            throw new ApiException(ResultCode.UNAUTHORIZED);
        }
        //存在就进行解密
        jwt=jwt.substring(tokenHead.length());
        String userName = jwtTokenUtil2.getUserNameFromToken(jwt);


        if (StrUtil.isBlank(userName)){
            throw new ApiException(ResultCode.UNAUTHORIZED);
        }

        /*//将userName存入ThreadLocal中(方便下面获取当前用户)
        JwtTokenUtil2.currentMember.set(userName);*/

        //得到userName就进行查询用户是否存在和 token是否过期
        User member = userService.getAdminByUsername(userName);

        boolean result = jwtTokenUtil2.isTokenExpired(jwt);

        if (!result){
            throw new ApiException(ResultCode.UNAUTHORIZED);
        }



        return true;

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值