又双叒叕因为项目的需要,我去学习了用JAVA去开发一个基于jwt验证的接口,整体来说并不算太难,理清思路还是比较容易的,下面我们就开始动手吧。
1.首先在pom.xml
中添加依赖,我们用的是0.9.0的jjwt库:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.在我们的数据模型package下创建一个jwt package,创建两个类:
认证信息类AccessPara.java
public class AccessPara {
public AccessPara(String clientId,String userName,String passWord){
setClientId(clientId);
setUserName(userName);
setPassword(passWord);
}
private String clientId;
private String userName;
private String password;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
token返回结果类AccessToken.java
public class AccessToken {
private String access_token;
private String token_type;
private long expires_in;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public String getToken_type() {
return token_type;
}
public void setToken_type(String token_type) {
this.token_type = token_type;
}
public long getExpires_in() {
return expires_in;
}
public void setExpires_in(long expires_in) {
this.expires_in = expires_in;
}
}
3.在common包中添加JWT常量配置类 Constant.java
public class Constant {
public static final String JWT_ID = UUID.randomUUID().toString();
/**
* 加密密文
*/
public static final String JWT_SECRET = "jwtSecret"; //可以自己定义
public static final int JWT_TTL = 60*60*1000; //millisecond 秘钥过期时间
}
4.添加构造jwt和解析jwt的帮助类 JwtHelper.java
public class JwtHelper {
/**
* 由字符串生成加密key
*
* @return
*/
public SecretKey generalKey() {
String stringKey = Constant.JWT_SECRET;
// 本地的密码解码
byte[] encodedKey = Base64.decodeBase64(stringKey);
// 根据给定的字节数组使用AES加密算法构造一个密钥
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
*
* jti(JWT ID):是JWT的唯一标识。
* iss(Issuser):代表这个JWT的签发主体;
* sub(Subject):代表这个JWT的主体,即它的所有人;
* aud(Audience):代表这个JWT的接收对象;
* exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
* nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
* iat(Issued at):是一个时间戳,代表这个JWT的签发时间;
* 创建jwt
* @param id
* @param issuer
* @param subject
* @param ttlMillis
* @return
* @throws Exception
*/
public String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {
// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
// Map<String, Object> claims = new HashMap<>();
//claims.put("uid", "123456");
//claims.put("user_name", "admin");
//claims.put("nick_name", "X-rapido");
// 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
// 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
SecretKey key = generalKey();
// 下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
// .setClaims(claims) // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId(id) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(now) // iat: jwt的签发时间
.setIssuer(issuer) // issuer:jwt签发人
.setSubject(subject) // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥
// 设置过期时间
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* 解密jwt
*
* @param jwt
* @return
* @throws Exception
*/
public Claims parseJWT(String jwt) throws JwtException {
SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样
Claims claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(jwt).getBody(); //设置需要解析的jwt
return claims;
}
}
都有详细的注释了,我就不一一解释了。
5.接着,我们就可以在控制器中编写获取AccessToken的接口请求代码了,具体代码如下:
@Path("/getToken")
@POST
@Consumes("application/x-www-form-urlencoded")
@Produces("application/json;chartset=utf-8")
public Object getToken(@FormParam("client_id") String client_id,@FormParam("client_name") String userName)
{
AccessPara user = new AccessPara(client_id, userName, "123456");
String subject = new Gson().toJson(user);
AccessToken atEntity = new AccessToken();
try {
JwtHelper util = new JwtHelper();
String accessToken = util.createJWT(Constant.JWT_ID, "hymake", subject, Constant.JWT_TTL);
System.out.println("accessToken:" + accessToken);
atEntity.setAccess_token(accessToken);
atEntity.setExpires_in(Constant.JWT_TTL);
atEntity.setToken_type("bearer");
System.out.println("\n解密\n");
Claims c = util.parseJWT(accessToken);
System.out.println(c.getId());
System.out.println("签发时间:"+c.getIssuedAt());
System.out.println(c.getSubject());
System.out.println(c.getIssuer());
System.out.println("过期时间:"+c.getExpiration());
} catch (Exception e) {
e.printStackTrace();
}
return atEntity;
}
正常来说,jwt接口需要传入用户的业务ID,用户名和密码,因本接口需求比较简单,所以只需传入业务id和用户名区分一下即可。
上述代码的流程可以概述为以下几点:
- 创建用户对象,用来作为jwt的主体
- 创建返回实体对象,转化用户对象为json
- 通过jwtHelper 生成AccessToken字符串,放入返回实体中。
- 解析AccessToken,打印出相关的签证信息。
- 返回实体。
postman结果如下:
这样我们的AccessToken就返回成功了。
有了AccessToken,我们怎么样才能在别人请求我们项目的所有接口时,都要验证一下是否携带了正确的AccessToken呢?答案是:配置过滤器
(注:如果使用springboot + jjwt的同学,可以使用interceptor,也就是拦截器进行请求拦截,因interceptor没法拦截jersey配置后的地址,所以本项目改用过滤器Filter进行请求拦截。)
1.先列出我的接口返回实体类ResContainer.java
和返回码枚举类ResultEnum.java
public class ResContainer<T>{
private T data;
private Integer retCode;
private String retMsg;
public String getRetMsg() {
return retMsg;
}
public void setRetMsg(String retMsg) {
this.retMsg = retMsg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Integer getRetCode() {
return retCode;
}
public void setRetCode(Integer retCode) {
this.retCode = retCode;
}
}
通过泛型往data
里填充接口返回的信息,retCode
和retMsg
则通过枚举类的各种状况来赋值。
public enum ResultEnum {
SUCCESS(200,"成功"),
SUCCESS_201(201,"未查询到业务信息!"),
UNKNOWN_ERROR(-1, "未知错误"),
HEADER_ERROR(1, "头部未包含authorization或者没有bearer token信息"),
LOGIN_ERROR(2, "登录失败,用户名或密码错误"),
SIGNATURE_ERROR(3, "JWT签名与本地计算的签名不匹配,JWT的有效性不能被验证,也不应该被信任"),
TOKEN_EXP_ERROR(4,"accessToken已过期,请重新获取!"),
TOKEN_ERROR(5,"accessToken解析错误!")
;
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
2.再接着,在common下创建一个结果工具类ResultUtil.java
,用来结合我们上面的返回结果和返回码:
@Component
public class ResultUtil {
/**
* 请求成功返回
* @param object
* @return
*/
public static ResContainer success(Object object){
ResContainer ResContainer=new ResContainer();
ResContainer.setRetCode(ResultEnum.SUCCESS.getCode());
ResContainer.setRetMsg(ResultEnum.SUCCESS.getMessage());
ResContainer.setData(object);
return ResContainer;
}
public static ResContainer success_201(Object object){
ResContainer ResContainer=new ResContainer();
ResContainer.setRetCode(ResultEnum.SUCCESS_201.getCode());
ResContainer.setRetMsg(ResultEnum.SUCCESS_201.getMessage());
ResContainer.setData(object);
return ResContainer;
}
public static ResContainer success(){
return success(null);
}
public static ResContainer error(Integer code,String resultmsg){
ResContainer ResContainer=new ResContainer();
ResContainer.setRetCode(code);
ResContainer.setRetMsg(resultmsg);
return ResContainer;
}
}
3.最后,我们来看看过滤器怎么写:
@WebFilter(filterName = "JwtFilter",urlPatterns = {"/TESTAPI/*"})
public class JwtFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(ResourceApi.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
//等到请求头信息authorization信息
final String authHeader = request.getHeader("authorization");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
chain.doFilter(req, res);
} else {
if (authHeader == null || !authHeader.startsWith("Bearer")) {
toResponse(response,request,ResultEnum.HEADER_ERROR);
return;
}
String token = authHeader.substring(7);
try {
JwtHelper jwtHelper = new JwtHelper();
Claims claims = jwtHelper.parseJWT(token);
if(claims == null){
toResponse(response,request,ResultEnum.LOGIN_ERROR);
return;
}
} catch (final JwtException e) {
if(e instanceof SignatureException){
toResponse(response,request,ResultEnum.SIGNATURE_ERROR);
}else if(e instanceof ExpiredJwtException){
toResponse(response,request,ResultEnum.TOKEN_EXP_ERROR);
}else{
toResponse(response,request,ResultEnum.TOKEN_ERROR);
}
//throw new NoteException(ResultEnum.TOKEN_ERROR);
return;
}
chain.doFilter(req, res);
}
}
/**
* 响应
* @param response
* @param i 类型
* @throws IOException
*/
private void toResponse(HttpServletResponse response,HttpServletRequest request,ResultEnum re) throws IOException {
HttpServletResponse httpResponse = response;
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json; charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Origin","*");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PATCH,PUT");
httpResponse.setHeader("Access-Control-Max-Age", "3600");
httpResponse.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,x-requested-with,X-Custom-Header," +
"Content-Type,Accept,Authorization");
String method = request.getMethod();
if ("OPTIONS".equalsIgnoreCase(method)){
logger.info("OPTIONS请求");
httpResponse.setStatus(HttpServletResponse.SC_ACCEPTED);
}
ObjectMapper mapper = new ObjectMapper();
PrintWriter writer = httpResponse.getWriter();
ResultUtil resultUtil = new ResultUtil();
ResContainer resContainer = resultUtil.error(re.getCode(),re.getMessage());
writer.write(mapper.writeValueAsString(resContainer));
writer.close();
if (writer!=null)
writer = null;
}
@Override
public void destroy() {
}
主要代码在doFilter里,我们根据if判断以及Exception
的类型判断,去执行toResponse
函数返回各项jwt的异常错误码和信息。
我们来测试一下:
- 执行一个普通的接口,不包含auth头部
- 执行接口,包含一个过期的auth
- 赋予正常的token,返回正确的接口信息