登陆

简略代码完结JWT(json web token)完结SSO单点登录

admin 2019-06-07 272人围观 ,发现0个评论

本文作者:加耀              

投稿来历:

https://zhuanlan.zhihu.com/p/64377462?utm_source=wechat_session&utm_medium=social&utm_oi=775841244587773952


运用JWT完结SSO单点登录

前两个月在公司面试过程中,发现许多求职者在简历中都写有完结过单点登录,而且运用的技能品种繁复,刚好公司项目中完结单点登录的是运用一款叫做JWT(json web token)的结构,其完结原理也挺简略的,遂想,是否自己能够用简略代码完结一个简易版的JWT来完结单点登录认证(SSO),所谓SSO单点登录,其实是指的一类处理方案,有许多种办法都能够完结,这儿描绘的JWT便是其间一种;

首要,咱们先来JWT官方看一下JWT的简略介绍吧;

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

img

假如将上图进行简化,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的生成和认证流程,这儿给画了一张简略图供参阅

img

如上图所示,依据指定的加密算法和密钥对数据信息加密得到一个签名,然后将算法、数据、签名同时运用Base64加密得到一个JWT字符串;而认证流程则是对JWT密文进行Base64解密后运用相同的算法对数据再次签名,然后将两简略代码完结JWT(json web token)完结SSO单点登录次签名进行比较,判别数据是否有被篡改

在全体流程上,算是比较简略了;再了解JWT的生成和认证原理后,咱们就能够着手开端写代码了,咱们能够运用一些其它的办法来完结相似的功用,然后完结JWT相似的作用;

首要,咱们创立一个SpringBoot工程(便利调试不必自己写恳求映射),创立好工程后,首要咱们需求装备JWT的相关信息,比方:加密办法(作为是Header部分)、数据信息及token有用时刻、JWT生成和认证算法等

在这儿,咱们先界说一个枚举FailureTime,用来界说支撑的过期时刻战略

public enum FailureTime {
    /**
     * 秒
     */

    SECOND,
    /**
     * 分
     */

    MINUTE,
    /**
     * 时
     */

    HOUR,
    /**
     * 天
     */

    DAY

}

在上面的代码中,咱们界说好这个jwt支撑的过期时刻战略有秒、分、时、天四种四品种型;界说好规矩后,咱们再来写一个类,用来依据规矩生成token相应的过期时刻的东西类

public class FailureTimeUtils {

    /**
     * @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);
        }
        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();
    }

    /**
     * 得到多少秒之后的时刻
     *
   &简略代码完结JWT(json web token)完结SSO单点登录nbsp; * @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();
    }

}

上面的代码中,咱们界说了几个办法,分别是核算几天后的当时时刻和多少秒后的当时时刻;然后咱们再来界说一个枚举用来界说所支撑的加密算法;

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对错对称加密算法(此处不做解说);

下面再界说记载用户数据的部分,咱们创立一个JwtClaims 用来存储咱们需求保存到JWT中的特性数据,代码如下

import java.util.HashMap;
im末世火线系统port java.util.Objects;

public class JwtClaims extends HashMap {
    public JwtClaims() {
        this.put(ID, null);
        this.put(NAME, null);
        this.put(PHONE, null);
        this.put(FAILURETIME, null);
    }
    String ID = "id";
    String NAME = "name";
    String PHONE = "phone";
    /**
     * 有用期
     */

    String FAILURETIME = "failureTime";
    public JwtClaims put(String key, Object value) {
        super.put(key, value);
        return this;
    }
    /**
     * 重写hashCode办法
     *
     * @return
     */

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), this);
    }
}

在上面咱们将JWT中需求用到的数据都界说好了后,下面咱们就能够开端写JWT相关的算法了,代码如下所示:

@Slf4j
public class Jwts extends ConcurrentHashMap {

    private static Jwts jwts;

    static {
        jwts = new Jwts();
    }

    /**
     * 默许加密密钥
     */

    private final String jwtSafetySecret = "0dcac1b6ec8843488fbe90e166617e34";

    /**
     * 指定加密算法和密钥
     *
     * @param header
     * @param jwtSafetySecret
     * @return
     */

    public&n简略代码完结JWT(json web token)完结SSO单点登录bsp;static Jwts header(Header header, String jwtSafetySecret) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("code", header);
        map.put("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");
        jwtClaims.put("uuid", UUID.randomUUID());
        // 生成签名
        Object jwtSafetySecretObj = headerObj.get("jwtSafetySecret");
        // 从头部信息中去除密钥信息
        headerObj.remove("jwtSafetySecret");
        String jwtSafetySecret = jwtSafetySecretObj == null ? this.jwtSafetySecret : jwtSafetySecretObj.toString();
        Object code = headerObj.get("code");
        String encryptionType = code == null ? "AES" : code.toString();
        // 开端签名
        String signature = dataSignature(headerObj, jwtClaims, encryptionType, jwtSafetySecret);
        // 生成token
        String token = Base64Utils.getBase64(JSONObject.toJSONString(headerObj)) + "."
                + Base64Utils.getBase64(JSONObject.toJSONString(jwtClaims)) + "."
                + signature;
        System.out.println("生成的token为:" + token);
        return token;
    }
    /**
     * 生成摘要
     */

    private static String dataSignature(HashMap<String, Object> headerObj, JwtClaims jwtClaims, String encryptionType, String jwtSafetySecret) throws Exception {
        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;
    }
    /**
     * @demand: 校验token完好性和时效性   
     */

    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已过有用期");
            }
        }

        // 验证数据篡改
        Object code = obj.get("code");
        String encryptionType = code == null ? "AES" : code.toString();
        // 比较签名
        String signatureNew = dataSignature(obj, jwtClaims, encryptionType, jwtSafetySecret);
        return signature.equals(signatureNew.replaceAll("\r\n","")) ? true : false;
    }

}

在上述的代码中,咱们界说了一个静态变量jwts,此处触及线程安全,暂时先不调整,后期再做优化;在上述代码中,完结了对Header和payload签名操作,然后生成一个新的token,其原理和下图相似;

img

然后在代码中咱们还完结了对Token认证的操作,其办法为:safetyVerification,在办法中,咱们经过对token中的三部分进行签名和比对而且完结token时效性判别(当没有装备token时效性是则表明永久有用);在这个过程中能够有用避免数据被篡改,然后确保数据安全;

对JWT加密和解密方面的中心代码大略如此,其它的引入了一些东西类相似国密加密算法、AES算法及Base64加密算法,这些在完好代码中都有,此处就不逐个展现;GitLab地址:

  • https://gitlab.com/qingsongxi/myjwt

代码结构如下图所示:

img

在这儿,咱们需求界说一个装备文件application.properties,在装备文件中参加相关参数,比方 对称加密密钥、token有用期、需求阻拦的URL等等

# 密钥key
jwt.safety.secret=y2W89L6BkRAFljhN
# token有用期
jwt.valid.time=7
# 需求jwt阻拦的url
jwt.secret-url=/findCustomerById
# 端口
server.port=80

在这儿咱们需求界说一个阻拦器,用来阻拦需求token才干拜访的URL;

/**
 * @author: JiaYao
 * @demand: 自界说web阻拦器
 */

@Slf4j
@Component
public class WebInterceptor implements HandlerInterceptor {

    /**
     * JWT密钥
     */

    @Value("${jwt.safety.secret}")
    private String jwtSafetySecret;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        //  进入阻拦器 WebInterceptor...
        String authorization = request.getHeader("Authorization");
        if (authorization == null || !authorization.startsWith("Bearer ")) {
            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 {
 &nb简略代码完结JWT(json web token)完结SSO单点登录sp;      httpServletResponse.setContentType("text/json; charset=UTF-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(Json.newInstance(Apistatus.CODE_401)));
        return false;
    }

    /**
     * 在未登录状况或登录状况失效时恳求需求登录状况才干恳求的URL
     *
     * @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;
    }

    @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 
{
    }
}


/**
 * @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;
    /**
     * JWT密钥
     */

    @Value("${jwt.safety.secret}")
    private String jwtSafetySecret;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        jwtSecretUrl = jwtSecretUrl.replaceAll(" """);
        registry.addInterceptor(webInterceptor).addPathPatterns(jwtSecretUrl.split(","));
    }

}

到这儿,咱们的JWT小东西基本上就算是现已写完了,只需求整合到详细的事务中就能够开端投入运用,下面编写一个拜访操控层,在里边界说两个办法,一个是恳求登录获取token,另一个是恳求需求登录下才干恳求的资源;

/**
 * 类 名: LoginController
 */

@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;
        }
    }

    /**
     * 依据用户id查询用户信息
     *
     * @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;
        }
    }

}

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

@Service
public class LoginService {

    @Value("${jwt.safety.secret}")
    private String jwtSafetySecret;

    @Value("${jwt.valid.time}")
    private int jwtValidTime;

    /**
     * 登录
     *
     * @param customerId
     * @return
     */

    public String login(String customerId) {
        Customer customer = new Customer();
        customer.setId(customerId);
        customer.setName("jiayao");
        customer.setPhone("1234567890");
        return createTokenString(customer);
    }

    /**
     * 依据id查用户
     *
     * @param customerId
     * @return
     */

    public Customer findCustomerById(String customerId) {
        Customer customer = new Customer();
        customer.setId(customerId);
        customer.setName("jiayao");
        customer.setPhone("1234567890");
        return customer;
    }

    /**
     * 生成token
     *
     * @param customer
     * @return
     */

    public String createTokenString(Customer customer) {
        String jwtToken = null;
        try {
            jwtToken = Jwts.header(Header.SM4, 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();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return jwtToken.replaceAll("\r\n","");
    }

}

在上述代码中,有一个createTokenString的办法,此办法可进一步抽取为一个静态的东西类,在里边咱们指定加密办法和密钥信息、指定token有用战略;

发动项目后咱们经过Postman恳求登录接口获取token信息,如下:

img

如上图所示,经过恳求登录接口咱们成功获取到了token,咱们运用这个token去恳求一个需求登录才干恳求的资源试试;

img
img

如上图所示,经过阻拦器后经过request恳求向里边增加特点claims,将用户数据增加进来,然后进入办法后就能够直接拿到用户数据然后确定是哪个用户登录的,即便在多体系情况下,选用相同的逻辑相同是能够解析的,然后完结单点登录;

在上述代码中还有一个问题是:生成的token在有用期内无法被毁掉,那么就会存在一个安全问题,即用户屡次登录生成多个token,可是前面生成的token仍是处于有用状况,无法被及时毁掉;鉴于这点,能够选用Redis缓存来处理这个问题,而且还能够完结多个体系同享Redis数据然后确保在在同一时刻内只要一个有用的token;

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

GitLab地址:

  • https://link.zhihu.com/?target=https%3A//gitlab.com/qingsongxi/myjwt.git

最终

乐于输出干货的Java技能大众号:Java3y。大众号内有200多篇原创技能文章、海量视频资源、精巧脑图,无妨来重视一下!

有协助?美观!转发!


引荐阅览

请关注微信公众号
微信二维码
不容错过
Powered By Z-BlogPHP