暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

简单代码实现JWT完成SSO单点登录

加耀 2019-08-02
241

 

       前两个月在公司面试过程中,发现很多求职者在简历中都写有实现过单点登录,并且使用的技术种类繁多,刚好公司项目中实现单点登录的是使用一款叫做JWT(json web token)的框架,其实现原理也挺简单的,遂想,是否自己可以用简单代码实现一个简易版的JWT来完成单点登录认证;

 

       首先,我们先来JWT官方看一下JWT的简单介绍吧;

 

       JWT的官网地址是:https://jwt.io/,我们在JWT的官网可以看到一个完整的JWT是由三个部分组成,分别是Header头部、Payload数据部分、Signature签名三部分组成;如图:


        如果将上图进行简化,JWT数据结构大抵如下

// Header
{
   
"alg": "HS256",
   
"typ": "JWT"
}
// Payload
{
   
// reserved claims
   
"sub": "1234567890",
   
"name": "John Doe",
   
"iat": "1516239022"

}
// $Signature
HS256(Base64(Header) + "." + Base64(Payload), "自定义密钥kyey")
// JWT
JWT = Base64(Header)+ "." + Base64(Payload) + "." + $Signature

 

       为了更便捷的看懂JWT的生成和认证流程,这里给画了一张简略图供参考

 

       如上图所示,根据指定的加密算法和密钥对数据信息加密得到一个签名,然后将算法、数据、签名一并使用Base64加密得到一个JWT字符串;而认证流程则是对JWT密文进行Base64解密后使用相同的算法对数据再次签名,然后将两次签名进行比较,判断数据是否有被篡改;

 

       在整体流程上,算是比较简单了;再理解JWT的生成和认证原理后,我们就可以着手开始写代码了,我们可以使用一些其它的方式来完成类似的功能,从而实现JWT类似的效果;

 

       首先,我们创建一个SpringBoot工程(方便调试不用自己写请求映射),创建好工程后,首先我们需要配置JWT的相关信息,比如:加密方式(当做是Header部分)、数据信息及token有效时间、JWT生成和认证算法等

      

       在这里,我们先定义一个枚举FailureTime,用来定义支持的过期时间策略

package cn.jiayao.myjwt.jwts.date;
/**
* 类 名: FailureTime
* 描 述:
* 作 者: 黄加耀
* 创 建: 2019/4/30 : 10:55
* 邮 箱: huangjy19940202@gmail.com
*
* @author: jiaYao
*/
public enum FailureTime {
/**
* 秒
*/
SECOND,
/**
* 分
*/
MINUTE,
/**
* 时
*/
HOUR,
/**
* 天
*/
DAY,
/**
* 永久
*/
FOREVER
}

 

      在上面的代码中,我们定义好这个jwt支持的过期时间策略有秒、分、时、天、永久五种四种类型;定义好规则后,我们再来写一个类,用来根据规则生成token相应的过期时间的工具类

package cn.jiayao.myjwt.jwts.date;
import java.util.Calendar;
import java.util.Date;
/**
* 类 名: FailureTimeUtils
* 描 述:
* 作 者: 黄加耀
* 创 建: 2019/4/30 : 11:27
* 邮 箱: huangjy19940202@gmail.com
*
* @author: jiaYao
*/
public class FailureTimeUtils {

/**
* @author: JiaYao
* @demand: 根据指定的时间规则和时间生成有效时间
* @parameters:
* @creationDate:
* @email: huangjy19940202@gmail.com
*/
public static Date creatValidTime(FailureTime failureTime, int jwtValidTime) {
Date date = new Date();
if (failureTime.name().equals(FailureTime.SECOND)) {
return createBySecond(date, jwtValidTime);
}
if (failureTime.name().equals(FailureTime.MINUTE)) {
return createBySecond(date, jwtValidTime * 60);
}
if (failureTime.name().equals(FailureTime.HOUR)) {
return createBySecond(date, jwtValidTime * 60 * 60);
}
if (failureTime.name().equals(FailureTime.DAY)) {
return getDateAfter(date, jwtValidTime);
}
if (failureTime.name().equals(FailureTime.FOREVER)){
return null;
}
return null;
}

/**
* 得到几天后的时间
*
* @param day
* @return
*/
public static Date getDateAfter(Date date, int day) {
Calendar now = Calendar.getInstance();
now.setTime(date);
now.set(Calendar.DATE, now.get(Calendar.DATE) + day);
return now.getTime();
}

/**
* 得到几天前的时间
*
* @param date
* @param day
* @return
*/
public static Date getDateBefore(Date date, int day) {
Calendar now = Calendar.getInstance();
now.setTime(date);
now.set(Calendar.DATE, now.get(Calendar.DATE) - day);
return now.getTime();
}

/**
* 得到多少秒之后的时间
*
* @param date
* @param jwtValidTime
* @return
*/
public static Date createBySecond(Date date, int jwtValidTime) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.SECOND, jwtValidTime);
return calendar.getTime();
}

}


       上面的代码中,我们定义了几个方法,分别是计算几天后的当前时间和多少秒后的当前时间;然后我们再来定义一个枚举用来定义所支持的加密算法;

package cn.jiayao.myjwt.jwts.data;
/**
* 类 名: Header
* 描 述: 定义加密算法
* 作 者: 黄加耀
* 创 建: 2019/4/30 : 9:25
* 邮 箱: huangjy19940202@gmail.com
*
* @author: jiaYao
*/
public enum Header {
SM3("sm3","国密3加密算法,其算法不可逆,类似于MD5"),
SM4("sm4","国密4加密算法,对称加密"),
AES("aes","AES加密算法,对称加密");
private String code;
private String details;
Header(String code, String details) {
this.code = code;
this.details = details;
}
}


       在上面代码中,我们定义我们这个JWT支持的加密方式有三种,分别是SM3、SM4、AES,都是属于对称加密算法;SM2是非对称加密算法(此处不做讲解);

 

       在上面我们将JWT中需要用到的数据都定义好了后,下面我们就可以开始写JWT相关的算法了,在上面的简介中,我们了解到JWT分为3个部分,分别是头部信息、数据信息、签名信息,那么我们如何来描述jwt本身呢?

 

       在现有的数据结构中,好像Map的键值队结构比较适合作为JWT的结构来存储数据;我们可以定义三个键值队来分别存储头部信息、数据信息、签名信息;在头部信息信息中我们存储一种加密算法,在数据部分,我们的value是有多个属性的,可以是一个对象,但是如果是对象的话,后期如果再需要动态的往里面添加一些个性化的数据,这个对象的字段属性好像也需要一直变动,不太好维护;

 

       那么有什么办法可以动态的像对象中添加字段从而实现存储到的信息多元化?

 

       其实没那么复杂,还是和前面一样,用一个Map就能完美解决这个问题的;也就是map中的value再存储一个map;

 

       而对于签名部分,我们对数据签名后一般都是得到的一长串加密字符,所以直接使用一个String来接收就可以了;到这里,我们需要实现的数据结构差不多可以定下来了;

      

       思路已经明确后我们就可以开始着手写代码了,代码如下所示:

package cn.jiayao.myjwt.jwts.data;
import cn.jiayao.myjwt.jwts.secret.Base64Utils;
import cn.jiayao.myjwt.jwts.secret.aes.AESUtils;
import cn.jiayao.myjwt.jwts.secret.sm3.SM3Cipher;
import cn.jiayao.myjwt.jwts.secret.sm4.SM4Util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 类 名: Jwts
* 描 述:
* 作 者: 黄加耀
* 创 建: 2019/4/30 : 11:14
* 邮 箱: huangjy19940202@gmail.com
*
* @author: jiaYao
*/
@Slf4j
public class Jwts extends ConcurrentHashMap {

private static Jwts jwts;
static {
jwts = new Jwts();
}

/**
* 默认加密密钥
*/
private final static String jwtSafetySecret = "0dcac1b6ec8843488fbe90e166617e34";
/**
* 采用默认加密算法
* @param header
* @return
*/
public static Jwts header(Header header){
return header(header,jwtSafetySecret);
}

/**
* 指定加密算法和密钥
*
* @param header
* @param jwtSafetySecret
* @return
*/
public static Jwts header(Header header, String jwtSafetySecret) {
HashMap<String, Object> map = new HashMap<>(8);
map.put("code", header);
map.put("jwtSafetySecret", StringUtils.isEmpty(jwtSafetySecret) ? Jwts.jwtSafetySecret : jwtSafetySecret);
jwts.put("header", map);
return jwts;
}

/**
* @param jwtClaims
* @return
*/
public Jwts payload(JwtClaims jwtClaims) {
jwts.put("payload", jwtClaims);
return jwts;
}

/**
* 签名并生成token
*
* @return
*/
public String compact() throws Exception {
// 头部
HashMap<String, Object> headerObj = (HashMap<String, Object>) jwts.get("header");
// 数据
JwtClaims jwtClaims = (JwtClaims) jwts.get("payload");
// uuid保证每次获取到的Token都是不同的
jwtClaims.put("uuid", UUID.randomUUID());
// 生成签名
Object jwtSafetySecretObj = headerObj.get("jwtSafetySecret");
// 从头部信息中去除密钥信息
headerObj.remove("jwtSafetySecret");
String byJwtSafetySecret = jwtSafetySecretObj == null ? jwtSafetySecret : jwtSafetySecretObj.toString();
// 开始签名
String signature = dataSignature(headerObj, jwtClaims, byJwtSafetySecret);
// 生成token
String token = Base64Utils.getBase64(JSONObject.toJSONString(headerObj)) + "."
+ Base64Utils.getBase64(JSONObject.toJSONString(jwtClaims)) + "."
+ signature;
log.info("生成的token为:" + token);
return token;
}
/**
* 生成摘要
*
* @param headerObj
* @param jwtClaims
* @param jwtSafetySecret
* @return
*/
private static String dataSignature(HashMap<String, Object> headerObj, JwtClaims jwtClaims, String jwtSafetySecret) throws Exception {
Object code = headerObj.get("code");
// 默认采用AES加密
String encryptionType = code == null ? "AES" : code.toString();
String dataSignature = null;
if (encryptionType.equals(Header.AES.name())) {
dataSignature = AESUtils.encrypt(JSONObject.toJSONString(headerObj) + JSONObject.toJSONString(jwtClaims), jwtSafetySecret);
} else if (encryptionType.equals(Header.SM3.name())) {
dataSignature = SM3Cipher.sm3Digest(JSONObject.toJSONString(headerObj) + JSONObject.toJSONString(jwtClaims), jwtSafetySecret);
} else if (encryptionType.equals(Header.SM4.name())) {
dataSignature = new SM4Util().encode(JSONObject.toJSONString(headerObj) + JSONObject.toJSONString(jwtClaims), jwtSafetySecret);
}
return dataSignature;
}
/**
* @author: JiaYao
* @demand: 校验token完整性和时效性
* @parameters:
* @creationDate:
* @email:
*/
public static Boolean safetyVerification(String tokenString, String jwtSafetySecret) throws Exception {
// 有坑,转义字符
String[] split = tokenString.split("\\.");
if (split.length != 3) {
throw new RuntimeException("无效的token");
}
// 头部信息
HashMap<String, Object> obj = JSON.parseObject(Base64Utils.getFromBase64(split[0]), HashMap.class);
// 数据信息
JwtClaims jwtClaims = JSON.parseObject(Base64Utils.getFromBase64(split[1]), JwtClaims.class);
// 签名信息
String signature = split[2];
// 验证token是否在有效期内
if (jwtClaims.get("failureTime") != null) {
Date failureTime = (Date) jwtClaims.get("failureTime");
int i = failureTime.compareTo(new Date());
if (i > 0) {
throw new RuntimeException("此token已过有效期");
}
}
// 比较签名
String signatureNew = dataSignature(obj, jwtClaims, jwtSafetySecret);
boolean flag = false;
if (!signature.equals(signatureNew.replaceAll("\r\n",""))){
signatureNew = dataSignature(obj, jwtClaims, Jwts.jwtSafetySecret);
if (signature.equals(signatureNew.replaceAll("\r\n",""))){
flag = true;
}
}else{
flag = true;
}
return flag;
}


}


       在上述的代码中,我们定义了一个静态变量jwts,由于静态变量会涉及线程安全,所以我在类Jwts上继承了ConcurrentHashMap类;在上述代码中,完成了对Header和payload签名操作,签名方法为dataSignature,然后生成一个新的token,我们通过用户请求的token进行相同规则的签名即可得到一个新的签名和token中的签名是否一致即可知道数据是否有效;

 

并且其原理和下图相似;

 

       然后在代码中我们还完成了对Token认证的操作,其方法为:safetyVerification,在方法中,我们通过对token中的三部分进行签名和比对并且完成token时效性判断(当没有配置token时效性是则表示永久有效);在这个步骤中可以有效防止数据被篡改,从而保证数据安全;

 

       对JWT加密和解密方面的核心代码大抵如此,其它的引入了一些工具类类似国密加密算法、AES算法及Base64加密算法,这些在完整代码中都有,此处就不一一展示;


        GitLab地址:https://gitlab.com/qingsongxi/myjwt.git

 

       代码结构如下图所示:


       在这里,我们需要定义一个配置文件application.properties,在配置文件中加入相关参数,比如 对称加密密钥、token有效期、需要拦截的URL等等,在配置文件中,还新增了一种在登录和未登录都能请求的url规则;

# 密钥key
jwt.safety.secret=y2W89L6BkRAFljhN
# token有效期
jwt.valid.time=7
# 需要jwt拦截的url
jwt.secret-url=/findCustomerById,/test
# 允许登录和未登录下都能请求的Url列表(这种url还需要配置到需要被jwt拦截的url中)
jwt.login-logout.requestUrl=/test
# 端口
server.port=80

 

       在这里我们需要定义一个拦截器,用来拦截需要token才能访问的URL;

package cn.jiayao.myjwt.modular.interceptor;
import cn.jiayao.myjwt.jwts.data.JwtClaims;
import cn.jiayao.myjwt.jwts.data.Jwts;
import cn.jiayao.myjwt.jwts.secret.Base64Utils;
import cn.jiayao.myjwt.modular.tools.Apistatus;
import cn.jiayao.myjwt.modular.tools.Json;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Set;
/**
* @author: JiaYao
* @demand: 自定义web拦截器
* @parameters:
* @creationDate:
* @email: huangjy19940202@gmail.com
*/
@Slf4j
@Component
public class WebInterceptor implements HandlerInterceptor , ApplicationListener<ContextRefreshedEvent> {

/**
* JWT密钥
*/
@Value("${jwt.safety.secret}")
private String jwtSafetySecret;
@Value("${jwt.login-logout.requestUrl}")
private String loginLogoutUrl;
/**
* 允许登录和未登录下都能请求的Url列表
*/
private Set<String> loginOutSet;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
// 进入拦截器 WebInterceptor...
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
// 判断是否是可以未登录也能请求的url
if (loginOutSet.contains(request.getRequestURI())) {
return true;
}
return noAccess403(response);
} else {
try {
String token = authorization.substring(7).replaceAll(" ", "");
// 验证token的完整性和有效性
if (StringUtils.isNotEmpty(token) && Jwts.safetyVerification(token, jwtSafetySecret)) {
JwtClaims jwtClaims = JSON.parseObject(Base64Utils.getFromBase64(token.split("\\.")[1]), JwtClaims.class);
request.setAttribute("claims", jwtClaims);
return true;
}
} catch (Exception e) {
e.printStackTrace();
return noAccess(response);
}
}
return false;
}

/**
* 在未登录状态或登录状态失效时请求需要登录状态才能请求的URL
*
* @param httpServletResponse
* @return
* @throws Exception
*/
public boolean noAccess(HttpServletResponse httpServletResponse) throws Exception {
httpServletResponse.setContentType("text/json; charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(Json.newInstance(Apistatus.CODE_401)));
return false;
}

/**
* 在未登录状态下请求了需要登录状态下才能请求的接口
*
* @param httpServletResponse
* @return
* @throws Exception
*/
public boolean noAccess403(HttpServletResponse httpServletResponse) throws Exception {
httpServletResponse.setContentType("text/json; charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(Json.newInstance(Apistatus.CODE_403)));
return false;
}

/**
* Spring容器创建完成后执行
*
* @param contextRefreshedEvent
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
loginOutSet = new HashSet<>(64);
String[] urls = loginLogoutUrl.replaceAll(" ", "").split(",");
for (String url : urls) {
loginOutSet.add(url);
}
}

@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object
o, ModelAndView modelAndView) throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Object o, Exception e) throws Exception {
}

}


以及

package cn.jiayao.myjwt.modular.interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @author: JiaYao
* @demand: 将拦截器添加到列表中,即观察者与被观察者
* @parameters:
* @creationDate:2018/12/19 0019 9:16
*/
@Configuration
public class WebRequestInterceptor extends WebMvcConfigurerAdapter {

@Autowired
private WebInterceptor webInterceptor;
/**
* 需要JWT拦截的Url
*/
@Value("${jwt.secret-url}")
private String jwtSecretUrl;
@Override
public void addInterceptors(InterceptorRegistry registry) {
jwtSecretUrl = jwtSecretUrl.replaceAll(" ", "");
registry.addInterceptor(webInterceptor).addPathPatterns(jwtSecretUrl.split(","));
}

}

 

       到这里,我们的JWT小工具基本上就算是已经写完了,只需要整合到具体的业务中就可以开始投入使用,下面编写一个访问控制层,在里面定义三个方法,一个是请求登录获取token,另一个是请求需要登录下才能请求的资源,还有一个就是测试再登录和未登录情况下都能访问的url;

package cn.jiayao.myjwt.modular.controller;
import cn.jiayao.myjwt.jwts.data.JwtClaims;
import cn.jiayao.myjwt.modular.service.LoginService;
import cn.jiayao.myjwt.modular.tools.Json;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 类 名: LoginController
* 描 述:
* 作 者: 黄加耀
* 创 建: 2019/4/30 : 15:40
* 邮 箱: huangjy19940202@gmail.com
*
* @author: jiaYao
*/
@Slf4j
@RestController
public class LoginController {

@Autowired
private LoginService loginService;
/**
* 登录
*
* @param customerId
* @return
*/
@GetMapping(value = "/login")
public Json login(String customerId) {
try {
return Json.newInstance(loginService.login(customerId));
} catch (Exception e) {
log.error("登录失败,错误信息{}", e.getMessage());
return Json.CODE_500;
}
}

/**
* 登录
*
* @param request
* @return
*/
@GetMapping(value = "/findCustomerById")
public Json findCustomerById(HttpServletRequest request) {
try {
String customerId = ((JwtClaims) request.getAttribute("claims")).get("id").toString();
return Json.newInstance(loginService.findCustomerById(customerId));
} catch (Exception e) {
log.error("登录失败,错误信息{}", e.getMessage());
return Json.CODE_500;
}
}

/**
* 测试登录状态下和未登录状态下的请求
* @param request
* @return
*/
@GetMapping(value = "/test")
public Json test(HttpServletRequest request){
String customerId = null;
Object claims = request.getAttribute("claims");
if (claims != null){
customerId = ((JwtClaims)claims).get("id").toString();
}
if (customerId == null){
return Json.newInstance("我在未登录的状态下请求进来了。。。。。");
}else{
return Json.newInstance("我在登录的状态下请求进来了。。。。。我的id=" + customerId);
}
}

}

 

       然后再来编写一个业务层代码

package cn.jiayao.myjwt.modular.service;
import cn.jiayao.myjwt.modular.entity.Customer;
import cn.jiayao.myjwt.modular.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 类 名: LoginService
* 描 述:
* 作 者: 黄加耀
* 创 建: 2019/4/30 : 10:52
* 邮 箱: huangjy19940202@gmail.com
*
* @author: jiaYao
*/
@Service
public class LoginService {

@Autowired
private TokenUtils tokenUtils;
/**
* 登录
*
* @param customerId
* @return
*/
public String login(String customerId) {
Customer customer = new Customer();
customer.setId(customerId);
return tokenUtils.createDefaultTokenStringSm4(customer);
}

/**
* 根据id查用户
*
* @param customerId
* @return
*/
public Customer findCustomerById(String customerId) {
Customer customer = new Customer();
customer.setId(customerId);
return customer;
}



}

 

       到这里,我们再编写一个Token工具类用于统一生成token令牌用的,代码如下:

package cn.jiayao.myjwt.modular.utils;
import cn.jiayao.myjwt.jwts.data.Header;
import cn.jiayao.myjwt.jwts.data.JwtClaims;
import cn.jiayao.myjwt.jwts.data.Jwts;
import cn.jiayao.myjwt.jwts.date.FailureTime;
import cn.jiayao.myjwt.jwts.date.FailureTimeUtils;
import cn.jiayao.myjwt.modular.entity.Customer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 类 名: TokenUtils
* 描 述:
* 作 者: 黄加耀
* 创 建: 2019/8/1 : 18:25
* 邮 箱: huangjy19940202@gmail.com
*
* @author: jiaYao
*/
@Component
public class TokenUtils {


@Value("${jwt.safety.secret}")
private String jwtSafetySecret;
@Value("${jwt.valid.time}")
private int jwtValidTime;
/**
* 使用SM4加密生成token,指定加密言值
*
* @param customer
* @return
*/
public String createTokenStringSm4(Customer customer) {
if (customer == null) throw new RuntimeException("入参不能为null");
String jwtToken = null;
try {
jwtToken = commonJwtToken(Header.SM4, jwtSafetySecret, customer);
} catch (Exception e) {
e.printStackTrace();
}
return jwtToken.replaceAll("\r\n","");
}

/**
* 使用AES生成token,指定加密言值
*
* @param customer
* @return
*/
public String createTokenStringAes(Customer customer) {
if (customer == null) throw new RuntimeException("入参不能为null");
String jwtToken = null;
try {
jwtToken = commonJwtToken(Header.AES, jwtSafetySecret, customer);
} catch (Exception e) {
e.printStackTrace();
}
return jwtToken.replaceAll("\r\n","");
}

/**
* 使用AES生成token,指定加密言值
*
* @param customer
* @return
*/
public String createTokenStringAesSm3(Customer customer) {
if (customer == null) throw new RuntimeException("入参不能为null");
String jwtToken = null;
try {
jwtToken = commonJwtToken(Header.SM3, jwtSafetySecret, customer);
} catch (Exception e) {
e.printStackTrace();
}
return jwtToken.replaceAll("\r\n","");
}

/**
* 使用Sm4生成token,使用系统默认言值
*
* @param customer
* @return
*/
public String createDefaultTokenStringSm4(Customer customer) {
if (customer == null) throw new RuntimeException("入参不能为null");
String jwtToken = null;
try {
jwtToken = commonJwtToken(Header.SM4, null, customer);
} catch (Exception e) {
e.printStackTrace();
}
return jwtToken.replaceAll("\r\n","");
}

/**
* 使用Aes生成token,使用系统默认言值
*
* @param customer
* @return
*/
public String createDefaultTokenStringAes(Customer customer) {
if (customer == null) throw new RuntimeException("入参不能为null");
String jwtToken = null;
try {
jwtToken = commonJwtToken(Header.AES, null, customer);
} catch (Exception e) {
e.printStackTrace();
}
return jwtToken.replaceAll("\r\n","");
}

/**
* 使用Sm3生成token,使用系统默认言值
*
* @param customer
* @return
*/
public String createDefaultTokenStringSm3(Customer customer) {
if (customer == null) throw new RuntimeException("入参不能为null");
String jwtToken = null;
try {
jwtToken = commonJwtToken(Header.SM3, null, customer);
} catch (Exception e) {
e.printStackTrace();
}
return jwtToken.replaceAll("\r\n","");
}

/**
* 公共代码提取
* @param header
* @param jwtSafetySecret 可为null, 会使用默认值
* @param customer
* @return
* @throws Exception
*/
private String commonJwtToken(Header header, String jwtSafetySecret, Customer customer) throws Exception {
return Jwts.header(header, jwtSafetySecret)
.payload(new JwtClaims()
.put("id", customer.getId())
.put("name", customer.getName())
.put("phone", customer.getPhone())
.put("failureTime", FailureTimeUtils.creatValidTime(FailureTime.DAY, jwtValidTime))
.put("mytest", "我的个性属性"))
.compact();
}


}

 

       就这样我们的代码部分编写完毕,我们可以通过一些demo来测试一下这个jwt;

 

       启动项目后我们通过Postman请求登录接口获取token信息,如下:

 

       如上图所示,通过请求登录接口我们成功获取到了token,我们使用这个token去请求一个需要登录才能请求的资源试试;

      

       如上所示,我们测试了一下登录获取token和使用token请求数据,都已经实现成功了;我们再来测试一下在登录和未登录情况下对同一接口的请求结果;

      

       通过上面两张图可以看出,在请求的headers中带有token时和不带token时都成功的请求到了方法,成功的实现了在登录和未登录状态下对同一接口的请求;

 

       在上面演示的过程代码中,经过拦截器后通过request请求向里面添加属性claims,将用户数据添加进来,然后进入方法后就可以直接拿到用户数据从而确定是哪个用户登录的,即使在多系统情况下,采用同样的逻辑一样是可以解析的,从而实现单点登录;

      

       在上述代码中还有一个问题是:生成的token在有效期内无法被销毁,那么就会存在一个安全问题,即用户多次登录生成多个token,但是前面生成的token还是处于有效状态,无法被及时销毁;鉴于这点,可以采用Redis缓存来解决这个问题,并且还可以实现多个系统共享Redis数据从而保证在在同一时间内只有一个有效的token或者是签名信息;

 

       可能有朋友会问,在用户数据的map中,有添加一个UUID是做什么用的,下午在测试的时候我发现对于同一个用户多次生成的token都是相同的,而Jwt(json web token) 中每次生成的都是不一样的,所以我在这里试想了一下,添加一个uuid后可以使数据部分发生变化,从而保证token的唯一性;

 

       GitLab地址:https://gitlab.com/qingsongxi/myjwt.git

        

 

      

 

 

2019年8月1日 22:38:38


文章转载自加耀,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论