第5.1.4 SpringCloud JWT

JWT遵循RFC 7519,详细协议描述参见rfc7519.txt.pdf,当然有人并不看好它,比如讲真,别再使用JWT了!
先暂且搁置这个问题,毕竟我不是黑客专家,不知道安全这道门到底有多深。先看看如何利用JWT来实现单点登录。我在第4.1.2章 WEB系统最佳实践 单点登录介绍过cas的原理。简单来说有两方面:1、token认证,token的生命周期(生产、流转、销毁等环节)2、重定向,如果token认证失败,则重定向到某个url,要求接入的子系统都需要设置。
看图理解JWT如何用于单点登录,这篇文章也有介绍jwt实现单点登录,也可以看看。
1 jwt是什么
JWT全面解读、使用步骤
也可以看看国服最强JWT生成Token做登录校验讲解,看完保证你学会
以前是cookie+session的方式,难道使用了jwt就能发生革命性的变化吗?如果服务端不留存jwt,那么验证就是客户端来验证的。客户端自验证jwt,那么重要的问题,就是安全问题。
2 jwt怎么用
2.1 pom.xml
版本现在已经0.9了。

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>

2.2 统一门户登录时做了什么
关于vue的路由参见:第6.1.3 vue动态路由初探,这里说明一下jwt的身份认证

import router from './index'
import store from '../store'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '../utils/auth' // getToken from cookie
import { verify } from '@/api/login'
NProgress.configure({ showSpinner: false })// NProgress Configuration

const whiteList = ['/login', '/authredirect']// no redirect whitelist

router.beforeEach((to, from, next) => {
  NProgress.start() // start progress bar
  if (to.path === '/logout') {
    next()
  } else {
    if (getToken()) { // 判读是否有token
      let service = to.query.service
      if (service !== undefined) {
      //  存在service参数,则说明是子系统传递过来的,cas是不需要的。
        let serviceArr = JSON.parse(localStorage.getItem('service'))
        if (serviceArr !== null) {
          serviceArr.push(service.replace('login', 'logout'))
        } else {
          serviceArr = []
          serviceArr.push(service.replace('login', 'logout'))
        }
        localStorage.setItem('service', JSON.stringify(_.uniq(serviceArr)))
       // 校验token,如果校验通过,则进行页面重定向
        verify(getToken()).then(response => {
          window.location.href = service + '?ticket=' + getToken()
        }).catch(error => {
          console.log(error)
          store.dispatch('LogOut').then(() => {
            window.location.href = service
          })
        })
        return
      }
      if (to.path === '/login') {
        next({ path: '/index' })
        NProgress.done()
      } else {
        if (store.getters.user === undefined) {
          store.dispatch('GetInfo').then(info => {
            console.log(info)
            let userId = store.getters.user.id
            store.dispatch('GetQuickEntry', userId).then(info => {
              store.dispatch('GetSystem', userId).then(info => {
                next({ path: '/index' })
              })
            })
          }).catch(() => {
          })
        } else {
          next()
        }
      }
    } else {
      if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
        next()
      } else {
        next('/login') // 否则全部重定向到登录页
        NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
      }
    }
  }
})

router.afterEach(() => {
  NProgress.done() // finish progress bar
})

调试一下,发现跳转的页面是index
1
那么这个index是怎么来的呢,可以在登录代码中找到原来,登录成功后,会通过this.$router.push进行页面跳转。
2
接下来的问题,页面跳转的时候,要进行token验证,那么token是怎么产生的呢?
2.3 token产生
这里先聊一下vuex,理解vuex – vue的状态管理模式vuex是一个状态容器,vuex的action需要通过store.dispatch进行触发,
3
通过上面这段代码,就可以知道登录成功后,将token写入到cookie中了

import Cookies from 'js-cookie'

const TokenKey = 'Authorization'

export function getToken () {
  return Cookies.get(TokenKey)
}

export function setToken (token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken () {
  return Cookies.remove(TokenKey)
}

浏览器中就可以看到对应的cookie值了。
4
java后台侧用的jwt又是如何产生的呢?注意下面的代码使用的是claims记录用户身份信息,参考json web token

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
/**
     * 密钥加密token
     *
     * @param jwtInfo
     * @param priKey
     * @param expire
     * @return
     * @throws Exception
     */
    public static String generateToken(IJWTInfo jwtInfo, byte priKey[], int expire) throws Exception {
        String compactJws = Jwts.builder()
                .setSubject(jwtInfo.getUniqueName())
                .claim(CommonConstants.JWT_KEY_USER_ID, jwtInfo.getId())
                .claim(CommonConstants.JWT_KEY_NAME, jwtInfo.getName())
                .setExpiration(DateTime.now().plusSeconds(expire).toDate())
                .signWith(SignatureAlgorithm.RS256, rsaKeyHelper.getPrivateKey(priKey))
                .compact();
        return compactJws;
    }
public class JWTInfo implements Serializable,IJWTInfo {
    private String username;
    private String userId;
    private String name;

    public JWTInfo(String username, String userId, String name) {
        this.username = username;
        this.userId = userId;
        this.name = name;
    }

    @Override
    public String getUniqueName() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        JWTInfo jwtInfo = (JWTInfo) o;

        if (username != null ? !username.equals(jwtInfo.username) : jwtInfo.username != null) {
            return false;
        }
        return userId != null ? userId.equals(jwtInfo.userId) : jwtInfo.userId == null;

    }

    @Override
    public int hashCode() {
        int result = username != null ? username.hashCode() : 0;
        result = 31 * result + (userId != null ? userId.hashCode() : 0);
        return result;
    }
}

2.4 token加密
jwt的加密是顺着上面来解释的,
jwt的参数配置,expire的单位是秒,14400标识4个小时,而rsa-secret是公私钥对的密码。

jwt:
  token-header: Authorization
  expire: 14400
  rsa-secret: lm123T12^%8904645

这里用的RS256算法,但io.jsonwebtoken并不仅支持这一种,看源码,可以看到支持

HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true), 

  HS384("HS384", "HMAC using SHA-384", "HMAC", "HmacSHA384", true), 

  HS512("HS512", "HMAC using SHA-512", "HMAC", "HmacSHA512", true), 

  RS256("RS256", "RSASSA-PKCS-v1_5 using SHA-256", "RSA", "SHA256withRSA", true), 

  RS384("RS384", "RSASSA-PKCS-v1_5 using SHA-384", "RSA", "SHA384withRSA", true), 

  RS512("RS512", "RSASSA-PKCS-v1_5 using SHA-512", "RSA", "SHA512withRSA", true), 

  ES256("ES256", "ECDSA using P-256 and SHA-256", "Elliptic Curve", "SHA256withECDSA", false), 

  ES384("ES384", "ECDSA using P-384 and SHA-384", "Elliptic Curve", "SHA384withECDSA", false), 

  ES512("ES512", "ECDSA using P-512 and SHA-512", "Elliptic Curve", "SHA512withECDSA", false), 

  PS256("PS256", "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "RSA", "SHA256withRSAandMGF1", false), 

  PS384("PS384", "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "RSA", "SHA384withRSAandMGF1", false), 

  PS512("PS512", "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "RSA", "SHA512withRSAandMGF1", false);

生成 JWT (jwt-generate),这篇文章提到:

对于算法类型 HS256、HS384 和 HS512,引用的加密对象必须为“共享密钥”。
对于算法类型 RS256、RS384、RS512、ES256、ES384 和 ES512,引用的加密对象必须为“加密密钥(专用密钥)”。
加密资料可通过 JSON Web 密钥 (JWK) 提供。
如果指定了加密对象和 JWK,那么加密对象用于对 JWT 进行签名

通过在线生成非对称加密公钥私钥对,但是公钥、私钥的存储是需要加密的。安全级别高的,可以将密钥由加密机硬件分散出来。普通系统倒不能那么麻烦,生成公私钥对可以将它存储到redis中。
那么公私钥对,又是如何加载的呢?下面代码要用到两个公私钥对,如果发现redis没有,则通过程序RsaKeyHelper.generateKey(keyConfiguration.getUserSecret());产生并写入。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.Map;

@Configuration
public class AuthServerRunner implements CommandLineRunner {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    private static final String REDIS_USER_PRI_KEY = "AG:AUTH:JWT:PRI";
    private static final String REDIS_USER_PUB_KEY = "AG:AUTH:JWT:PUB";
    private static final String REDIS_SERVICE_PRI_KEY = "AG:AUTH:CLIENT:PRI";
    private static final String REDIS_SERVICE_PUB_KEY = "AG:AUTH:CLIENT:PUB";

    @Autowired
    private KeyConfiguration keyConfiguration;

    @Override
    public void run(String... args) throws Exception {
        if (redisTemplate.hasKey(REDIS_USER_PRI_KEY)&&redisTemplate.hasKey(REDIS_USER_PUB_KEY)&&redisTemplate.hasKey(REDIS_SERVICE_PRI_KEY)&&redisTemplate.hasKey(REDIS_SERVICE_PUB_KEY)) {
            keyConfiguration.setUserPriKey(RsaKeyHelper.toBytes(redisTemplate.opsForValue().get(REDIS_USER_PRI_KEY).toString()));
            keyConfiguration.setUserPubKey(RsaKeyHelper.toBytes(redisTemplate.opsForValue().get(REDIS_USER_PUB_KEY).toString()));

        } else {
            Map<String, byte[]> keyMap = RsaKeyHelper.generateKey(keyConfiguration.getUserSecret());
            keyConfiguration.setUserPriKey(keyMap.get("pri"));
            keyConfiguration.setUserPubKey(keyMap.get("pub"));
            redisTemplate.opsForValue().set(REDIS_USER_PRI_KEY, RsaKeyHelper.toHexString(keyMap.get("pri")));
            redisTemplate.opsForValue().set(REDIS_USER_PUB_KEY, RsaKeyHelper.toHexString(keyMap.get("pub")));

            redisTemplate.opsForValue().set(REDIS_SERVICE_PRI_KEY, RsaKeyHelper.toHexString(keyMap.get("pri")));
            redisTemplate.opsForValue().set(REDIS_SERVICE_PUB_KEY, RsaKeyHelper.toHexString(keyMap.get("pub")));

        }
    }
}

产生公私钥的java代码

import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

public static Map<String, byte[]> generateKey(String password) throws IOException, NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(password.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        Map<String, byte[]> map = new HashMap<String, byte[]>();
        map.put("pub", publicKeyBytes);
        map.put("pri", privateKeyBytes);
        return map;
    }

2.5 磁盘文件的方式
有的人将key写入到磁盘文件中,这样直接读文件也是一样的,就不用放在redis或者加密机了.如果是集群环境,那么这份文件在不同机器上使用同一份副本就可以了。
按照下面的代码,只需要配置Key文件存放的路径就可以了。

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.crypto.MacProvider;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.Key;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 
 * @Description KEY工具类,因为生成算法随机,所以将生成后的key保存至本地,以便加解密一致
 */
@Component
public class KeyUtil {
	@Value("${key_file_path}")
	private String filePath;
	
	public Key getkey() {
        File file=new File(filePath);
        ObjectInputStream ois = null;
        try {
	        if(!file.exists()){
	            Key key =MacProvider.generateKey(SignatureAlgorithm.HS512);
	            ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(file));
	            oo.writeObject(key);
	            oo.close();
	            return key;
	        }
	        ois = new ObjectInputStream(new FileInputStream(file));
	        Key key= (Key) ois.readObject();
            return key;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }finally{
        	try {
				ois.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
        }
    }
}
在产生access_token的代码,
```java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public String getJWTString(String username, Key key) {
		if (CheckEmptyUtil.isEmpty(username)) {
			throw new NullPointerException("用户名不能为空");
		}
		if (CheckEmptyUtil.isEmpty(key)) {
			throw new NullPointerException("密钥不能为空");
		}
		Calendar c = Calendar.getInstance();
		c.add(c.DATE, access_token_expire);
		SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
		String jwtString = Jwts.builder()
		.setIssuer("Jersey-Security-Basic")
		.setSubject(username)
		.setAudience("user")
		.setExpiration(c.getTime())//有效时长
		.setIssuedAt(new Date())
		.signWith(signatureAlgorithm, key)
		.compact();
		return jwtString;
	}

刷新token

import org.apache.oltu.oauth2.as.issuer.MD5Generator;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
public String getRefreshToken(String username) throws OAuthSystemException{
		OAuthIssuer oauthIssuer = new OAuthIssuerImpl(new MD5Generator());
		String refreshToken = oauthIssuer.refreshToken();
		try {
			redisService.putString(refreshToken, username, (long)refresh_token_expire * 24 * 3600 * 1000);
		} catch (Exception e) {
			logger.error(e.getMessage());
		}
		return refreshToken;
	}

3 JWT更新策略
JWT(JSON Web Token)自动延长到期时间,这篇文章讲了几种更新策略,只是我没看到具体的实现。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

warrah

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值