什么是JWT?
JSON Web Token(JWT)是一個(gè)開放標(biāo)準(zhǔn)(RFC 7519)侣肄,它定義了一種緊湊且獨(dú)立的方式,可以在各方之間作為JSON對(duì)象安全地傳輸信息印屁。此信息可以通過數(shù)字簽名進(jìn)行驗(yàn)證和信任循捺。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對(duì)進(jìn)行簽名。
雖然JWT可以加密以在各方之間提供保密雄人,但我們將專注于簽名令牌从橘。簽名令牌可以驗(yàn)證其中包含的聲明的完整性磁餐,而加密令牌則隱藏其他方的聲明休傍。當(dāng)使用公鑰/私鑰對(duì)簽署令牌時(shí)岳掐,簽名還證明只有持有私鑰的一方是簽署私鑰的一方悟狱。
使用場(chǎng)景
特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景孕豹。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息舆床, 以便于從資源服務(wù)器獲取資源缕粹,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息登夫,該token也可直接被用于認(rèn)證很钓,也可被加密香府。
JWT是由三段信息構(gòu)成的董栽,將這三段信息文本用.鏈接一起就構(gòu)成了Jwt字符串。
格式如下:
xxxxx.yyyyy.zzzzz
就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的構(gòu)成
第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似于飛機(jī)上承載的物品)企孩,第三部分是簽證(signature)锭碳。
header 頭部
標(biāo)頭通常由兩部分組成:令牌的類型,即JWT勿璃,以及正在使用的簽名算法擒抛,例如HMAC SHA256或RSA。
這里的加密算法是單向函數(shù)散列算法补疑,常見的有MD5歧沪、SHA、HAMC癣丧。這里使用基于密鑰的Hash算法HMAC生成散列值槽畔。
MD5 message-digest algorithm 5 (信息-摘要算法)縮寫,廣泛用于加密和解密技術(shù)胁编,常用于文件校驗(yàn)厢钧。校驗(yàn)?不管文件多大嬉橙,經(jīng)過MD5后都能生成唯一的MD5值
SHA (Secure Hash Algorithm早直,安全散列算法),數(shù)字簽名等密碼學(xué)應(yīng)用中重要的工具市框,安全性高于MD5霞扬。
HMAC (Hash Message Authentication Code,散列消息鑒別碼枫振,基于密鑰的Hash算法的認(rèn)證協(xié)議喻圃。用公開函數(shù)和密鑰產(chǎn)生一個(gè)固定長(zhǎng)度的值作為認(rèn)證標(biāo)識(shí),用這個(gè)標(biāo)識(shí)鑒別消息的完整性粪滤。常用于接口簽名驗(yàn)證
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后將頭部進(jìn)行base64加密(該加密是可以對(duì)稱解密的),構(gòu)成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
payload 載荷
令牌的第二部分是有效負(fù)載斧拍,其中包含聲明。聲明是關(guān)于實(shí)體(通常是用戶)和其他數(shù)據(jù)的聲明杖小。聲明有三種類型:注冊(cè)肆汹,公開和私人。
載荷就是存放有效信息的地方予权,這些有效信息包含三個(gè)部分:
標(biāo)準(zhǔn)中注冊(cè)的聲明
公共的聲明
私有的聲明
標(biāo)準(zhǔn)中注冊(cè)的聲明 (建議但不強(qiáng)制使用) :
iss: jwt簽發(fā)者
sub: jwt所面向的用戶
aud: 接收jwt的一方
exp: jwt的過期時(shí)間昂勉,這個(gè)過期時(shí)間必須要大于簽發(fā)時(shí)間
nbf: 定義在什么時(shí)間之前,該jwt都是不可用的.
iat: jwt的簽發(fā)時(shí)間
jti: jwt的唯一身份標(biāo)識(shí)扫腺,主要用來作為一次性token,從而回避重放攻擊岗照。
公共的聲明:
公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息.但不建議添加敏感信息,因?yàn)樵摬糠衷诳蛻舳丝山饷?/p>
私有聲明是提供者和消費(fèi)者所共同定義的聲明谴返,一般不建議存放敏感信息煞肾,因?yàn)閎ase64是對(duì)稱解密的咧织,意味著該部分信息可以歸類為明文信息嗓袱。
定義一個(gè)payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后將其進(jìn)行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
請(qǐng)注意习绢,對(duì)于簽名令牌渠抹,此信息雖然可以防止被篡改,但任何人都可以讀取闪萄。除非加密梧却,否則不要將秘密信息放在JWT的有效負(fù)載或頭元素中。
signature 簽名
要?jiǎng)?chuàng)建簽名部分败去,您必須采用編碼標(biāo)頭放航,編碼的有效負(fù)載,鹽值圆裕,標(biāo)頭中指定的算法广鳍,并對(duì)其進(jìn)行簽名。
這個(gè)簽證信息由三部分組成:
header (base64后的)
payload (base64后的)
secret
這個(gè)部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串吓妆, 然后通過header中聲明的加密方式進(jìn)行加鹽secret組合加密赊时,然后就構(gòu)成了jwt的第三部分。
例如行拢,如果要使用HMAC SHA256算法祖秒,將按以下方式創(chuàng)建簽名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
javascript例子如下:
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.連接成一個(gè)完整的字符串,構(gòu)成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服務(wù)器端的,jwt的簽發(fā)生成也是在服務(wù)器端的舟奠,secret就是用來進(jìn)行jwt的簽發(fā)和jwt的驗(yàn)證竭缝, 所以,它就是你服務(wù)端的私鑰沼瘫,在任何場(chǎng)景都不應(yīng)該流露出去抬纸。一旦客戶端得知這個(gè)secret, 那就意味著客戶端是可以自我簽發(fā)jwt了。
與SpringBoot shiro集成
- 導(dǎo)入所需jar包
compile 'com.auth0:java-jwt:3.4.0'
compile 'org.apache.shiro:shiro-spring:1.4.0'
compile 'org.springframework.boot:spring-boot-starter-aop:2.0.4.RELEASE'
compile group: 'com.alibaba', name: 'druid', version: '1.1.10'
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.46'
compile group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '1.3.2'
compile group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '2.0.4.RELEASE'
使用Mybatis+Shiro做權(quán)限驗(yàn)證晕鹊,這里有一個(gè)坑要注意下松却,AOP jar包一定要導(dǎo)入,不然驗(yàn)證權(quán)限注解將失效溅话。
導(dǎo)致不會(huì)進(jìn)入doGetAuthorizationInfo()方法晓锻。
- 實(shí)現(xiàn)JWT 請(qǐng)求驗(yàn)證
主要思路
首先用戶登錄成功后,利用官方的JWT包飞几,配置生成并返回一段token砚哆,接著配置JWT的檢驗(yàn)token過濾器,讓請(qǐng)求都需要驗(yàn)證是否加上此token在請(qǐng)求頭上屑墨。
沒有則會(huì)跳到無授權(quán)躁锁。
利用JWT包纷铣,構(gòu)造生成TOKEN和檢驗(yàn)token的方法。
public class JWTUtil {
/**
* 過期時(shí)間 24 小時(shí)
*/
private static final long EXPIRE_TIME = 60 * 24 * 60 * 1000;
/**
* 密鑰战转,注意這里如果真實(shí)用到搜立,應(yīng)當(dāng)設(shè)置到復(fù)雜點(diǎn),相當(dāng)于私鑰的存在槐秧。如果被人拿到啄踊,想到于它可以自己制造token了。
*/
private static final String SECRET = "LIAODASHUAI";
/**
* 生成 token, 5min后過期
*
* @param username 用戶名
* @return 加密的token string
*/
public static String createToken(String username) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
// 附帶username信息
return JWT.create()
.withClaim("username", username)
//到期時(shí)間
.withExpiresAt(date)
//創(chuàng)建一個(gè)新的JWT刁标,并使用給定的算法進(jìn)行標(biāo)記
.sign(algorithm);
}
/**
* 校驗(yàn) token 是否正確
*
* @param token 密鑰
* @param username 用戶名
* @return 是否正確 boolean
*/
public static boolean verify(String token, String username) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//在token中附帶了username信息
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
//驗(yàn)證 token
verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 獲得token中的信息颠通,無需secret解密也能獲得
*
* @param token the token
* @return token中包含的用戶名 username
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
重新實(shí)現(xiàn)AuthenticationToken類,讓其存放token膀懈,便于校驗(yàn)顿锰。
public class JWTToken implements AuthenticationToken {
private String token;
/**
* Instantiates a new Jwt token.
*
* @param token the token
*/
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
接著到了關(guān)鍵的JWT過濾器實(shí)現(xiàn),此過濾器繼承實(shí)現(xiàn)了BasicHttpAuthenticationFilter的部分方法启搂。
主要作用是:
檢驗(yàn)請(qǐng)求頭是否帶有 token,req.getHeader("token")!=null
如果帶有 token硼控,執(zhí)行 shiro 的 login() 方法,將 token 提交到 Realm 中進(jìn)行檢驗(yàn)狐血;如果沒有 token淀歇,說明當(dāng)前狀態(tài)為游客狀態(tài)(或者其他一些不需要進(jìn)行認(rèn)證的接口)
如果在 token 校驗(yàn)的過程中出現(xiàn)錯(cuò)誤,如 token 校驗(yàn)失敗匈织,那么我會(huì)將該請(qǐng)求視為認(rèn)證不通過浪默,則重定向到 /unauthorized/**
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 如果帶有 token,則對(duì) token 進(jìn)行檢查缀匕,否則直接通過
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
//判斷請(qǐng)求的請(qǐng)求頭是否帶上 "token"
if (isLoginAttempt(request, response)) {
//如果存在纳决,則進(jìn)入 executeLogin 方法執(zhí)行登入,檢查 token 是否正確
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//token 錯(cuò)誤
responseError(response, e.getMessage());
}
}
//如果請(qǐng)求頭不存在 Token乡小,則可能是執(zhí)行登陸操作或者是游客狀態(tài)訪問阔加,無需檢查 token,直接返回 true
return true;
}
/**
* 判斷用戶是否想要登入满钟。
* 檢測(cè) header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
return token != null;
}
/**
* 執(zhí)行登陸操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
JWTToken jwtToken = new JWTToken(token);
// 提交給realm進(jìn)行登入胜榔,如果錯(cuò)誤他會(huì)拋出異常并被捕獲
getSubject(request, response).login(jwtToken);
// 如果沒有拋出異常則代表登入成功,返回true
return true;
}
/**
* 對(duì)跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求湃番,這里我們給option請(qǐng)求直接返回正常狀態(tài)
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 將非法請(qǐng)求跳轉(zhuǎn)到 /unauthorized/**
*/
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//設(shè)置編碼夭织,否則中文字符在重定向時(shí)會(huì)變?yōu)榭兆址? message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
繼承AuthorizingRealm,實(shí)現(xiàn)用戶授權(quán)的驗(yàn)證和權(quán)限的驗(yàn)證
public class CustomRealm extends AuthorizingRealm {
@Autowired
UserInfoMapper userInfoMapper;
@Autowired
RoleMapper roleMapper;
/**
* 必須重寫此方法吠撮,不然會(huì)報(bào)錯(cuò)
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 默認(rèn)使用此方法進(jìn)行用戶名正確與否驗(yàn)證尊惰,錯(cuò)誤拋出異常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份認(rèn)證方法————");
String token = (String) authenticationToken.getCredentials();
// 解密獲得username,用于和數(shù)據(jù)庫進(jìn)行對(duì)比
String username = JWTUtil.getUsername(token);
if (username == null || !JWTUtil.verify(token, username)) {
throw new AuthenticationException("token認(rèn)證失斉拧题禀!");
}
UserInfo userInfo = userInfoMapper.selectByName(username);
if (userInfo == null) {
throw new AuthenticationException("該用戶不存在!");
}
if (userInfo.getState() == 1) {
throw new AuthenticationException("該用戶已被封號(hào)膀捷!");
}
return new SimpleAuthenticationInfo(token, token, "MyRealm");
}
/**
* 只有當(dāng)需要檢測(cè)用戶權(quán)限的時(shí)候才會(huì)調(diào)用此方法迈嘹,例如checkRole,checkPermission之類的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("————權(quán)限認(rèn)證————");
String username = JWTUtil.getUsername(principals.toString());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 此處最好使用緩存提升速度
UserInfo userInfo = userInfoMapper.selectByName(username);
userInfo = userInfoMapper.selectUserOfRole(userInfo.getUid());
if (userInfo == null || userInfo.getRoleList().isEmpty()) {
return authorizationInfo;
}
for (Role role : userInfo.getRoleList()) {
authorizationInfo.addRole(role.getRole());
role = roleMapper.selectRoleOfPerm(role.getId());
if (role == null || role.getPermissions().isEmpty()) {
continue;
}
for (Permission p : role.getPermissions()) {
authorizationInfo.addStringPermission(p.getPermission());
}
}
return authorizationInfo;
}
}
配置ShiroConfig,將自定義的過濾器設(shè)置進(jìn)去
@Configuration
public class ShiroConfig {
/**
* 先走 filter 担孔,然后 filter 如果檢測(cè)到請(qǐng)求頭存在 token江锨,則用 token 去 login,走 Realm 去驗(yàn)證
*
* @param securityManager the security manager
* @return the shiro filter factory bean
*/
@Bean
public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filterMap = new HashMap<>();
//設(shè)置我們自定義的JWT過濾器
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
// 設(shè)置無權(quán)限時(shí)跳轉(zhuǎn)的 url;
factoryBean.setUnauthorizedUrl("/unauthorized/無權(quán)限");
Map<String, String> filterRuleMap = new HashMap<>();
//訪問/login和/unauthorized 不需要經(jīng)過過濾器
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/unauthorized/**", "anon");
// 所有請(qǐng)求通過我們自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 訪問 /unauthorized/** 不通過JWTFilter
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 注入 securityManager
*
* @return the security manager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設(shè)置自定義 realm.
securityManager.setRealm(customRealm());
/*
* 關(guān)閉shiro自帶的session糕篇,詳情見文檔
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
/**
* 開啟shiro aop注解支持. 使用代理方式; 所以需要開啟代碼支持;
*
* @param securityManager 安全管理器
* @return 授權(quán)Advisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
配值好后,接入swagger2,方便測(cè)試接口,配置swagger時(shí)酌心,設(shè)置一個(gè)header參數(shù)的token,方便我們調(diào)用拌消。
@Configuration
@EnableSwagger2
public class SwaggerConfig {
/**
* Create rest api docket.
*
* @return the docket
*/
@Bean
public Docket createRestApi() {
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
//header中的token參數(shù)非必填,傳空也可以
tokenPar.name("token").description("請(qǐng)求接口所需Token")
.modelRef(new ModelRef("string")).parameterType("header")
.required(false).build();
pars.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(metaData())
.select()
.apis(RequestHandlerSelectors.basePackage("com.dashuai.learning.jwt.api"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(pars);
}
private ApiInfo metaData() {
return new ApiInfoBuilder()
.title("集成JWT API文檔")
.description("描述")
.termsOfServiceUrl("")
.contact(new Contact("dashuai", "https://github.com/liaozihong", "15017263266@173.com"))
.version("1.0")
.build();
}
}
調(diào)用授權(quán)api安券,登錄成功墩崩,會(huì)返回token:
拿到返回的token,調(diào)用接口侯勉,可以看到成功調(diào)用:
去掉token或使用錯(cuò)誤的token將會(huì)報(bào)token認(rèn)證失旔谐铩:
JWT 弊端,如果使用JWT來著做會(huì)話管理址貌,那么注銷铐拐、改密、續(xù)簽等問題练对,你將要慢慢爬坑遍蟋。
具體可參考大佬寫的一篇文章:http://blog.didispace.com/learn-how-to-use-jwt-xjf/
Demo源碼:https://github.com/liaozihong/SpringBoot-Learning/tree/master/SpringBoot-JWT
參考鏈接:
JWT官方鏈接
使用JWt帶來的一些問題
https://www.xncoding.com/2017/07/09/spring/sb-jwt.html