JSON Web Token(JWT)机制
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sxt</groupId>
<artifactId>sso-jwt</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<!-- JWT核心依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<!-- java开发JWT的依赖jar包。 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- 给springmvc提供的响应扩展。@ResponseBody -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
JWTSubject.java
package com.sxt.sso.commons;
/**
* 作为Subject数据使用。也就是payload中保存的public claims
* 其中不包含任何敏感数据
* 开发中建议使用实体类型。或BO,DTO数据对象。
*/
public class JWTSubject {
private String username;
public JWTSubject() {
super();
}
public JWTSubject(String username) {
super();
this.username = username;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
JWTUtils.java
package com.sxt.sso.commons;
import java.util.Date;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
/**
* JWT工具
*/
public class JWTUtils {
// 服务器的key。用于做加解密的key数据。 如果可以使用客户端生成的key。当前定义的常亮可以不使用。
private static final String JWT_SECERT = "test_jwt_secert" ;
private static final ObjectMapper MAPPER = new ObjectMapper();
public static final int JWT_ERRCODE_EXPIRE = 1005;//Token过期
public static final int JWT_ERRCODE_FAIL = 1006;//验证不通过
public static SecretKey generalKey() {
try {
// byte[] encodedKey = Base64.decode(JWT_SECERT);
// 不管哪种方式最终得到一个byte[]类型的key就行
byte[] encodedKey = JWT_SECERT.getBytes("UTF-8");
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 签发JWT,创建token的方法。
* @param id jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
* @param iss jwt签发者
* @param subject jwt所面向的用户。payload中记录的public claims。当前环境中就是用户的登录名。
* @param ttlMillis 有效期,单位毫秒
* @return token, token是一次性的。是为一个用户的有效登录周期准备的一个token。用户退出或超时,token失效。
* @throws Exception
*/
public static String createJWT(String id,String iss, String subject, long ttlMillis) {
// 加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 当前时间。
long nowMillis = System.currentTimeMillis();
// 当前时间的日期对象。
Date now = new Date(nowMillis);
SecretKey secretKey = generalKey();
// 创建JWT的构建器。 就是使用指定的信息和加密算法,生成Token的工具。
JwtBuilder builder = Jwts.builder()
.setId(id) // 设置身份标志。就是一个客户端的唯一标记。 如:可以使用用户的主键,客户端的IP,服务器生成的随机数据。
.setIssuer(iss)
.setSubject(subject)
.setIssuedAt(now) // token生成的时间。
.signWith(signatureAlgorithm, secretKey); // 设定密匙和算法
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis); // token的失效时间。
builder.setExpiration(expDate);
}
return builder.compact(); // 生成token
}
/**
* 验证JWT
* @param jwtStr
* @return
*/
public static JWTResult validateJWT(String jwtStr) {
JWTResult checkResult = new JWTResult();
Claims claims = null;
try {
claims = parseJWT(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) { // token超时
checkResult.setErrCode(JWT_ERRCODE_EXPIRE);
checkResult.setSuccess(false);
} catch (SignatureException e) { // 校验失败
checkResult.setErrCode(JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}
/**
*
* 解析JWT字符串
* @param jwt 就是服务器为客户端生成的签名数据,就是token。
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody(); // getBody获取的就是token中记录的payload数据。就是payload中保存的所有的claims。
}
/**
* 生成subject信息
* @param subObj - 要转换的对象。
* @return java对象->JSON字符串出错时返回null
*/
public static String generalSubject(Object subObj){
try {
return MAPPER.writeValueAsString(subObj);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
}
JWTResponseData.java
package com.sxt.sso.commons;
/**
* 发送给客户端的数据对象。
* 商业开发中,一般除特殊请求外,大多数的响应数据都是一个统一类型的数据。
* 统一数据有统一的处理方式。便于开发和维护。
*/
public class JWTResponseData {
private Integer code;// 返回码,类似HTTP响应码。如:200成功,500服务器错误,404资源不存在等。
private Object data;// 业务数据
private String msg;// 返回描述
private String token;// 身份标识, JWT生成的令牌。
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
JWTResult.java
package com.sxt.sso.commons;
import io.jsonwebtoken.Claims;
/**
* 结果对象。
*/
public class JWTResult {
/**
* 错误编码。在JWTUtils中定义的常量。
* 200为正确
*/
private int errCode;
/**
* 是否成功,代表结果的状态。
*/
private boolean success;
/**
* 验证过程中payload中的数据。
*/
private Claims claims;
public int getErrCode() {
return errCode;
}
public void setErrCode(int errCode) {
this.errCode = errCode;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Claims getClaims() {
return claims;
}
public void setClaims(Claims claims) {
this.claims = claims;
}
}
JWTController.java
package com.sxt.sso.controller;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.sxt.sso.commons.JWTResponseData;
import com.sxt.sso.commons.JWTResult;
import com.sxt.sso.commons.JWTSubject;
import com.sxt.sso.commons.JWTUsers;
import com.sxt.sso.commons.JWTUtils;
@Controller
public class JWTController {
@RequestMapping("/testAll")
@ResponseBody
public Object testAll(HttpServletRequest request){
String token = request.getHeader("Authorization");
JWTResult result = JWTUtils.validateJWT(token);
JWTResponseData responseData = new JWTResponseData();
if(result.isSuccess()){
responseData.setCode(200);
responseData.setData(result.getClaims().getSubject());
// 重新生成token,就是为了重置token的有效期。
String newToken = JWTUtils.createJWT(result.getClaims().getId(),
result.getClaims().getIssuer(), result.getClaims().getSubject(),
1*60*1000);
responseData.setToken(newToken);
return responseData;
}else{
responseData.setCode(500);
responseData.setMsg("用户未登录");
return responseData;
}
}
@RequestMapping("/login")
@ResponseBody
public Object login(String username, String password){
JWTResponseData responseData = null;
// 认证用户信息。本案例中访问静态数据。
if(JWTUsers.isLogin(username, password)){
JWTSubject subject = new JWTSubject(username);
String jwtToken = JWTUtils.createJWT(UUID.randomUUID().toString(), "sxt-test-jwt",
JWTUtils.generalSubject(subject), 1*60*1000);
responseData = new JWTResponseData();
responseData.setCode(200);
responseData.setData(null);
responseData.setMsg("登录成功");
responseData.setToken(jwtToken);
}else{
responseData = new JWTResponseData();
responseData.setCode(500);
responseData.setData(null);
responseData.setMsg("登录失败");
responseData.setToken(null);
}
return responseData;
}
}
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript">
function login(){
var username = $("#username").val();
var password = $("#password").val();
var params = "username="+username+"&password="+password;
$.ajax({
'url' : '${pageContext.request.contextPath }/login',
'data' : params,
'success' : function(data){
if(data.code == 200){
var token = data.token;
// web storage的查看 - 在浏览器的开发者面板中的application中查看。
// local storage - 本地存储的数据。 长期有效的。
// session storage - 会话存储的数据。 一次会话有效。
var localStorage = window.localStorage; // 浏览器提供的存储空间。 根据key-value存储数据。
localStorage.token = token;
}else{
alert(data.msg);
}
}
});
}
function setHeader(xhr){ // XmlHttpRequest
xhr.setRequestHeader("Authorization",window.localStorage.token);
}
function testLocalStorage(){
$.ajax({
'url' : '${pageContext.request.contextPath}/testAll',
'success' : function(data){
if(data.code == 200){
window.localStorage.token = data.token;
alert(data.data);
}else{
alert(data.msg);
}
},
'beforeSend' : setHeader
});
}
</script>
</head>
<body >
<center>
<table>
<caption>登录测试</caption>
<tr>
<td style="text-align: right; padding-right: 5px">
登录名:
</td>
<td style="text-align: left; padding-left: 5px">
<input type="text" name="username" id="username"/>
</td>
</tr>
<tr>
<td style="text-align: right; padding-right: 5px">
密码:
</td>
<td style="text-align: left; padding-left: 5px">
<input type="text" name="password" id="password"/>
</td>
</tr>
<tr>
<td style="text-align: right; padding-right: 5px" colspan="2">
<input type="button" value="登录" onclick="login();" />
</td>
</tr>
</table>
</center>
<input type="button" value="testLocalStorage" onclick="testLocalStorage();"/>
</body>
</html>