第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
那么这个index是怎么来的呢,可以在登录代码中找到原来,登录成功后,会通过this.$router.push
进行页面跳转。
接下来的问题,页面跳转的时候,要进行token验证,那么token是怎么产生的呢?
2.3 token产生
这里先聊一下vuex,理解vuex – vue的状态管理模式,vuex是一个状态容器,vuex的action需要通过store.dispatch
进行触发,
通过上面这段代码,就可以知道登录成功后,将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值了。
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;
}
推荐阅读
-
详解SpringCloud服务认证(JWT)
-
SpringCloud教程 | 第11篇:分布式配置中心(Spring Cloud Config) 客户端实战
-
SpringCloud Alibaba微服务实战二十一 - JWT增强
-
SpringCloud学习笔记十:深入理解Spring Cloud Security OAuth2及JWT
-
第5.1.4 SpringCloud JWT
-
【SpringCloud微服务】第3章 服务治理SpringCloudEureka(五)——Eureka源码分析
-
【SpringCloud微服务】第2章 微服务构建 Spring Boot
-
SpringCloud Alibaba微服务实战二十一 - JWT增强